C# で回を回してみた。
描画の基礎
以下のコードで、とりあえずウィンドウを出し、「回」を描画できる。
using System.Drawing;
using System.Windows.Forms;
class DrawTest: Form
{
public static void Main()
{
Application.Run(new DrawTest());
}
public DrawTest()
{
Text = "描画テスト";
ClientSize = new Size(640, 480);
Paint += PaintHandler;
}
private void PaintHandler(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Size s = ClientSize;
int minSize = System.Math.Min(s.Width, s.Height);
if (minSize <= 0) return;
Font font = new Font(Font.Name, minSize * 0.6f);
string str = "回";
g.DrawString(str, font, Brushes.Black, 10, 10);
}
}
実行すると、以下のウィンドウが出る。
このプログラムでは、ウィンドウをリサイズしても自動での再描画は行われず、一旦「回」の一部が隠れる程度にウィンドウを小さくして戻すと、一旦隠れた部分が消えたままになる。
今回は、最終的にリサイズしたか否かにかかわらず短い間隔で再描画を繰り返すようになるため、これへの対処は行わない。
実行方法
今回は、(最近の) Windows に標準搭載されているコンパイラを用いて、これをコンパイルする。
まずは、コンパイラが
C:\Windows\Microsoft.NET\Framework\v4.0.30319
にあるので、このディレクトリを PATH に追加する。
(最後のバージョンを表しそうな部分は環境により違うかもしれないので、自分の環境で確認すること)
続いて、コマンド
csc /target:winexe DrawTest.cs
を実行し、コンパイルを行う。
/target:winexe は、実行時にコマンド プロンプトをダサない exe ファイルを作ることを表している。
DrawTest.cs の部分は、コンパイルするファイルを指定する。
これにより、DrawTest.exe ができる (名前はコンパイル対象に合わせて自動で決まる) ので、これを実行する。
コードの解説
using System.Drawing;
using System.Windows.Forms;
System.Drawing および System.Windows.Forms に属する (標準) ライブラリを、これらの名前の指定を省略して使えるようにする。
class DrawTest: Form
{
}
ウィンドウを表す Form クラスを継承したクラスを作成し、独自の処理を行うウィンドウの記述を行う。
public static void Main()
{
Application.Run(new DrawTest());
}
作成したクラスのインスタンスを生成し、Application.Run メソッドに渡すことで、アプリケーションの実行を開始する。
public DrawTest()
{
Text = "描画テスト";
ClientSize = new Size(640, 480);
Paint += PaintHandler;
}
このクラスのコンストラクタ (インスタンス作成時に実行する処理) を定義する。
ウィンドウタイトルを表す Text を設定する。
ClientSize に Size の値を設定することで、ウィンドウの大きさを描画領域のサイズを基準として設定する。
描画を行うため、Paint イベントのハンドラを設定する。
private void PaintHandler(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Size s = ClientSize;
int minSize = System.Math.Min(s.Width, s.Height);
if (minSize <= 0) return;
Font font = new Font(Font.Name, minSize * 0.6f);
string str = "回";
g.DrawString(str, font, Brushes.Black, 10, 10);
}
Paint イベントのハンドラとして用い、描画時に行う処理を定義する。
まずは、PaintEventArgs.Graphics プロパティをから、描画に使用する Graphics クラスのオブジェクトを取得する。
描画領域の大きさ ClientSize を取得し、Math.Min メソッドでその高さと幅のうち大きくない方を取得する。
この大きさが 0 だった場合、そのまま描画しようとすると例外が発生してしまったので、処理を終了する。
フォント (Font オブジェクト) を作成する。
Form の Font プロパティでフォントを取得し、そのフォント名 (Name) を用いて新しいフォントを作成する。
最後に、DrawString メソッドを用いて文字列 "回" を描画する。
ブラシとして Brushes.Black を指定し、黒色で描画を行う。
テキストの描画領域を取得する
回を回すにあたり、まずは座標の指定に対して「回」がどこに描画されるかを把握したい。
今回は、描画される文字の大きさ (描画される領域) を取得するため、MeasureString と MeasureCharacterRanges の 2 種類のメソッドを試した。
using System.Drawing;
using System.Windows.Forms;
class TextSizeTest: Form
{
public static void Main()
{
Application.Run(new TextSizeTest());
}
public TextSizeTest()
{
Text = "テキストサイズ取得テスト";
ClientSize = new Size(640, 480);
Paint += PaintHandler;
}
private void PaintHandler(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Size s = ClientSize;
int minSize = System.Math.Min(s.Width, s.Height);
if (minSize <= 0) return;
Font font = new Font(Font.Name, minSize * 0.6f);
string str = "回";
g.DrawString(str, font, Brushes.Black, 10, 10);
SizeF textSize = g.MeasureString(str, font);
g.DrawRectangle(Pens.Red, 10, 10, textSize.Width, textSize.Height);
StringFormat sf = new StringFormat();
sf.SetMeasurableCharacterRanges(new CharacterRange[]{ new CharacterRange(0, 1) });
Region[] regions = g.MeasureCharacterRanges(str, font, new RectangleF(10, 10, s.Width, s.Height), sf);
RectangleF strRect = regions[0].GetBounds(g);
g.DrawRectangle(Pens.Blue, strRect.X, strRect.Y, strRect.Width, strRect.Height);
}
}
実行すると、以下のウィンドウが出る。
赤い四角が MeasureString による取得結果を、青い四角が MeasureCharacterRanges による取得結果を表す。
MeasureString による描画サイズの取得
SizeF textSize = g.MeasureString(str, font);
g.DrawRectangle(Pens.Red, 10, 10, textSize.Width, textSize.Height);
Graphics オブジェクトの MeasureString メソッドに描画する文字列と描画に用いるフォントを渡すことで、その文字列をそのフォントで描画したときのサイズを SizeF 型で取得できる。
SizeF には、描画の幅 Width と高さ Height が格納されている。
今回は、サイズを取得したら、その結果を DrawRectangle メソッドで描画する。
描画に用いる色は、赤色 (Pens.Red) を指定した。
サイズだけで位置は取得できないので、DrawString で指定する座標の (10, 10) から描画されると仮定した。
この方法では簡単に描画サイズを取得できるが、上の実行結果を見ると、取得できた領域の中で「回」が上に明らかに偏ってしまっている。
これでは、回転させたときに目立つブレが発生することになり、今回の用途には適さなそうだ。
MeasureCharacterRanges による描画領域の取得
StringFormat sf = new StringFormat();
sf.SetMeasurableCharacterRanges(new CharacterRange[]{ new CharacterRange(0, 1) });
Region[] regions = g.MeasureCharacterRanges(str, font, new RectangleF(10, 10, s.Width, s.Height), sf);
RectangleF strRect = regions[0].GetBounds(g);
g.DrawRectangle(Pens.Blue, strRect.X, strRect.Y, strRect.Width, strRect.Height);
Graphics オブジェクトの MeasureCharacterRanges メソッドを用いて、文字列が描画される領域を取得する。
このメソッドは、描画する文字列と用いるフォントに加え、文字列の描画を指示する領域 (RectangleF)、および指定した文字列のうち描画される領域を求める部分を格納した StringFormat オブジェクトを要求する。
文字列のうち描画される領域を求める部分は、StringFormat オブジェクトの作成後、SetMeasurableCharacterRanges メソッドを呼び出すことで指定する。
このメソッドには、CharacterRange の配列を渡す。
CharacterRange は、コンストラクターに指定する部分の開始位置と長さを渡すことで作成できる。
MeasureCharacterRanges メソッドを呼び出すと、SetMeasurableCharacterRanges で指定した配列に対応する形で取得結果の配列が返る。
取得結果は、Region オブジェクトで表される。
このオブジェクトは複雑な図形の表現に対応しているようだが、今はそこまで求めていないので、GetBounds メソッドで表している図形を囲む四角形を取得する。
すると、位置とサイズを持った四角形 RectangleF が取得できる。
最後に、取得した四角形を DrawRectangle で描画する。
描画色は青色 (Pens.Blue) に設定した。
上の実行結果を確認すると、「回」が取得した領域内で若干中心から左に寄っている気がするが、このくらいなら目立たず、使える範囲だと判断した。
回を回す
以下のコードで、回を回すことができた。
using System.Drawing;
using System.Windows.Forms;
class RotateKai: Form
{
public static void Main()
{
Application.Run(new RotateKai());
}
public RotateKai()
{
Text = "回が回る回";
ClientSize = new Size(640, 480);
DoubleBuffered = true;
Paint += PaintHandler;
Timer timer = new System.Windows.Forms.Timer();
timer.Interval = 20;
timer.Tick += (s, e) => Invalidate();
timer.Start();
}
private void PaintHandler(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Size s = ClientSize;
int minSize = System.Math.Min(s.Width, s.Height);
if (minSize <= 0) return;
Font font = new Font(Font.Name, minSize * 0.6f);
string str = "回";
StringFormat sf = new StringFormat();
sf.SetMeasurableCharacterRanges(new CharacterRange[]{ new CharacterRange(0, 1) });
Region[] regions = g.MeasureCharacterRanges(str, font, new RectangleF(0, 0, s.Width, s.Height), sf);
RectangleF strRect = regions[0].GetBounds(g);
g.TranslateTransform(s.Width / 2, s.Height / 2);
g.RotateTransform((uint)System.Environment.TickCount % 1000 * 360f / 1000f);
g.DrawString(str, font, Brushes.Black, -(strRect.X + strRect.Width / 2), -(strRect.Y + strRect.Height / 2));
}
}
実行すると、以下のウィンドウが出る。
ちらつきの防止
そのまま高頻度での再描画を行ってアニメーションさせると、「回」の表示がちらついてしまった。
そこで、DoubleBuffered プロパティを true に設定することで、ダブルバッファリングを有効化し、ちらつきにくくした。
DoubleBuffered = true;
アニメーションの実行
数十ミリ秒ごとに画面を再描画し、回を回すアニメーションを行う。
そのために、System.Windows.Forms.Timer を用いた。
これは、フォームでの使用に適したタイマーである。
using 指定があるため、System.Windows.Forms を省略して Timer と書ける。
しかし、C# には他の Timer もあるので、事故を未然に防ぐため、今回の Timer オブジェクトの作成では省略せずに System.Windows.Forms.Timer とした。
Timer timer = new System.Windows.Forms.Timer();
timer.Interval = 20;
timer.Tick += (s, e) => Invalidate();
timer.Start();
Timer オブジェクトの作成後、Interval で実行間隔として目指す時間をミリ秒単位で指定する。
Tick イベントのハンドラを設定し、その中で Form の Invalidate メソッドを呼び出して再描画させる。
これらの設定を行った後、Start メソッドを呼び出し、タイマーの動作を開始させる。
回を回しての描画
g.TranslateTransform(s.Width / 2, s.Height / 2);
g.RotateTransform((uint)System.Environment.TickCount % 1000 * 360f / 1000f);
g.DrawString(str, font, Brushes.Black, -(strRect.X + strRect.Width / 2), -(strRect.Y + strRect.Height / 2));
TranslateTransform および RotateTransform を用いて描画の際の変換を指定し、DrawString で「回」を描画する。
この変換により、回が回る。
TranslateTransform は、(dx, dy) を指定し、設定後に座標 (0, 0) を指定して描画すると設定前の座標 (dx, dy) に描画されるような平行移動の変換を設定する。
RotateTransform は、時計回りが正となる度単位の角度を指定し、原点を中心にその角度回転させる変換を指示する。
今回は、TranslateTransform でウィンドウの中心を原点に設定する。
そして、System.Environment.TickCount を用いてミリ秒単位の時刻を取得し、それを 1 秒で 1 周するような角度に変換し、RotateTransform で回転させる。
最後に、DrawString を用いて、MeasureCharacterRanges で取得した描画領域の中心が原点に来るような座標を指定して「回」を描画する。
おわりに
C# で回を回すことができた。
今回のプログラミングには、
文字列を回転させて描画する - .NET Tips (VB.NET,C#...)
も参考になった。


