かつて「ひまつぶし」というフリーソフトがあったらしいです。実際に遊んでみたことはありませんが、パソコン雑誌の説明によると次々とあらわれる「ひま」という文字をクリックするというソフトらしいです。これをやっていると「俺って暇だなあ」という気持ちになるとのこと。
本当に鳩でも分かるC#講座としてこれをやってみようと思います。文字を描画する方法はクリックした場所に文字を描画するでやったとおりです。
まずはランダムに座標を指定して「暇」という文字を表示させます。これは一定間隔をあけて繰り返したいのでタイマーを使います。タイマーを使うのはストップウォッチのようなアプリを作る場合だけではありません。
次に文字を描画したら次に考えることは文字がある部分がクリックされたことを知るためにはどうするか?です。
ランダムに座標を指定して文字を描画する
ランダムに座標を指定して文字を描画する方法を考えます。
最初にフィールド変数として以下を作成します。
Pointsは前回と同じです。ここに文字を表示する場所の座標が格納されます。変数 strに暇という文字列をいれておき、あとで表示したい文字列が変わったときにも対応できるようにしておきます。strFontは使用するフォントです。ここまでは前回のクリックした場所に文字を描画すると同じです。
次にタイマーを作成します。そして乱数を生成するためのオブジェクトも作成します。
1 2 3 4 5 6 7 8 9 |
public partial class Form1 : Form { List<Point> Points = new List<Point>(); string str = "暇"; Font strFont = new Font("MS ゴシック", 20); Timer timer = new Timer(); // タイマーを作成 Random random = new Random(); // 乱数を生成するために必要 } |
Form1クラスのコンストラクタ内でタイマーの初期設定をおこないます。Timer.Tickイベントが発生したら暇という文字を表示するのですが、Timer.Tickイベントが発生する間隔を指定しなければなりません。これがTimer.Interval = 1000;の部分です。単位はミリ秒で1000分の1秒です。1000を指定しているので1秒おきに暇という文字が描画されます。
Timer.Tickイベントが発生したらどうするのか? Timer.Tick += Timer_Tick;としていますが、これはTimer.Tickイベントが発生したらTimer_Tickを実行するという意味です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); timer.Interval = 1000; timer.Tick += Timer_Tick; timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { } } |
ではTimer_Tickのなかはどう書けばいいでしょうか?
random.Nextとすることで0以上で第一引数よりも小さい整数が返されます。0を指定した場合は0が返されます。負数を指定すると例外発生となります。
ここではフォームの幅よりも小さい整数を生成してこれを暇という文字が描画される座標のX座標とし、フォームの高さよりも小さい整数を生成してY座標にしています。
座標が確定したらこれをPointsのなかに格納します。そしてInvalidateメソッドを実行させればOnPaintによって文字が描画されます。
1 2 3 4 5 6 7 8 9 |
private void Timer_Tick(object sender, EventArgs e) { int x = random.Next(this.ClientSize.Width); int y = random.Next(this.ClientSize.Height); Points.Add(new Point(x, y)); this.Invalidate(); } |
文字が描画されている領域を調べる
文字が描画されている部分をクリックしたら文字を消す処理をしなければならないのですが、文字が描画されている部分をどうやって調べればいいのでしょうか? ここではTextRenderer.DrawTextメソッドで文字を描画してその領域の大きさを調べています。そして本当に取得した領域が正しいか確認するために文字のまわりに矩形を描画しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { foreach(Point pt in Points) { // NoPaddingにして、文字列を描画する TextRenderer.DrawText(e.Graphics, str, strFont, pt, Color.Black, TextFormatFlags.NoPadding); // 大きさを計測して、矩形(長方形)を描画する Size nopadSize = TextRenderer.MeasureText(e.Graphics, str, strFont, new Size(this.Width, this.Height), TextFormatFlags.NoPadding); e.Graphics.DrawRectangle(Pens.Blue, pt.X, pt.Y, nopadSize.Width, nopadSize.Height); } base.OnPaint(e); } } |
実際にプログラムを実行してみると正しく動いていることがわかります。
次にクリックされたときの処理を考えたいのですが、クリックされたら文字を削除しないといけません。正確な表現は、実際に削除するというよりOnPaintのなかで描画処理をおこなわないということです。リストのなかから該当する要素を削除すればいいのですが、それをできるようにするためには文字を描画する座標と上記の方法でもとめた矩形を紐付ける必要があります。
簡単な方法として一度文字を試しに描画してみて表示される座標と矩形の関係を調べる、があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { int stringWidth = 0; int stringHeight = 0; protected override void OnLoad(EventArgs e) { Point pt = new Point(10, 10); Graphics graphics = Graphics.FromHwnd(this.Handle); TextRenderer.DrawText(graphics, str, strFont, pt, Color.Black, TextFormatFlags.NoPadding); Size nopadSize = TextRenderer.MeasureText(graphics, str, strFont, new Size(this.ClientSize.Width, this.ClientSize.Height), TextFormatFlags.NoPadding); graphics.Dispose(); // 描画される文字の幅、高さが取得できる stringWidth = nopadSize.Width; stringHeight = nopadSize.Height; } } |
端より向こう側に文字が描画されてしまってはクリックできないのでその分手前に描画しなければなりません。Timer_Tickを修正します。
1 2 3 4 5 6 7 8 |
private void Timer_Tick(object sender, EventArgs e) { int x = random.Next(this.ClientSize.Width - stringWidth); int y = random.Next(this.ClientSize.Height - stringHeight); Points.Add(new Point(x, y)); this.Invalidate(); } |
文字の幅は変更されることはないので描画するたびに調べる必要はなくなりました。
1 2 3 4 5 6 7 |
protected override void OnPaint(PaintEventArgs e) { foreach(Point pt in Points) TextRenderer.DrawText(e.Graphics, str, strFont, pt, Color.Black, TextFormatFlags.NoPadding); base.OnPaint(e); } |
クリックされた部分に暇は存在するか?
クリックされたことを知るにはForm1クラス内に以下のようなメソッドを追加します。
1 2 3 4 5 6 7 |
public partial class Form1 : Form { protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); } } |
そして新しく追加したOnMouseDownのなかに以下のようなコードを追加します。
1 2 3 4 5 6 7 8 9 |
protected override void OnMouseDown(MouseEventArgs e) { // e.X, e.YとやればクリックされたX座標とY座標を取得できる RemoveFromList(new Point(e.X, e.Y)); // 後述 this.Invalidate(); base.OnMouseDown(e); } |
RemoveFromListはクリックされた場所から対応する要素をPointsから取り除きます。
取り除く要素はクリックされた部分の座標が(リストに格納されているX座標とX座標+stringWidthの間であること)と(リストに格納されているY座標とY座標+stringHeightの間であること)の両方を満たすもので最初に見つかったものです。
Points.FirstOrDefault(x => 条件式 )とやれば条件をみたすものがあればPointオブジェクトを返します。なければPoint.IsEmptyを返します。Point.IsEmpty以外が返されたらそれをPointsのなかから取り除きます。
1 2 3 4 5 6 7 8 9 |
public partial class Form1 : Form { void RemoveFromList(Point pt) { Point point = Points.FirstOrDefault(x => x.X < pt.X && pt.X < x.X + stringWidth && x.Y < pt.Y && pt.Y < x.Y + stringHeight); if(!point.IsEmpty) Points.Remove(point); } } |
RemoveFromListメソッドを実行したらそれを反映させるためにフォーム上を再描画します。これでクリックされた暇が消えます。
以下のようにRemoveFromListメソッドを変更すればつぶした暇の数を取得できます。
1 2 3 4 5 6 7 8 9 10 |
int removeCount = 0; void RemoveFromList(Point pt) { Point point = Points.FirstOrDefault(x => x.X < pt.X && pt.X < x.X + stringWidth && x.Y < pt.Y && pt.Y < x.Y + stringHeight); if (!point.IsEmpty) { Points.Remove(point); removeCount++; } } |
そしてOnPaintメソッドを以下のように書き換えればタイトルバーに「暇が○個残っています。あなたがつぶした暇は全部で○個です」と表示されます。
1 2 3 4 5 6 7 8 9 |
protected override void OnPaint(PaintEventArgs e) { foreach(Point pt in Points) TextRenderer.DrawText(e.Graphics, str, strFont, pt, Color.Black, TextFormatFlags.NoPadding); this.Text = String.Format("暇が{0}個残っています。あなたがつぶした暇は全部で{1}個です。", Points.Count, removeCount); base.OnPaint(e); } |
いかがだったでしょうか? 実際につくってみたいと思ったあなたに一言。
あなたって本当に暇ですね。