はじめに
既存資産であるJavaアプレットを .Net Framework上のC#で実装しなおすこととした。元々のプログラムはこてこての描画アプリであり、移植のための調査を色々実施したところ、一番ネックになりそうなのがXORModeというものであった。今回これをC#で実現するために調べたことをメモっておく。
XORModeとは
XORModeについて、まずはYahoo知恵袋の「javaのGraphicsクラスにあるsetXORModeとは何ですか」を参照してほしい。恐らく何のことかよくわからないと思う。自分もよくわからなかった。
こういう時は、目的から理解するに限る。
例えば、マウスを使った描画プログラムで線を引くとしよう。ユーザの操作としては、線を開始したい位置でマウスクリックし、線を終了する位置でマウスアップするはずだ。その場合JavaだろうがC#だろうが、大体以下のような実装になると思う。
1 マウスクリックのイベントハンドラで、クリックされた座標(x0, y0としよう)を記録する。
2 マウスムーブのイベントハンドラで、マウスが移動した座標と、(x0, y0)をつなぐ線を描画する。これは、まだどこに線を引くかは確定しておらず、線の終了位置を決めるためにユーザがマウスを移動している状態であり、仮の線を引いている状態だ。
(注) この時、マウスムーブする度に、前回のマウスムーブの時に描画した線は消去する。
3 マウスアップのイベントハンドラで、マウスアップされた座標(x1, y1としよう)を取得し、(x0, y0)と(x1, y1)をつなぐ線を描画する。これによって線の描画が確定する。
ここで、(注)に記載している、「前回のマウスムーブの時の線を消去する処理」がなかった場合は、下のようにマウス移動する度に線が増えていってしまう状態になる。
さて、「前回のマウスムーブの時に描画した線の消去」はどうしたらよいだろうか。
前回と同じ線を、今度は色を変えて背景色(この場合は白)で上書きしてあげればよいと思うかもしれない。確かにそれで前回の線は消えるのであるが、線を引く前に元々グラフィックオブジェクトに描画していたものまで白い線で上書きされてしまうのだ。
以下は、この方法で線を何回か描画したものだ。過去に引いた線がかすれているのが分かるだろう。
そこで、XORModeの登場である。XORModeにした状態で同じ内容(同じ色、同じ位置、同じ形等)で2回描画すると、あら不思議、線を引く前に描いていた内容も含め、完全に元の状態に戻るのである。「反転描画」ともいうようである。
即ちマウスムーブ、マウスドラッグなどにより、移動するオブジェクトを描画する際の必須のテクニックといえる(さっきまで知らなかったが(笑))。
そういえば元々のJavaのプログラムを見ると、移動中のオブジェクトを描画する度に、前回描画したオブジェクトをXORModeで上書きする処理がしつこいくらいに繰り返されている。
さて、XORModeが何かイメージが大体の分かったところで、以下、これをC#で実現するために四苦八苦した記録を書くこととしよう。
環境
今回は以下の環境で調査した。
- Windows 10
- Visual Stuido 2017
- Java 1.8 (アプレット)
まずは.Net Frameworkの標準APIのみで
まずは.Net FrameworkでXORModeがないか調べたところ、表示色を反転して線を引くを発見。「ControlPaintクラスのDrawReversibleLineメソッドを使うことで、表示色を反転して線を引くことができます。また、同じくControlPaintクラスのDrawReversibleFrameメソッドで枠を、FillReversibleRectangleメソッドで塗りつぶされた四角を描くことができます。」と書かれている。
これを元に以下の通り実装してみた。動かすにはユーザーコントロールで実装しているので、フォームに張り付ける必要がある。また、 UserControl1_MouseDown
をMouseDownイベントに、UserControl1_MouseMove
をMouseMoveイベントに、UserControl1_MouseUp
をMouseUpイベントに、それぞれVisual Studioで紐づける必要がある。
このソースにおいて、 ControlPaint.DrawReversibleLine
で線の描画を、ControlPaint.FillReversibleRectangle
で塗りつぶした四角形の描画を行っている。どちらもMouseMove時に、前回の描画の消去用と、今回の描画用に2回処理を記載しているのがポイントだ。
using System;
using System.Drawing;
using System.Windows.Forms;
namespace EditorSample
{
public partial class UserControl1 : UserControl
{
/**
* 描画色
*/
Color foreC = Color.Black;
/**
* 背景描画色
*/
Color backC = Color.White;
// マウスイベントの記録
int x0, y0, x1, y1;
enum Status
{
None,
Draw,
}
Status status = Status.None;
public UserControl1()
{
InitializeComponent();
}
private void UserControl1_Load(object sender, EventArgs e)
{
x0 = y0 = x1 = y1 = -1;
}
private void UserControl1_MouseDown(object sender, MouseEventArgs e)
{
Console.WriteLine("mouseDown" + "(" + e.X + "," + e.Y);
// クリック位置を記録
x0 = x1 = e.X;
y0 = y1 = e.Y;
status = Status.Draw;
}
private void UserControl1_MouseMove(object sender, MouseEventArgs e)
{
Console.WriteLine("mouseMove" + "(" + e.X + "," + e.Y);
Graphics g = CreateGraphics();
//-----------------------------
// 移動
//-----------------------------
/* 四角形 .Net標準の方法*/
if (x1 > 0)
{
ControlPaint.FillReversibleRectangle(new Rectangle(this.PointToScreen(new Point(x1, y1)), new Size(10, 10)), foreC);
}
ControlPaint.FillReversibleRectangle(new Rectangle(this.PointToScreen(new Point(e.X, e.Y)), new Size(10, 10)), foreC);
//-----------------------------
// 描画
//-----------------------------
if (status == Status.Draw)
{
Console.WriteLine("mouseDrug" + "(" + e.X + "," + e.Y);
Pen pen = new Pen(foreC);
ControlPaint.DrawReversibleLine(this.PointToScreen(new Point(x1, y1)), this.PointToScreen(new Point(x0, y0)), foreC);
ControlPaint.DrawReversibleLine(this.PointToScreen(new Point(e.X, e.Y)), this.PointToScreen(new Point(x0, y0)), foreC);
}
// 現在の位置を記録
x1 = e.X;
y1 = e.Y;
}
private void UserControl1_MouseUp(object sender, MouseEventArgs e)
{
Console.WriteLine("mouseUp" + "(" + e.X + "," + e.Y);
if (status == Status.Draw)
{
Graphics g = CreateGraphics();
Pen pen = new Pen(foreC);
g.DrawLine(pen, new Point(e.X, e.Y), new Point(x0, y0));
status = Status.None;
}
ControlPaint.FillReversibleRectangle(new Rectangle(this.PointToScreen(new Point(e.X, e.Y)), new Size(10, 10)), foreC);
}
}
}
線、四角形どちらもマウスの移動に追従して問題なく表示される。しかしながら、ControlPaintには、円や多角形に関するメソッドが用意されていない。今回移植するソースには、JavaのGraphicsオブジェクトにおいて、円を描くdrawOvalメソッドや、多角形を描くdrawPolygonメソッドも含まれているため、このままでは実現できないことが分かった。
次の手
そこで、さらに調べた結果、 https://github.com/EWSoftware/ImageMaps/blob/master/Source/WinForms/UnsafeNativeMethods.csに、gdi32.dllを呼び出して円や多角形で反転描画を実現する例が用意されていた。
このソースをプロジェクトに取り込み、以下の通り実装してみた。そうすると確かに円や多角形を反転描画することができた。
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using EWSoftware.ImageMaps;
namespace EditorSample
{
public partial class UserControl1 : UserControl
{
/**
* 描画色
*/
Color foreC = Color.Black;
/**
* 背景描画色
*/
Color backC = Color.White;
// マウスイベントの記録
int x0, y0, x1, y1;
enum Status
{
None,
Draw,
}
Status status = Status.None;
public UserControl1()
{
InitializeComponent();
}
private void UserControl1_Load(object sender, EventArgs e)
{
x0 = y0 = x1 = y1 = -1;
}
private void UserControl1_MouseDown(object sender, MouseEventArgs e)
{
Console.WriteLine("mouseDown" + "(" + e.X + "," + e.Y);
// クリック位置を記録
x0 = x1 = e.X;
y0 = y1 = e.Y;
status = Status.Draw;
}
private void UserControl1_MouseMove(object sender, MouseEventArgs e)
{
Console.WriteLine("mouseMove" + "(" + e.X + "," + e.Y);
Graphics g = CreateGraphics();
//-----------------------------
// 移動
//-----------------------------
/* 円 ライブラリを用いた方法 */
if (x1 > 0)
{
UnsafeNativeMethods.DrawReversibleCircle(g, new Point(x1, y1), 10, new Point(0, 0));
}
UnsafeNativeMethods.DrawReversibleCircle(g, new Point(e.X, e.Y), 10, new Point(0, 0));
/* polygon ライブラリを用いた方法 */
if (x1 > 0) {
List<Point> list_old = new List<Point>();
list_old.Add(new Point(x1, y1));
list_old.Add(new Point(x1 + 20, y1 - 10));
list_old.Add(new Point(x1 + 20, y1 + 10));
UnsafeNativeMethods.DrawReversiblePolygon(g, list_old, true, new Point(0, 0));
}
List<Point> list_new = new List<Point>();
list_new.Add(new Point(e.X, e.Y));
list_new.Add(new Point(e.X+20, e.Y-10));
list_new.Add(new Point(e.X+20, e.Y+10));
UnsafeNativeMethods.DrawReversiblePolygon(g, list_new, true, new Point(0, 0));
//-----------------------------
// 描画
//-----------------------------
if (status == Status.Draw)
{
Console.WriteLine("mouseDrug" + "(" + e.X + "," + e.Y);
Pen pen = new Pen(foreC);
ControlPaint.DrawReversibleLine(this.PointToScreen(new Point(x1, y1)), this.PointToScreen(new Point(x0, y0)), foreC);
ControlPaint.DrawReversibleLine(this.PointToScreen(new Point(e.X, e.Y)), this.PointToScreen(new Point(x0, y0)), foreC);
}
// 現在の位置を記録
x1 = e.X;
y1 = e.Y;
}
private void UserControl1_MouseUp(object sender, MouseEventArgs e)
{
Console.WriteLine("mouseUp" + "(" + e.X + "," + e.Y);
if (status == Status.Draw)
{
Graphics g = CreateGraphics();
Pen pen = new Pen(foreC);
g.DrawLine(pen, new Point(e.X, e.Y), new Point(x0, y0));
status = Status.None;
}
}
}
}
ただし、このライブラリは塗りつぶしが用意されていないのだ。まぁ、必要なかったので実装していないだけだろう。とはいえ、もうこのライブラリにすがるしかないので、何とか塗りつぶしを実現せねば。引き続きGoogleで思いつく限りのキーワードで検索しまくった。
とりあえず、円
どう探したかは覚えていないが、取り急ぎ円については、ライブラリの UnsafeNativeMethods.DrawReversibleCircle
メソッドのIntPtr oldBrush = SelectObject(hDC, GetStockObject(NULL_BRUSH));
のところで色が指定されていないので、ここを
IntPtr oldBrush = SelectObject(hDC, CreateSolidBrush(ColorTranslator.ToWin32(backColor)));
に置き換えてあればbackColorで指定したColorオブジェクトの色で塗りつぶされることが分かった。
最後の難関 Polygon
最後の難関は多角形の塗りつぶしだ。ライブラリのDrawReversiblePolygon
はLineを何回か引いて多角形を作成していた。これだと原理的に中身を塗りつぶすことは難しそうだ。そこで、再度Googleで検索したところ、http://wisdom.sakura.ne.jp/system/winapi/win32/win29.html
にPolygon
というまさにドンピシャのネイティブAPIが用意されており、SetPolyFillModeを設定して呼び出すと塗りつぶせそうだった。そこで、次の通りやってみた。
まずは、ライブラリのソースを修正し、PolygonとSetPolyFillModeをインポートする設定を追加した。またPolygon関数に多角形の座標を与えるためのPOINTAPIの構造体も定義した。
public struct POINTAPI
{
public int x;
public int y;
}
[DllImport("gdi32.dll")]
public static extern int Polygon(IntPtr hDC, ref POINTAPI lpPoint, int nCount);
[DllImport("gdi32.dll")]C
public static extern int SetPolyFillMode(IntPtr hdc, int nPolyFillMode);
そして以下のFillReversiblePolygonメソッドを実装した。ポイントは、SetPolyFillMode(hDC, 2);
で全部塗りつぶすよう指示をしているところと、.NetのListに入ったPointを、POINTAPIの配列に変換して、Polygon関数の引数に設定しているところだ。
internal static void FillReversiblePolygon(Graphics g, List<Point> points, Point offset, Color backColor)
{
IntPtr hDC = g.GetHdc();
SetPolyFillMode(hDC, 2);
IntPtr pen = CreatePen(PS_SOLID, 1, ColorTranslator.ToWin32(Color.Black));
IntPtr brush = CreateSolidBrush(ColorTranslator.ToWin32(backColor));
int oldROP = SetROP2(hDC, R2_NOTXORPEN);
IntPtr oldBrush = SelectObject(hDC, brush);
IntPtr oldPen = SelectObject(hDC, pen);
SetBkColor(hDC, ColorTranslator.ToWin32(Color.White));
POINTAPI[] pointsArray = new POINTAPI[points.Count];
for (int i = 0; i < points.Count; i++)
{
pointsArray[i].x = points[i].X;
pointsArray[i].y = points[i].Y;
}
Polygon(hDC, ref pointsArray[0], Enumerable.Count(points));
SelectObject(hDC, oldPen);
SelectObject(hDC, oldBrush);
SetROP2(hDC, oldROP);
DeleteObject(pen);
DeleteObject(brush);
g.ReleaseHdc(hDC);
}
さあ、お膳だてはととのった。ユーザーコントロールから呼び出してみよう。先ほどのソースの UnsafeNativeMethods.DrawReversiblePolygon
の呼び出しのところを以下のように変えてみる。Color.Blackは塗りつぶし色を示している。
/* polygon ライブラリを用いた方法 */
if (x1 > 0) {
List<Point> list_old = new List<Point>();
list_old.Add(new Point(x1, y1));
list_old.Add(new Point(x1 + 20, y1 - 10));
list_old.Add(new Point(x1 + 20, y1 + 10));
UnsafeNativeMethods.FillReversiblePolygon(g, list_old, new Point(e.X, 0), Color.Black);
}
List<Point> list_new = new List<Point>();
list_new.Add(new Point(e.X, e.Y));
list_new.Add(new Point(e.X+20, e.Y-10));
list_new.Add(new Point(e.X+20, e.Y+10));
UnsafeNativeMethods.FillReversiblePolygon(g, list_new, new Point(e.X, 0), Color.Black);
そうすると多角形についても、黒に塗りつぶされた状態で、マウスに追従して問題なく表示された。ふぅー、やっと終わった。
おわりに
たかがマウスムーブの間のオブジェクトの表示という、描画処理のメインではないところの技術的な調査に、大幅な時間を要してしまったが、開発とはそんなものであろう。
最大の懸念事項がクリアされたので、いよいよ様々なオブジェクトたちの描画処理を移植するというメインディッシュをいただくとしよう。
追記(2020/1/5)
UnsafeNativeMethods.DrawReversiblePolygonメソッドにおいて、CreateSolidBrushで作成したBrushをDeleteObjectで削除する処理をいれていなかったため修正。これをしないと、長時間使った場合に、グラフィックリソースを使い果たしてしまう恐れがある。