今回は「クリックした場所に文字を描画」するという、これまた単体では面白くもなんともないプログラムを作成します。前回の最後のほうで語ったように「世の中に存在する面白いものや役に立つものは、それだけではつまらなく面白くないものから作られている」のです。ただ本当に面白くないと誰も見てくれないので、面白くする努力はしています。
今回作成するものはフォーム上をクリックするとそこに「鳩」という文字が描画されるプログラムです。しかもゲームを作ろうとするとクライアント領域に画像や文字を描画するテクニックはよく使うので、将来的には役にたつものであると自負しております。
では、やってみましょう。
前回の本当に鳩でも分かるC#講座をやってみるでつかったものを使うのもありです。この場合、ボタンは必要ないので削除してしまいましょう。ボタンを選択してDeleteキーをおせばボタンは消えます。以下のコードも消してしまいましょう。
| 1 2 3 4 5 6 7 8 9 10 11 | private void button1_Click(object sender, EventArgs e) {     if(this.BackColor == Color.Red)         this.BackColor = Color.Blue;     else if(this.BackColor == Color.Blue)         this.BackColor = Color.Green;     else if (this.BackColor == Color.Green)         this.BackColor = Color.Yellow;     else         this.BackColor = Color.Red; } | 
まず、クリックした場所に文字を描画したいのであれば、クリックされたことを検知する仕組みを作らなければらないのですが、これは難しく考える必要がありません。以下のコードに4行追加するだけです。他にもMouseDownイベントを使う方法もあるのですが、コピペで簡単にできるのでこの方法を採用します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | namespace WinFormsApp1 {     public partial class Form1 : Form     {         public Form1()         {             InitializeComponent();         }         // この4行を追加する         protected override void OnMouseDown(MouseEventArgs e)         {             base.OnMouseDown(e);         }     } } | 
OnMouseDownはマウスボタンがクリックされたときに呼び出されるのですが、なかにはbase.OnMouseDown(e);としか書かれていません。これではクリックしてもなにもおきないので以下のように書いてみます。
最初にひとこと言っておくと、これはあまりいい方法ではありません。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | namespace WinFormsApp1 {     public partial class Form1 : Form     {         public Form1()         {             InitializeComponent();         }         // クリックされたときに表示したい文字列(今回は1文字だけ)         string str = "鳩";         // クリックされたときに表示したい文字列のフォント         Font strFont = new Font("MS ゴシック", 20);         protected override void OnMouseDown(MouseEventArgs e)         {             // Graphicsを取得             Graphics graphics = Graphics.FromHwnd(this.Handle);             // 文字列を描画             graphics.DrawString(str, strFont, new SolidBrush(Color.Black), new Point(e.X, e.Y));             // 終わったらgraphicsは破棄すること             graphics.Dispose();             // この部分はとりあえずそのままでOK             base.OnMouseDown(e);         }     } } | 
最初にクリックされたときに表示したい文字列をフィールド変数として定義しています。これはあとになって「鳩」ではなく「烏」(カラス)にしたくなるかもしれません。そのときはこの部分を「烏」に変更だけで対応できます。いまのところ「鳩」は一箇所にしかないのですが、そのうち「鳩」が何度も使われるかもしれません。そんなときに「鳩」をぜんぶ「烏」に置き換えなければなりません。しかし
人間とはミスをする動物である!
だから一箇所だけ変更すればよいようにわざわざstrというフィールド変数をつくっているのです。
次に表示させたい文字列のフォントを Font strFont = new Font(“MS ゴシック”, 20);としています。最初の引数を”MS ゴシック”、二番目の引数を20にすることでフォントの大きさを20ポイントに指定しています。これもあとになって気が変わるかもしれないので、ここを変えれば対応できるようにフィールド変数にしてあります。
文字をフォーム上に描画するのであればGraphicsオブジェクトを取得しなければなりません。それがこの部分です。
| 1 2 | // Graphicsを取得 Graphics graphics = Graphics.FromHwnd(this.Handle); | 
Graphicsを取得したらこれをつかって文字を描画します。文字はどこにどんな色でどのフォントを使って描画するのか? それは以下のように指定すれば、クリックされた位置に黒でMS ゴシック 20ポイントで描画されます。
| 1 2 | // 文字列を描画 graphics.DrawString(str, strFont, new SolidBrush(Color.Black), new Point(e.X, e.Y)); | 
終わったらgraphicsを破棄します。IDisposable インターフェースを実装するオブジェクトは、使い終わった時に必ず Dispose メソッドを呼び出して破棄しなければならないと決められています。これを忘れてしまうとパソコンが大爆発するような大惨事にはなりませんが、「IDisposable インターフェースを実装するオブジェクトは、使い終わった時に必ず Dispose メソッドを呼び出して破棄しなければならない」と決められているのでここは従っておきましょう。
| 1 2 | // 終わったらgraphicsは破棄すること graphics.Dispose(); | 
これで完成と言いたいのですが、実はこのプログラムには欠陥があります。
実際に実行してクリックしてください。クリックされた場所に「鳩」という文字が描画されるはずです。クリックされた位置が「鳩」の真ん中にならず右上になるのはここではたいした問題ではありません。フォームのサイズを小さく変更してみてください。また右上にある「_」ボタンをおして最小化させたあともどのサイズに戻してください。描画されていた鳩が消えてしまいます。鳩はいったいとこへ飛んでいってしまったのでしょうか?
Winndowsを使っていると複数のウィンドウが重なる場合があります。その場合、下にあるウィンドウの一部が上にあるウィンドウによって隠されてしまいます。そして下にあるウィンドウの一部をクリックすると上下関係がかわって隠れていた部分がみえるようになります。
まあこんなことはWinndowsを使っている方であれば誰でも知っていることですが、なぜこんなことが可能になるのでしょうか? それは隠れていた部分が再描画されるからです。そして再描画されるときにクリックして描画した「鳩」も再描画してほしいのですが、そのための処理はどこにも書かれていないので鳩は消えてしまうのです。
つまりウィンドウが再描画されるときに鳩も復元する処理が必要です。
Form1クラスのなかにOnPaint(PaintEventArgs e)を追加してください。すると全体はこうなるはずです。namespace WinFormsApp1の部分は省略しています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public partial class Form1 : Form {     public Form1()     {         InitializeComponent();     }     string str = "鳩";     Font strFont = new Font("MS ゴシック", 20);     protected override void OnMouseDown(MouseEventArgs e)     {         // 省略     }     protected override void OnPaint(PaintEventArgs e)     {         base.OnPaint(e);     } } | 
OnPaintのなかで再描画の処理がおこなわれます。ここに鳩を復元する処理を書けばよいですね。ではそのようにしてみましょう。
まずクリックされたら鳩をどこに描画するのかを記憶させる必要があります。そんなときに便利なのがListクラスです。
| 1 2 3 4 5 6 | public partial class Form1 : Form {     List<Point> Points = new List<Point>();     // それ以外はとりあえずそのままでOK } | 
そしてOnMouseDown(MouseEventArgs e)の部分を以下のように書き換えます。
| 1 2 3 4 5 6 7 8 9 10 | protected override void OnMouseDown(MouseEventArgs e) { 	// クリックされた座標をPointsのなかに保存する     Points.Add(new Point(e.X, e.Y));     // 再描画を要求する     this.Invalidate();     base.OnMouseDown(e); } | 
クリックされたらクリックされた座標をPointsのなかに格納していきます。そしてそのあとthis.Invalidate();とありますが、これはフォームに対して再描画を要求する(正確な表現ではありませんが、だいたいそういう意味です)ものです。するとさきほど作成したOnPaintの部分がが実行されます。ここにPointsのなかに保存していた座標の部分に「鳩」という文字が描画されるのです。
| 1 2 3 4 5 6 7 | protected override void OnPaint(PaintEventArgs e) {     foreach(Point pt in Points)         e.Graphics.DrawString(str, strFont, new SolidBrush(Color.Black), pt);     base.OnPaint(e); } | 
foreachは要素をひとつずつ取り出して処理をおこないます。Pointsのなかに保存されている座標は全部でいくつあるのかを考える必要はないので非常にありがたいです。
実際にこれを実行してクリックして「鳩」を描画したあとフォームを最小化したり、他のウィンドウの後ろに隠してふたたび表示させてみてください。今度は鳩は消えずに残っています。
当たり前の話かもしれませんが、プログラムを終了してしまったら鳩は消えてしまいます。
それではプログラムを終了しても鳩が消えない方法を考えてみましょう。ひとつの方法としてプログラムが終了するときに鳩の座標をファイルとして保存しておき、プログラムが起動したら自動的にこれを読み取って鳩を復元するという方法が考えられます。
Form1.csというファイルの上のほうに
| 1 2 3 4 5 6 7 8 9 | using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; | 
と書かれているはずです。この下に以下の2行を追加してください。
| 1 2 | using System.IO; using System.Xml.Serialization; | 
まずプログラムが終了するときにファイルを保存したいので、ファイルを保存する場所を決めます。Application.UserAppDataPathはユーザーのアプリケーション データのパスを取得します。
もし自分一人でしか使っていないパソコンであればユーザーのアプリケーション データのパスではなく、このプログラムの実行ファイルがあるパスでもかまいません。ユーザーのアプリケーション データのパスはプロジェクト名や実行ファイルのバージョンを変えると変わってしまうのでこちらのほうがわかりやすいかもしれません。その場合はApplication.UserAppDataPathではなくApplication.StartupPathにします。当然のことながらApplication.UserAppDataPathとApplication.StartupPathは別の場所です。
そしてprotected override void OnClosed(EventArgs e)を追加してください。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public partial class Form1 : Form {     // これを追加     string FilePath = Application.UserAppDataPath + "\\data.xml";     // または string FilePath = Application.StartupPath + "\\data.xml";     // これを追加     protected override void OnClosed(EventArgs e)     {         base.OnClosed(e);     }     // 追加ここまで     // それ以外の部分はとりあえずそのままでOK } | 
それから新しいクラスをつくります。簡単なクラスなのでメニューからクラスの追加を選ばずにForm1.csの一番したに書いてよいと思います。Form1が存在する名前空間と同じにしておきましょう。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | namespace WinFormsApp1 {     public partial class Form1     {         // Form1クラス内はここでは省略     }     // 新しく作成したDocクラス     public class Doc     {         public List<Point> Points;     } } | 
ではForm1クラスのOnClosedメソッドを以下のように編集します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | protected override void OnClosed(EventArgs e) {     // Docクラスのインスタンスを生成してdoc.PointsにForm1.Pointsをセットする     Doc doc = new Doc();     doc.Points = this.Points;     // これをXMLファイルとして保存する     XmlSerializer serializer = new XmlSerializer(typeof(Doc));     StreamWriter sw = new StreamWriter(FilePath);     serializer.Serialize(sw, doc);     sw.Close();     base.OnClosed(e); } | 
これでファイルが保存されます。もし保存場所としてApplication.StartupPath~を選択したのであればbin\Debug\net5.0-windowsフォルダのなかを調べてみてください。data.xmlというファイルが生成されているはずです。
実際にファイルを開いてみると数字が異なっているとは思いますが、こんな内容になっていると思います。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?xml version="1.0" encoding="utf-8"?> <Doc xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <Points>     <Point>       <X>219</X>       <Y>88</Y>     </Point>     <Point>       <X>185</X>       <Y>115</Y>     </Point>     <Point>       <X>132</X>       <Y>93</Y>     </Point>     <Point>       <X>132</X>       <Y>42</Y>     </Point>   </Points> </Doc> | 
さてでは次回プログラムを起動したとき、これをもとに鳩を復元できるようにしてみましょう。
まず本当に上記で保存したファイルは存在するのでしょうか? Debugフォルダをまるごと削除してしまったとか、プロジェクト名やバージョンを変更した場合、ファイルを探しに行く場所にはファイルは存在しません。そこで例外処理をおこなっています。ピッチャーが暴投しても優秀なキャッチャーがいれば例外は怖くありません。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public partial class Form1 : Form {     protected override void OnLoad(EventArgs e)     {         try         {             // 例外(エラー)が発生するかもしれない処理をここに書く             // 前回終了時に保存しておいたファイルからデータを読み込む             // 読み込んだXMLファイルからDocオブジェクトを取得する             XmlSerializer serializer = new XmlSerializer(typeof(Doc));             StreamReader sr = new StreamReader(FilePath);             Doc doc = (Doc)serializer.Deserialize(sr);             sr.Close();             // Docオブジェクトが取得できたら座標のリストをthis.Pointsにセット             this.Points = doc.Points;         }         catch         {             MessageBox.Show("例外発生です。");         }         base.OnLoad(e);     } } | 
これで前回終了時に存在した鳩を起動時に復元することができるようになりました。
