第 2 章 はじめての Windows フォームプログラミング

第1章で紹介したプログラムは、Windowsプログラムではありません。実行中に自分のウィンドウを開くことはありませんし、図を描くこともありませんでした。また、マウスからの入力信号を処理するコードも一切持っていませんでした。ユーザーとの入出力は、Consoleと呼ばれるクラスを通して行われていました。本章では1歩前に進みます。これ以降の章では、ロギングや基本的なデバッギング作業を行う場面以外では、Consoleクラスは一切使われることはありません。

読者の皆さんの中には、「ところで、コンソールアプリケーションとWindowsアプリケーション間の正確な違いとは何だろう?」と自問している人もいることでしょう。おもしろいことに、その疑問への回答を用意する作業は、以前ほど単純ではなくなっています。1つのアプリケーションは、いずれの性格も備えているのです。コンソールアプリケーションとして動作を開始し、その後、Windowsアプリケーションになり、また再度、コンソールアプリケーションとして動作を継続できるのです。Windowsアプリケーションも完全ではないにしても、コンソール出力画面を表示することができます。コンソールアプリケーションは、Windowsメッセージボックスを表示し、発生した問題をユーザーに通知した後に、メッセージボックスを閉じて再度コンソール画面を使用するようなことが可能なのです。

C#コンパイラから見ると、コンソールアプリケーションとWindowsアプリケーション間の相違は、target(tと省略可能)と呼ばれるコマンドラインスイッチだけです。コンソールアプリケーションを作成するには、次のようなスイッチを指定します。

/target:exe

targetスイッチを指定しない場合、コンソールアプリケーションが作成されます(デフォルト)。Windowsアプリケーションを作成する場合、次のようなコマンドラインスイッチを指定します。

/target:winexe

targetスイッチは、ライブラリやモジュールを作成するためにも使用できます。Microsoft Visual Studio .NETでは、プロジェクトのプロパティページダイアログボックスを使用することができます。[共通プロパティ]の[出力の種類]をコンソールアプリケーションかWindowsアプリケーションのいずれかに設定できます。

このコンパイラスイッチは、それほど有用な機能を提供しているとはいえません。実行ファイル内のフラグをセットし、プログラムのロードと実行方法を明示しているだけなのです。実行ファイルのフラグがコンソールアプリケーションにセットされて、Windows環境から実行されると、Windowsオペレーティングシステムはコマンドプロンプトウィンドウを作成し、その後にプログラムを起動しています。このため、プログラムは出力情報をコンソールに表示できるわけです。コンソールアプリケーションがコマンドプロンプトウィンドウから起動した場合には、MS-DOSプロンプトはプログラムが終了するまで復帰しません。一方、実行ファイルのフラグがWindowsアプリケーションにセットされると、コマンドプロンプトウィンドウは一切作成されません。プログラムからのコンソール出力は、すべてビットバケツに入ります。このようなプログラムをコマンドラインから起動すると、MS-DOSプロンプトはプログラム起動後に再度現れます。このことから、次のようなことを理解しておくとよいでしょう。

「Windowsアプリケーションをコンソールアプリケーションとしてコンパイルしても、都合の悪いことは何も起きない!」

しかし、コマンドプロンプトウィンドウの振る舞いは、リリースモードとデバッグモードで異なるということをきちんと理解しておく必要があります。リリースモードに入っている場合、プログラムが終了すると、コンソールウィンドウに「Press any key to continue」というメッセージが表示されます。このメッセージが表示されている間は、コンソールに送信されてきた出力情報を見ることができます。表示情報の確認が済めば、コンソールウィンドウを閉じることができます。デバッグモードでWindowsからプログラムを実行すると、コンソールウィンドウはプログラムが終了した時点で、警告もなく消えてしまいます。この場合、プログラムをシャットダウンする前に、コンソールへの出力情報を確認する必要があります。

本書に付属するすべてのVisual Studio .NETサンプルプロジェクトファイルは、コンソールアプリケーションを作成するように設定してあります。このため、プログラムを実行するときには、コマンドプロンプトウィンドウがまず開かれます。プログラム内でどのようなことが起こっているのか知りたい場合、Console.WriteやConsole.WriteLineステートメントをプログラム内の任意の箇所に記述できますから、コンソールウィンドウを使用することはそれなりのメリットがあります。既に触れたように、デバッグモードでプログラムを動作させている場合には、表示メッセージを確認することはできません。この場合、プログラムが終了した後で、WriteやWriteLineを使ってメッセージを表示するようなコーディングは避けた方がよいでしょう。Console.WriteLineステートメントを2、3箇所で使用すれば、開発中のプログラムの動作はほとんど把握できます。System.Diagnostics名前空間には、Consoleクラスと同等の機能を持つDebugクラスが定義されています。

もちろん、一般消費市場に出荷するWindowsプログラムを、コンソールアプリケーションとしてコンパイルするようなことは、だれもしません。ユーザーは、(UNIXやその流れを汲む環境に慣れていれば別ですが)コマンドプロンプトウィンドウが表示されると、気が動転してしまうでしょう。しかし、コマンドラインスイッチを切り替えるだけの作業ですから、いつでもWindowsプログラムを作成できます。

コンソールアプリケーションとWindowsアプリケーション間の真の相違は、ユーザー入力の受け取り方法にあります。コンソールアプリケーションは、Console.ReadやConsole.ReadLineメソッド経由でキーボード入力を受け取ります。一方、Windowsフォームアプリケーションは、キーボードやその他の入力を、イベント経由で受け取ります。イベントとユーザー入力の関係は、本書の重要な主題であり、以降の各章で詳述しています。

私は、本章のプロジェクトをVisual Studio .NETで作成しています。とは言っても、基本的には第1章と同じです。プロジェクトはVisual C# .NETプロジェクトとして作成していますが、当初は空のプロジェクトとして作成しました。プロジェクト内にプログラムを作成するときには、[新しい項目の追加]メニューを選択し、[ローカルプロジェクトアイテム]で[コードファイル]を指定しました。この手順では、Visual Studio .NETの自動コード生成機能を無効にできます。本書では、自分のコードは自分で用意する、という基本姿勢を堅持したいと思います。

しかし、C#コンパイラは、.NET共通言語ランタイム(Common Language Runtime:CLR)環境の一部であるいくつかのDLLを必要とします。コマンドラインからC#コンパイラを使用する場合には、次のようなreference(rと省略可能)コンパイラスイッチを指定する必要があります。

/r:System.dll,System.Windows.Forms.dll,System.Drawing.dll

Visual Studio .NETで作業する場合も、これらの3種類のファイルを指定する必要があります。ソリューションエクスプローラで、プロジェクト名の下にある[参照設定]を右クリックし、表示されるコンテキストメニューから[参照の追加]を選択します。[プロジェクト]メニューから[参照の追加]を選択しても同じことが行えます。ダイアログボックスにリスト表示されている項目から、次に示す3項目を選択します。

  • System.dll
  • System.Drawing.dll
  • System.Windows.Forms.dll

2.1 | メッセージボックス

私は、本章の冒頭でメッセージボックスに触れました。そこで、多くの人が慣れ親しんでいる例の2語を表示する、小さくとも本物のWindowsフォームプログラムを作ってみたいと思います。

                  
//-----------------------------------------------------
// MessageBoxHelloWorld.cs (C) 2001 by Charles Petzold
//-----------------------------------------------------
class MessageBoxHelloWorld
{
    public static void Main()
    {
        System.Windows.Forms.MessageBox.Show("Hello, world!");
    }
}

                

このプログラムは、第1章で紹介したConsoleHelloWorldプログラムとほとんど同じです。MessageBoxHelloWorldクラス、プログラムのエントリポイントとなるMainメソッド、コンソール版とほとんど差のない実行ステートメントで構成されています。クラス名がかなり長くなっていますが、次のように分解できます。

  • System.Windows.Forms ― 名前空間名
  • MessageBox ― 上記名前空間で定義されているクラス
  • Show ― 上記クラスで定義されている静的メソッド

Showは静的メソッドであるため、クラス名の後に記述する必要があります。クラスから作成されたオブジェクトの名前ではありません。これは、ConsoleクラスのWriteLineメソッドと同じです。このプログラムを実行すると、次のようメッセージボックスが表示されます。

Dd314306.02-01_1(ja-jp,MSDN.10).gif

[OK]ボタンをクリックすると、メッセージボックスが閉じ、Showメソッドが復帰してプログラムが終了します。

System.Windows.Formsは巨大な名前空間であり、約200個のクラスと100個の列挙体、41個のデリゲート、7個のインターフェイス、そして4種類の構造体を持っています。SystemとSystem.Drawing名前空間と並び、本書で紹介する最も大きな名前空間です。これ以降は、次のようなステートメントをWindowsフォームプログラムの先頭に記述することになります。

using System.Windows.Forms;

このため、MessageBoxクラスが持つ静的メソッドであるShowは、次のように記述されます。

MessageBox.Show("Hello, world!");

Windowsを操作したことがある人ならだれでも、表示される多数のメッセージボックスを目にしているはずです。メッセージボックスは、ユーザーに対して簡単なメッセージを表示し、ボタンをクリックしてもらうのが基本です。時には、2、3種類のボタンを表示し、そのうちの1個を選択してもらうような使い方もあります。また、メッセージにアイコンや簡単なキャプションを付け、メッセージボックスの意味を明確にしている開発者もいます。さらに、プログラマによっては、デバッギングのためにメッセージボックスを利用しています。メッセージボックスを使用すれば、すばやくテキスト情報を表示させることができると共に、その表示期間中はプログラムの動作を一時的に停止することもできます。

MessageBoxクラスは、Objectクラスから派生したクラスであるため、Objectクラスに実装されているいくつかのメソッドを継承しています。MessageBox自身が独自に実装しているのは、Showメソッドだけです。このメソッドはstaticメソッドとして定義され、12種類のバージョンが用意されています。次の表に6種類のShowメソッドを整理しておきます。

▼ MessageBoxのShowメソッド(抜粋)

DialogResult Show(string strText)
DialogResult Show(string strText, string strCaption)
DialogResult Show(string strText, string strCaption, MessageBoxButtons mbb)
DialogResult Show(string strText, string strCaption, MessageBoxButtons mbb, MessageBoxIcon mbi)
DialogResult Show(string strText, string strCaption, MessageBoxButtons mbb, MessageBoxIcon mbi, MessageBoxDefaultButton mbdb)
DialogResult Show(string strText, string strCaption, MessageBoxButtons mbb, MessageBoxIcon mbi, MessageBoxDefaultButton mbdb, MessageBoxOptions mbi)


残りの6個のオーバーロードShowメソッドは、Win32コードと共に使用されます。メッセージボックスのキャプションに指定するテキストは、アプリケーション名にするのが一般的です。たとえば、最初のWindowsフォームプログラムでは、次のようにこのメソッドを使用できるでしょう。

MessageBox.Show("Hello, world!", "MessageBoxHelloWorld");

第2の引数にテキストを指定しない場合、キャプションバーには何も表示されません。

メッセージボックスにボタンを表示したい場合には、次に示す列挙体値の1つを指定できます。

▼ MessageBoxButtons列挙体

メンバ  
OK0 
OKCancel1 
AbortRetryIgnore2 
YesNoCancel3 
YesNo4 

たとえば、[OK]ボタンと[キャンセル]ボタンを表示したい場合には、次の列挙体値を指定します。

MessageBox.Show("Hello, world!", "MessageBoxHelloWorld", MessageBoxButtons.OKCancel);

この引数を指定せずにMessageBox.Showを使用した場合、[OK]ボタンがデフォルトで表示されます。AbortRetryIgnore([中止]、[再試行]、および[無視]ボタンを表示)は、MS-DOSが使用していたあの悪名高きメッセージをベースにしています。記憶している方もいるかと思いますが、何らかの理由により応答しないデバイス(通常はフロッピーディスク)にアクセスを試みると、MS-DOSはこのメッセージを表示していました。これらのボタンの使用は、特別な理由がない限り、グラフィック環境では避けた方がよいでしょう。

また、メッセージボックスにアイコンを表示させる場合には、次に示すMessage BoxIcon列挙体値の1つを使用できます。

▼ MessageBoxIcon列挙体

メンバ  
None0x00 
Hand0x10 
Stop0x10 
Error0x10 
Question0x20 
Exclamation0x30 
Warning0x30 
Asterisk0x40 
Information0x40 

列挙体値からわかるように、実際に使用できるのは4種類のアイコンだけです。次のようなコードを記述します。

MessageBox.Show("Hello, world!", "MessageBoxHelloWorld", MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation);

MessageBoxButtonsを指定して2個以上のボタンを表示させている場合、Message BoxDefaultButton列挙体値を使用すれば、デフォルトで有効となるボタンを指定できます。

▼ MessageBoxDefaultButton列挙体

メンバ  
Button10x000 
Button20x100 
Button30x200 

たとえば、次のようなコードを記述できます。

MessageBox.Show("Hello, world!", "MessageBoxHelloWorld", MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button2);

このコードが実行されると、[キャンセル]という2番目のボタンがデフォルトボタンになります。つまり、メッセージボックスが表示されると、この2番目のボタンがフォーカスを持ちます。

Showメソッドで使用できるもう1つの列挙体値は、次に示すMessageBoxOptionsです。

▼ MessageBoxOptions列挙体

メンバ  
DefaultDesktopOnly0x020000 
RightAlign0x080000 
RtlReading0x100000 
ServiceNotification0x200000 

しかし、これらの値が使用されることはめったにありません。

メッセージボックスに複数のボタンを表示する場合には、どのボタンがクリックされたのかを知る必要があります。このような場合、MessageBoxから返される次のような値を判定することになります。

▼ DialogResult列挙体

メンバ  
None0 
OK1 
Cancel2 
Abort3 
Retry4 
Ignore5 
Yes6 
No7 

次のようなコードを用意すれば、MessageBoxから返される値を判定することができます。

                  
DialogResult dr = MessageBox.Show("Do you want to create a new file?",
                                  "WonderWord",
                                  MessageBoxButtons.YesNoCancel,
                                  MessageBoxIcon.Question);
if (dr == DialogResult.Yes)
{
    // [はい]ボタンがクリックされたときの処理
}
else if (dr == DialogResult.No)
{
    // [いいえ]ボタンがクリックされたときの処理
}
else
{
    // [キャンセル]ボタンがクリックされたときの処理
}

                

あるいは、次のように、switchとcaseステートメントを使用してもよいでしょう。

                  
switch (MessageBox.Show("Do you want to create a new file?",
                        "WonderWord",
                        MessageBoxButtons.YesNoCancel,
                        MessageBoxIcon.Question))
{
case DialogResult.Yes:
    // [はい]ボタンがクリックされたときの処理
    break;
case DialogResult.No:
    // [いいえ]ボタンがクリックされたときの処理
    break;
case DialogResult.Cancel:
    // [キャンセル]ボタンがクリックされたときの処理
    break;
}

                

メッセージボックスは、簡潔な説明文などをその場で表示したいような場面ではたいへん便利です。たとえば、「My Documents」という名称をエイリアスに持つWindowsディレクトリの正式名称を表示することにしましょう。この情報は、System名前空間に定義されているEnvironmentクラスから取得できます。このクラスは、1つの引数を受け取るGetFolderPathという静的メソッドを持っています。このメソッドに渡す引数は、Environment.SpecialFolder列挙体のメンバです。2つの名前がピリオドで区切られているのは、SpecialFolderがEnvironmentクラス内で定義されていることを示しています。次のようなコードを記述できます。

                  
//--------------------------------------------------
// MyDocumentsFolder.cs (C) 2001 by Charles Petzold
//--------------------------------------------------
using System;
using System.Windows.Forms;
class MyDocumentsFolder
{
    public static void Main()
    {
        MessageBox.Show(
            Environment.GetFolderPath(Environment.SpecialFolder.Personal),
            "My Documents Folder");
    }
}

                

このコードを実行すると、私の環境では次のような文字列が返されます。

Dd314306.02-02(ja-jp,MSDN.10).gif

2.2 | フォーム

もちろん、メッセージボックスは、Windowsプログラムの本質的な要素ではありません。完全なWindowsプログラムを作成するには、まず、Windowsプログラミングの世界の主役ともいえる「ウィンドウ」を作成する必要があります。.NET世界では、ウィンドウではなく、「フォーム」と呼ばれます。Windowsフォームプログラムは、フォームをメインアプリケーションウィンドウとして作成するのが一般的です。また、アプリケーションは、フォームをダイアログボックスとして使用したりします。

メインアプリケーションウィンドウとして使用されるフォームは、アプリケーション名を表示する「キャプションバー」(「タイトルバー」とも呼ばれます)と、その下に配置されている「メニューバー」、そして「クライアント領域」と呼ばれる内側の領域で構成されるのが一般的です。サイズを調整できる境界線、あるいはフォームの大きさの変更を防止する細い境界線が、フォーム全体を囲んでいることもあります。しかし、第9章までは、私たちのフォームはメニューを持ちません。

ここからは、いくつかの非標準的な方法でフォームを作成し、その後に一般的なフォーム作成方法を詳述することにします。一部の読者は、このようなアプローチに違和感をおぼえると思いますが、フォームとその作成の意味をより深く理解できるようになるでしょう。

それでは、フォームを作成する最も短いプログラムを作成してみます。このプログラムは、ここではNewForm.csと呼びます。

                  
//----------------------------------------
// NewForm.cs (C) 2001 by Charles Petzold
//----------------------------------------
class NewForm
{
    public static void Main()
    {
        new System.Windows.Forms.Form();
    }
}

                

これは冗談ですが、このプログラムよりさらに小さなプログラムというのであれば、より短いクラス名を使用し、コメントを取り除き、余分なスペースとpublicアクセス修飾子(この場合、厳密にはあってもなくてもよい)を削除する以外に方法はないでしょう。

Formは、System.Windows.Forms名前空間に定義されているクラスです。NewFormプログラムは、Formクラスのインスタンスを新規に作成するために、new演算子を使用しています。これまでの説明からわかるように、次のようなusingディレクティブをプログラムの先頭に追加指定すれば、プログラムサイズを大きくすることができます。

using System.Windows.Forms;

追加した後、Main内のソースコードを次のように変更します。

new Form();

あるいは、Form型のオブジェクトを次のように定義してもよいでしょう。

Form formOfMine;

このオブジェクトにnew演算子の結果を次のように代入します。

formOfMine = new Form();

変数定義とオブジェクト代入を1行で行うことができます。

Form formOfMine = new Form();

Formクラスは、ContainerControlから派生していますが、実際には、次に示すような派生階層があります。先頭に位置するのは、既に説明した.NET Framework内のすべてのオブジェクトが派生しているObjectクラスです。

Dd314306.02-03(ja-jp,MSDN.10).gif

Controlというのは、ボタン、スクロールバー、エディットフィールドなどのユーザーインターフェイスを集合的に指す言葉です。このため、Controlは、そのようなオブジェクトをサポートするために必要な基本コードの多くを実装しています。特に、キーボードとマウスによる入力やビジュアル部品の多くを実装しています。Scrollable Controlクラスは、コントロールに自動的にスクロール機能(第4章で詳述)を追加し、ContainerControlクラスは、ダイアログボックスなどが他のコントロールの親として機能するようにします。簡単に言えば、他のコントロールはこのコンテナコントロールの表面に表示されるということになります。

NewFormプログラムは、フォームを作成しているのは確かですが、ちょっとした問題を抱えています。Formクラスのコンストラクタは、作成したフォームを実際には表示しないのです。フォームが作成されても、見えることはありません。プログラムが動作を終了すると、そのフォームは破棄されてしまいます。

2.3 | フォームの表示

ShowFormという次のプログラムは、前のプログラムの欠陥を修正しています。

                  
//-----------------------------------------
// ShowForm.cs (C) 2001 by Charles Petzold
//-----------------------------------------
using System.Windows.Forms;
class ShowForm
{
    public static void Main()
    {
        Form form = new Form();
        form.Show();
    }
}

                

このプログラムには、usingステートメントが含まれているので、入力するコード量は少なくなっています。このステートメントがなければ、2つのFormの前には、System.Windows.Formsを追加する必要があります。プログラム内の小文字のformは、Formクラスのインスタンスを示します。インスタンス名は自由に付けることができます。しかし、Visual Basicのような大文字と小文字を区別しない言語では、小文字のformはインスタンス名として使用できません。コンパイラは、formとFormを区別できなくなるからです。このような場合、Formクラスのインスタンスには、異なる名前を付けるのが基本といえます。

Showは、FormがControlから継承した2つのメソッドのうちの1つです。フォーム(あるいはコントロール)を表示するかどうかは、このメソッドで制御します。

▼ Controlメソッド(抜粋)

メソッド 機能  
void Show()コントロールを見えるようにする。 
void Hide()コントロールを隠す。 

フォームを見えるようにするには、次のようなコードを記述します。

form.Show();

あるいは

form.Visible = true;

最初のShowはメソッドです。Visibleはフィールドのように見えますが、実際はプロパティです。

▼ Controlプロパティ(抜粋)

プロパティ アクセス状態  
boolVisibleget/set 

ShowFormプログラムはフォームを確かに見えるようにしますが、それを見るためには神経を使います。フォームが表示されるとすぐに、消えてしまいます。高速なマシンを使用している場合、おそらくまったく見ることができないと思います。

この振る舞いは、既に紹介したコンソールアプリケーションとWindowsアプリケーション間の相違を端的に語っています。コマンドラインプログラムは、動作が終了しても、出力内容がそのままコンソール画面に残ります。一方、Windowsアプリケーションは、動作が終了すると、ウィンドウとそこに表示した情報を破棄し、リソースを解放します。

それでは、ShowFormプログラムが表示するフォームをじっくり眺めることができるように、プログラムの動作を遅くすることはできないのでしょうか? 皆さんは、sleepという概念をご存知ですか? System.Threading名前空間をじっくり眺めてみるとわかりますが、Threadという名称のクラスがあり、そのクラスはSleepという静的メソッドを持っているはずです。このメソッドは実は、指定された期間(ミリ秒単位)プログラム(より正確には「スレッド」)の動作を停止する機能を実装しているのです。

次に紹介するサンプルプログラムは、Sleepメソッドを2回呼び出し(2.5秒間×2回停止)、フォームを見えるようにしています。

                  
//-------------------------------------------------
// ShowFormAndSleep.cs (C) 2001 by Charles Petzold
//-------------------------------------------------
using System.Threading;
using System.Windows.Forms;
class ShowFormAndSleep
{
    public static void Main()
    {
        Form form = new Form();
        form.Show();
        Thread.Sleep(2500);
        form.Text = "My First Form";
        Thread.Sleep(2500);
    }
}

                

既に気付いていると思いますが、このプログラムではTextプロパティに文字列を設定しています。

▼ Controlプロパティ(抜粋)

プロパティ アクセス状態  
stringTextget/set 

Textはきわめて重要なプロパティの1つです。ボタンコントロールの場合、Textプロパティは、ボタン上に表示されるテキストを保持します。エディットフィールドの場合には、フィールド内の実際のテキストをこのプロパティが管理しています。フォームのTextプロパティは、キャプションバーに表示されるテキストを管理しています。このプログラムを起動すると、2.5秒の間はキャプションがブランクとなっているフォームが表示され、その後にキャプションバーにテキストが表示され、2.5秒経過すると、フォームが消滅します。

このプログラムはSleepメソッドを応用し、前のプログラムに比べるとちょっとした進歩を遂げています。しかし、私は、Sleepメソッドでフォーム表示を強制的に継続させるのは適切ではない、と考えています。

2.4 | アプリケーションの実行

ここで紹介するメソッドは、魔法のメソッドであり、Runという名称を持っています。このメソッドは、System.Windows.Forms名前空間に定義されているApplicationクラスの一部です。ConsoleとMessageBoxクラスと同じように、Applicationクラスからインスタンスを生成することはできません。すべてのメンバは、staticとして定義されています。次に紹介するサンプルプログラムは、フォームを作成してTextプロパティとVisibleプロパティを設定した後に、Application.Runを呼び出しています。

                  
//---------------------------------------------
// RunFormBadly.cs (C) 2001 by Charles Petzold
//---------------------------------------------
using System.Windows.Forms;
class RunFormBadly
{
    public static void Main()
    {
        Form form = new Form();
        form.Text = "Not a Good Idea...";
        form.Visible = true;
        Application.Run();
    }
}

                

このプログラムは、表面上は何の問題も抱えていないといってよいでしょう。次のようなフォームが表示されます。

Dd314306.02-04(ja-jp,MSDN.10).gif

マウスでキャプションバーを掴めば、これまでのようにフォームを画面上で移動することができます。サイズ変更の境界線を使えば、これまたフォームの大きさを自由に変更できます。[最小化]や[最大化]ボタンをクリックすれば、フォームは指示されたとおりに形を変えます。ウィンドウの左隅にあるアイコンをクリックすれば、システムメニュー(Windowsフォームでは「コントロールボックス」とも呼ばれます)を表示することができます。右隅にある[閉じる]ボタンをクリックすれば、ウィンドウ自体を閉じることができます。

しかし、このプログラムは後ではっきりしますが、たいへん重大な欠陥を持っています。フォームを閉じると、Application.Runメソッドは復帰せず、フォームが見えない状態に入っているにもかかわらず、プログラムは動作を続行しているのです。このプログラムをコンソールアプリケーションとしてコンパイルしてみると、このあたりの状況がはっきりします。フォームを閉じても、「Press any key to continue」といういつものテキストメッセージがコマンドプロンプト画面に表示されません。プログラムを終了するには、Ctrl+Cキーを押す以外にありません。このプログラムをコンソールプログラムとしてコンパイルしない場合には、Windowsタスクマネージャを起動して[プロセス]タブを表示し、RunFormBadlyアプリケーションを選択した後に手動で終了する必要があります。このため、私は、コンソールアプリケーションとしてコンパイルしているわけです。Ctrl+Cキーという単純な入力だけでプログラムを終了できます。

次に紹介するサンプルプログラムは、Application.Runメソッドの呼び出しを改善したものです。FormオブジェクトをRunメソッドの引数として渡しています。

                  
//----------------------------------------------
// RunFormBetter.cs (C) 2001 by Charles Petzold
//----------------------------------------------
using System.Windows.Forms;
class RunFormBetter
{
    public static void Main()
    {
        Form form = new Form();
        form.Text = "My Very Own Form";
        Application.Run(form);
    }
}

                

ソースコードを注意してみるとわかるかと思いますが、このプログラムにはShowメソッド呼び出しがありません。また、Visibleプロパティも設定されていません。フォームは、Application.Runメソッドにより自動的に見える状態に移行されます。さらに、このメソッドに引数として渡したフォームを閉じると、制御がMainに戻され、プログラムの動作は適切に終了します。

Win32 APIを使用してきた熟練プログラマは、Application.Runメソッドはプログラムをメッセージループに入れ、受け取ったフォームが実装しているコードからそのメッセージループに終了(quit)メッセージがポストされているのでは、と想像していることでしょう。実はそのとおりなのです。フォームが閉じられると、終了メッセージがポストされているのです。そして、アプリケーションを真のWindowsアプリケーションたらしめているのは、まさにApplication.Runメソッドなのです。

2.5 | さまざまなWindowsアプリケーション

ここでは、Windowsアプリケーションの動作をより詳しく検討します。次のサンプルプログラムをまず見てください。

                  
//-----------------------------------------
// TwoForms.cs (C) 2001 by Charles Petzold
//-----------------------------------------
using System.Windows.Forms;
class TwoForms
{
    public static void Main()
    {
        Form form1 = new Form();
        Form form2 = new Form();
        form1.Text = "Form passed to Run()";
        form2.Text = "Second form";
        form2.Show();
        Application.Run(form1);
        MessageBox.Show("Application.Run() has returned " +
                        "control back to Main. Bye, bye!",
                        "TwoForms");
    }
}

                

このプログラムは動作を開始すると、form1とform2という名称の2つのフォームを作成し、それぞれのキャプションに異なるテキストを設定し、区別できるようにしています。form2はShowメソッドにより表示され、form1はApplication.Runメソッドに渡されています。Application.RunがMainに制御を返すときには、その旨をメッセージボックスに表示させて知らせます。

TwoFormsプログラムを2、3回実行してみると、動作背景がかなりわかると思います。form2を最初に閉じると、form1は何の影響も受けません。Application.RunメソッドからMainメソッドに制御を戻すには、form1を閉じる以外にありません。このとき、メッセージボックスが表示されます。しかし、form1を先に閉じると、2つのフォームが画面から消え、制御がApplication.RunからMainに戻り、メッセージボックスが表示されます。

既に気付いているかもしれませんが、Application.Runメソッドの実装内容がこのプログラムでは重要な意味を持っているのです。このメソッドは、引数として受け取ったフォームが閉じられると、プログラム内で作成されたすべての他のフォームを閉じているのです。Application.RunメソッドにFormオブジェクトを渡さない場合(既に紹介したRunFormBadlyのように)、プログラム内では明示的にApplication. Exitメソッドを呼び出し、制御をMainに戻す必要があります。しかし、Application.Runメソッドを呼び出していない場合、Application.Exitをどこで呼び出せばよいのでしょうか? この疑問への回答を用意する前に、イベントという概念を学ぶ必要があります。このため、イベントとApplication.Exitの関係をまとめて後述することにします。結論を先に言うと、イベントを使えば、Application.Exitメソッドを呼び出し、制御をMainメソッドに返すことができます。

2.6 | フォームのプロパティ

多くの他のクラスと同じように、Formクラスは多数のプロパティを定義し、その祖先から、中でもControlクラスからプロパティを継承しています。既に紹介したプロパティには、TextとVisibleがあります。次に紹介するサンプルプログラムは、多数のプロパティを設定しています。このプログラムコードからフォームを作成して表示することで、多数のプロパティを操作できることがわかると思います。

                  
//-----------------------------------------------
// FormProperties.cs (C) 2001 by Charles Petzold
//-----------------------------------------------
using System.Drawing;
using System.Windows.Forms;
class FormProperties
{
    public static void Main()
    {
        Form form = new Form();
        form.Text            = "Form Properties";
        form.BackColor       = Color.BlanchedAlmond;
        form.Width          *= 2;
        form.Height         /= 2;
        form.FormBorderStyle = FormBorderStyle.FixedSingle;
        form.MaximizeBox     = false;
        form.Cursor          = Cursors.Hand;
        form.StartPosition   = FormStartPosition.CenterScreen;
        Application.Run(form);
    }
}

                

BackColorは、フォームの背景色を決定するプロパティです。これは第3章で説明することですが、ColorはSystem.Drawing名前空間(usingステートメントに注意)で定義されている構造体で、141種類のプロパティを持っています。このプロパティは、実際には色名を表しています。色名については、本書の巻末の色見本を参照してください。

WidthとHeightプロパティは、フォームの表示時の大きさを決定します。これらのプロパティを変更する2つのステートメントは、内部でgetとset動作を実行して、デフォルトのウィンドウ幅を2倍にし、高さを半分に設定しています。

FormBorderStyleは列挙体であり、フォームの境界線の機能と外観を管理すると共に、その他のフォーム要素も定義します。利用できる列挙体の値を次の表に示します。

▼ FormBorderStyle列挙体

メンバ 説明
None0境界なし、キャプションバーなし
FixedSingle1FixedDialogと同じ
Fixed3D23D表示
FixedDialog3ダイアログボックス用
Sizable4デフォルト
FixedToolWindow5コントロールボックスなしの小さなキャプションバー
SizableToolWindow6FixedToolWindowと同じ。ただし、境界線はサイズ変更可

デフォルトのFormBorderStyle.Sizableを利用すると、左側にコントロールボックスを持つキャプションバー、キャプションバーテキスト、[最小化]ボタン、[最大化]ボタン、そしてその右側に[閉じる]ボタンを備えたフォームが表示されます。ToolWindowを選択すると、表示されるフォームは小さなキャプションバーになり、コントロールボックス、[最小化]ボタン、および[最大化]ボタンを持ちません。

このプログラムでは、FormBorderStyle.FixedSingleを選択していますから、ユーザーはフォームのサイズを変更できません。また、MaximizeBoxプロパティをfalseに設定していますから、次に示すように、[最大化]ボタンは無効となっています。

Dd314306.02-05(ja-jp,MSDN.10).gif

Cursorプロパティは、フォームのクライアント領域に表示されるマウスの形状を決定します。StartPositionプロパティは、フォームが最初に表示される位置を定義し、FormStartPosition列挙体であるCenterScreenは、フォームの表示位置をWindowsのデフォルトではなく、画面中央になるように指示します。

FormPropertiesプログラムを検討した人の中には、Windowsフォームアプリケーションの構造に戸惑いを禁じえない人もいるのではないでしょうか? フォームを通してユーザーと対話するためには、Application.Runメソッドを呼び出す必要があるように思えますが、Application.Runはフォームが閉じられるまで制御を戻しません。

簡単に言ってしまえば、私たちが独自のコードを記述する隙がまったくないようにみえます。

2.7 | イベントドリブンと入力

多くのコンソールアプリケーションは、ユーザーとまったく対話しません。基本的なコンソールアプリケーションは、必要な情報をコマンドライン引数として受け取り、処理を行い、そして終了します。コンソールプログラムがユーザーとの対話を必要とするときには、キーボードから必要な情報を受け取ります。.NET Frameworkでは、コンソールプログラムはConsoleクラスのReadあるいはReadLineメソッドを呼び出し、キーボードからの入力を受け取ります。プログラムは、入力を受け取るためにいったん動作を停止した後、受け取ったデータに応じた処理をそのまま続行します。

しかし、グラフィック環境で動作するように記述されたプログラムは、異なる入力モデルを持っています。理由は単純です。複数の入力デバイスをサポートする必要があるからです。プログラムは、キーボードからの入力だけではなく、マウスなどからの入力を受け取る必要があります。また、ボタン、メニュー、スクロールバーなどのコントロールを作成し、ユーザーと間接的に対話することもできます。

理論的には、複数の入力デバイスをサポートするプログラミング環境は、シリアルプーリング技法を応用すれば、すべての入力処理を行うことができます。「シリアルプーリング」では、キーボードからの入力をチェックし、もしなければ、マウスからの入力をチェックします。マウスからの入力もなければ、メニューからの入力をチェックし、メニューはその内部でキーボードとマウスからの入力をチェックします。このように、シリアルプーリングは、次から次へ入力デバイスからの信号をチェックします。Windowsがリリースされる以前は、マウス入力を受け付けるキャラクタモードのPCプログラムは、基本的にシリアルプーリングを採用する必要がありました。

その後、「イベントドリブン」モデルと呼ばれる、複数入力デバイスに適した入力モデルが考え出されました。Windowsフォームに実装されているように、個々の入力タイプはクラス内の異なるメソッドに対応付けられています。特定の入力イベントが発生すると(たとえば、キーボードのキーの押下、マウスの移動、あるいはプログラムのメニュー項目の選択)、対応するメソッドが呼び出されます。この呼び出しは、一見すると、プログラムの外部にあるメソッドが呼び出されているように見えます。

この入力モデルは、慣れないうちは必要以上に複雑に感じます。ユーザーがタイプ入力したり、マウスを動かしたり、ボタンを押したり、スクロールバーをスクロールさせたり、あるいはメニュー項目を選択すると、プログラムはいろいろな方角からやってくるメソッド呼び出しに、忙しく対応しなければなりません。実際には、すべてのメソッドは、同じ実行スレッドに実装されていますから、これほど極端ではありません。重要なことは、多数のイベントが発生しても、プログラムの実行が中断するようなことはないことです。あるメソッドがイベントの処理を完了すると、別のイベントに対応するメソッドが呼び出されるという秩序が形成されているのです。

Windowsフォームプログラムがフォーム上で初期化を完了すると、そのプログラムの動作(つまり、実行されるコード)は、イベントへの応答が整ったという意味を持つのです。プログラムは多くの時間、Application.Runメソッドの奥深くで静かに息を潜め、イベントが発生するのをひたすら待っているのです。Windowsフォームプログラムは、「ステートマシン」であると考えてしまうとわかりやすいかもしれません。マシン状態は、発生するイベントによってその都度決定されます。

イベントは、.NET FrameworkとC#の至る所に織り込まれているため、確実に理解しておく必要があります。イベントは、コンストラクタ、フィールド、メソッド、プロパティと共に、実はクラスのメンバなのです。プログラムがイベントを処理するようにメソッドを定義していれば、そのメソッドは「イベントハンドラ」と呼ばれます。ハンドラの引数は、delegateと呼ばれる関数プロトタイプの定義と一致させることになっています。詳細については後で説明します。

第5章で説明しますが、キーボードイベントには3種類の異なるタイプがあります。たとえば、キーが押されたことを通知するイベントや、キーが離されたことを通知するイベントなどがあります。これらの2つに加えて、特定のキーの組み合わせによって文字コードが生成されたことを通知するイベントがあります。

第6章では7種類のマウスイベントを紹介することになっています。マウスが移動したときや、どのマウスボタンがクリックあるいはダブルクリックされたかを通知してくるイベントなどを詳述します。

第7章ではタイマイベントを紹介します。このイベントは定期的に発生し、一定の時間が経過したことを通知してきます。時計プログラムは、このイベントを応用して、表示時刻を1秒ごとに更新しています。

第8章ではコントロールイベントを取り上げます。ボタンやテキストボックス、あるいはリストボックスなどのコントロールを作成し、それをフォーム上に配置すると、それらのコントロールはイベントを使って情報をフォームに通知してきます。コントロールが発生するイベントは、たとえば、ボタンがクリックされたことや、テキストボックス内のテキストが変更されたことなどを知らせてくれます。

第9章ではメニューを取り上げます。メニューも実は、イベントを使って情報をフォームに通知しています。ドロップダウンメニューが開かれたことを通知するイベント、メニュー項目が選択されたことを通知するイベント、メニュー項目がクリックされたことを通知するイベントなどを紹介します。

かなり風変わりなイベントもあります。しかし、イベントと呼べるかどうかわからないそのイベントは、実は最も重要なイベントの1つです。そのイベントは、Paintイベントと呼ばれています。Paintイベントは、私たちのプログラムに対して、ウィンドウ画面に情報を表示することを通知します。

Paintイベントほど、コマンドラインプログラムとグラフィカルプログラム間の違いを教えてくれるものはありません。コマンドラインプログラムは、必要に応じて出力情報を勝手に表示します。一方、Windowsフォームプログラムは、同じように自由に情報を表示できます。しかし、それは決して望ましいことではありません。Paintイベントは、フォームのクライアント領域の一部あるいはすべてが無効であることを通知し、再描画のタイミングを私たちのプログラムに教えてくれます。

クライアント領域が無効になるということは、どういうことでしょうか? フォームが作成された当初は、すべてのクライアント領域は無効となっています。これは、プログラムが何も描画していないからです。プログラムが受信する最初のPaintイベントは、クライアント領域内に何かを描画せよ、という意味を持っています。

私たちが複数のウィンドウを画面上で移動すると、ウィンドウどうしの描画領域が重なってしまうことがあります。このとき、一部のウィンドウのクライアント領域は、他のウィンドウにより覆われてしまいます。これはクライアント領域の一部を失うことを意味しますが、Windowsはその部分を保存してくれるわけではありません。このため、クライアント領域の一部を失うウィンドウは、別のPaintイベントを受け取ります。最小化されたウィンドウを元の大きさに戻すときも、Paintイベントを受け取ります。

Windowsプログラムは、任意の時点で、自分のクライアント領域全体を再描画できる必要があります。このため、個々のプログラムは、必要なすべての情報を自分で保持していなければなりません(あるいは、すばやくアクセスできるようにしておく必要があります)。Paintイベントに対して適切に応答するようにプログラム構造を設計することは、いろいろな制限をクリアする必要があり、困難な作業です。しかし、要領さえ覚えてしまえば、それほどでもないのです。

2.8 | Paintイベントの処理

イベントを効率的に説明したい場合、具体的な例を挙げることが最良のアプローチといってよいでしょう。プログラム内でPaintイベントを処理する場合、まず、PaintEventHandlerというものを理解する必要があります。これはデリゲートであり、System.Windows.Forms名前空間に次のように定義されています。

public delegate void PaintEventHandler(object objSender, PaintEventArgs pea);

このステートメントは関数プロトタイプのようだ、と感じている方もいることでしょう。その直感は、それほど的を外しているわけではありません。第2の引数は、System.Windows.Forms名前空間に定義されている、PaintEventArgsという名称を持つクラスを受け取ることを示しています。このクラスについては、すぐ後で説明します。

本章で既に紹介したようなプログラム内でPaintイベントを処理するには、次に示すような同じ引数を受け取り、しかもPaintEventHandlerデリゲート型を返す、静的メソッドをクラス内に定義する必要があります。

static void MyPaintHandler(object objSender, PaintEventArgs pea)
{

}

定義後、このイベントハンドラを、次のような特殊な構文を使って、FormクラスのPaintイベントにアタッチします。

form.Paint += new PaintEventHandler(MyPaintHandler);

Paintは、Controlクラスに定義されているイベントであり、継承によりFormクラスの一部となっています。Paintイベントは、+=と-=という2種類の代入演算のみをサポートしています。+=演算子は、メソッドをイベントにアタッチすることにより、イベントハンドラをインストールします。一般的には、次のような構文となります。

object.event += new delegate(method)

イベントからメソッドをデタッチするときには、次のような-=演算子を使用します。

object.event -= new delegate(method)

Paintイベントハンドラは、2つの引数を受け取ります。1つはobjSenderと命名されたオブジェクトであり、残りの1つはpeaと略称されるPaintEventArgsクラスです。最初の引数は、このPaintイベントが適応されるオブジェクトを指しています。この場合には、フォームオブジェクトということになります。オブジェクトは、イベントを発生させる源であるため、「sender」と名付けられています。

PaintEventArgsクラスは、System.Windows.Forms名前空間で定義され、GraphicsとClipRectangleという名称の2種類のプロパティを持っています。これらのプロパティには、読み取り専用属性が付けられています。

▼ PaintEventArgsプロパティ

プロパティ名 アクセス状態 意味
GraphicsGraphicsgetグラフィック出力オブジェクト
RectangleClipRectangleget無効な矩形

Graphicsプロパティは、System.Drawing名前空間に定義されているGraphicsクラスのインスタンスを保持しています。このクラスはFormと共に、Windowsフォームライブラリではきわめて重要なクラスの1つです。フォーム上にグラフィックスやテキストを描画するときには、このクラスを使用します。System.Drawing名前空間は、GDI+と呼ばれるグラフィックプログラムシステムを実装しています。GDI+は、その名称から推測できるように、Windows Graphics Device Interfaceの改良版です。ClipRectangleプロパティについては、第4章で取り上げます。

本書の多くのプログラムでは、次のようなコードが記述されています。

Graphics grfx = pea.Graphics;

このコードは、Paintイベントハンドラ内の最初の行に記述されています。Graphicsオブジェクトの名前は、自由に付けることができます。プログラマによっては、小文字のgraphicsを使用しますが、このオブジェクトはグラフィックス関連コードで頻繁に出てくるため、gなどと1語で表現する人もいます。私はその中間を採用します。

詳細な説明に入る前に、Paintイベントハンドラを実装する実際のプログラムを紹介しておきます。

                  
//-------------------------------------------
// PaintEvent.cs (C) 2001 by Charles Petzold
//-------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class PaintEvent
{
    public static void Main()
    {
        Form form   = new Form();
        form.Text   = "Paint Event";
        form.Paint += new PaintEventHandler(MyPaintHandler);
        Application.Run(form);
    }
    static void MyPaintHandler(object objSender, PaintEventArgs pea)
    {
        Graphics grfx = pea.Graphics;
        grfx.Clear(Color.Chocolate);
    }
}

                

Mainメソッド内でフォームが作成されると、MyPaintHandlerという名称のメソッドがそのフォームのPaintイベントにアタッチされます。プログラムはこのハンドラ内で、PaintEventArgsクラスからGraphicsオブジェクトを取得し、そのオブジェクトのClearメソッドを呼び出しています。このメソッドは、最も単純な描画メソッドの1つであり、Graphicsクラスに定義されています。

▼ Graphicsメソッド(抜粋)

メソッド 意味
void Clear(Color clr)クライアント領域全体を指定された色で塗り替える。

このメソッドが受け取る引数は、Color型のオブジェクトです。Colorクラスについては第3章で詳述します。本章のFormPropertiesプログラムの説明で触れたように、色を指定する場合には、Color構造体の静的プロパティとして実装されている141種類の色名の1つを使用するのが最も簡単です。

プログラムが受け取るおおよそのPaintイベント回数を知るには、次のようなステートメントをMyPaintHandler内に記述するとよいでしょう。

Console.WriteLine("Paint Event");

第3章で紹介する2、3のプログラムは、Paintイベントの発生回数をビジュアルに表示しています。

これから紹介するすべてのWindowsフォームプログラムは、少なくとも、プログラムの先頭に次のようなusingステートメントを持っています。

using System;
using System.Drawing;
using System.Windows.Forms;

一般的には、これらの名前空間を使用すれば、基本的なWindowsフォームアプリケーションをすべて作成できます。表現を変えれば、これらの名前空間がWindowsフォームアプリケーションの基本機能を提供すると考えておいてください。

これらの3種類のusingステートメントと、3個のDLLの対応関係を理解したい人もいるでしょう。プログラムをコンパイルするときには、対応する3個のDLLを参照として指定する必要があります。CやC++プログラマにとっては、usingステートメントが#includeステートメントのように見えるかもしれませんが、それは事実ではありません。どちらかといえば、Visual BasicのWithステートメントに似ています。usingステートメントは、単に完全修飾名の入力作業を軽減するだけの意味しか持ちません。CやC++プログラムのヘッダーファイルが提供する情報(型宣言、関数宣言、クラス定義など)は、参照として指定されたDLLが提供します。これらのDLLは、クラスを実装するために、実行中のプログラムとリンクされます。

2.9 | テキストの表示

Graphicsクラスは、直線、曲線、矩形、円弧、ビットマップイメージなどのグラフィクスを描くための多数のメソッドを提供しています。フォーム内にテキストを表示するGraphicsメソッドは、DrawStringと呼ばれています(ズボンを締めるひもではありません)。

DrawStringメソッドは、6種類のオーバーロードメソッドとして実装されています。しかし、最初の3種類の引数は、すべてのメソッドで同じです。最も単純なDrawStringメソッドを示します。

void DrawString(string str, Font font, Brush brush, float x, float y)

皆さんの中には、このメソッドは表示する文字列、それを表示させる座標値を引数だけを受け取るのではないだろうかと予想した人もいるでしょう。表示するテキストに適応されるフォントや、Brush(テキストの表示色を制御する)と呼ばれる情報を受け取ることなど予想しなかったでしょう。これらの引数は、GDI+が状態を持たない(ステートレス)グラフィックプログラミングシステムであることを暗示してくれています。結論を言ってしまえば、このメソッドは、各種の図形を表示するために必要なほぼすべての情報を受け取っているわけです。

このメソッドの欠点といえば、それは多量の情報を受け取りすぎてしまうことでしょう。このため、1文字だけ表示する場合などは、第2、第3の引数を省略してしまうはずです。あるいは、より簡単なメソッド呼び出しを探し出す人も結構いるようです。

DrawStringメソッドの第1の引数は、表示するテキスト文字列です。たとえば、次のように指定します。

grfx.DrawString("Hello, world!", ...);

それでは次に、他の引数も詳細に検討してみましょう。

2.9.1 | フォント

DrawStringメソッドに渡す第2の引数は、テキストを描画するためのフォントです。これは、System.Drawing名前空間に定義されているFontクラスのオブジェクトです。

Fontクラスの詳細については、第17章で取り上げます。現時点では、Windowsフォームプログラムは、さまざまなサイズのフォントを利用できるとのみ言っておきます。この後に紹介するサンプルプログラムでは当面、デフォルトフォントを使っていきます。ありがたいことに、Controlクラスから派生しているすべてのクラスは、デフォルトフォントを保持するFontという名称のプロパティを継承しています。

▼ Controlプロパティ(抜粋)

プロパティ アクセス状態 意味
FontFontget/setコントロールのデフォルトフォント

プロパティとクラスがいずれもFontという名称を持っていますから、一部の読者は混乱していることでしょう。安心してください。数か月もすれば、混乱から抜け出ることができます。

フォームにPaintイベントをインストールすると、第1の引数を目的のオブジェクトにキャストすることにより、イベントが適応されるオブジェクトを取得できます。キャスト例を次に示します。

Form form = (Form)objSender;

objSenderはForm型のオブジェクトですから、このキャストはうまくいきます。しかし、objSenderがForm型のオブジェクトでない場合(あるいは、Formから派生したクラスのオブジェクトでない場合)、このステートメントが実行されると例外が発生します。このため、イベントハンドラ内では、form.Fontを使って、フォームのデフォルトフォントを参照するとよいでしょう。DrawString呼び出しは次のようになります。

grfx.DrawString(str, form.Font, ...);

複数のDrawStringメソッドを呼び出している場合、まずFont型のオブジェクトを定義し、そのオブジェクトにフォームのデフォルトフォントを代入してもよいでしょう。

Font font = form.Font;

このステートメントはフォントだらけです。最初のFontはSystem.Drawing名前空間に定義されているクラスです。小文字のfontはそのクラスのオブジェクトです。最後のFontはFormクラスのプロパティです。DrawStringメソッド呼び出しは次のように記述できます。

grfx.DrawString(str, font, ...);

より単純に記述する場合には、Fontオブジェクトをfと命名してしまうとよいでしょう。

2.9.2 | ブラシ

DrawStringメソッドに渡す第3の引数は、フォント特性である「色」を管理しています。私は「色」を強調しています。この理由は、この引数は実はBrush型のオブジェクトだからなのです。ブラシは単なる色ではなく、それ以上の機能を備えています。ブラシは、色の組成、魅惑的なパターン、ビットマップイメージなども管理しています。実際、ブラシはすばらしい機能を多数提供しており、かなり強力なクラスです。本来なら、独立した章を設けて説明する必要があるクラスなのです。本書では、第21章が該当します。本章では、便宜的に単純なブラシを使用することにします。複雑なブラシを使って、皆さんを混乱させたくはないのです。

表示する文字に豊かな色彩を与える最も単純な方法は、Brushesクラスを使用することです。このクラスには、sが付き複数扱いの名詞になっていることに注意してください。単数のBrushではありません。このBrushesクラスは、141種類の静的でかつ読み取り専用のプロパティを持っています。既に触れたColorクラスに実装されている色名と同じです。BrushesプロパティはBrush型のオブジェクトを返します。これらは静的プロパティですから、クラス名とプロパティ名を指定するだけで参照できます。たとえば、次のようなコードが記述できます。

grfx.DrawString(str, font, Brushes.PapayaWhip, ...);

ここで一部の読者は、次のように思っていることでしょう。

「確かに多彩な色を選択し、その組成やパターンも変更すれば、テキスト表示は楽しくなるだろう。しかし、現実はどうだろう。表示するテキストの97.5%は、単純な色つまり黒で表示しているはずである。一部の例外を除けば、第3の引数にBrushes. Blackを指定するだけで十分なはずだ」

それでは、Brush型のオブジェクトを次のように定義することとします。

Brush brush = Brushes.Black;

このオブジェクトをDrawString呼び出しで使用する場合には、次のようなコードを記述します。

grfx.DrawString(str, font, brush, ...);

もちろん、brushは単純にbと記述し、タイプ入力量を減らしてもよいでしょう。

しかし、私は、Brushes.Blackをこのように使用することは間違いだと思います。ここには、1つの暗黙の前提があります。それは、フォームの背景色が黒ではないという前提です。この前提は常に真でしょうか? 真ではありません。フォームの背景色が黒の場合、表示させたい文字列は見えなくなってしまいます。

現時点では、これ以上議論を深めないことにします。ただ、フォームのBackColorプロパティをColor.Whiteなどに設定している場合にのみ、Brushes.Blackを使用するようにしてください。つまり、黒い文字列が見えるように、フォームの背景色を選択してください。より高度なアプローチについては、第3章で取り上げます。

2.9.3 | 座標

DrawStringメソッドは、テキスト文字列の表示開始位置となる、水平(x軸)と垂直(y軸)の交点座標を定義する2つの引き数を受け取ります。

数学を専攻した人や、あるいは数学の単位を取得するために苦しんだ経験のある人は、次のような2次元座標を今でも脳裏に思い描くことができるでしょう。

Dd314306.02-06_1(ja-jp,MSDN.10).gif

これは、xy座標(あるいはデカルト座標)と呼ばれ、フランス人数学者で哲学者でもあるルネデカルト(1596-1650)が考案したと言われます。デカルトは、コンピュータグラフィックスを支えている解析幾何の草案者とも言われています (1)。原点座標(0, 0)は中央に設定します。x値は右方向に増え、y値は上方向に増えます。

(1) デカルトが1637年に記した解析幾何に関する論文は、『The Geometry of Rene Descartes』(New York:Dover 1954年)として英語版が入手可能です。

しかし、デカルトが考えた座標は、ほとんどのグラフィック環境で使用されている座標と一致するものではありません。yの値が上方向に向かって増える座標体系は、ほとんどの欧米言語の記述方式と一致しません。また、初期のコンピュータグラフィックスでは、プログラマが直接ビデオメモリにコードを書き込む作業が必要でした。ビデオメモリバッファは、画面上端から始まるように配置されています。これは、コンピュータモニタが画面の上から下に向かって走査するような設計になっているからです。テレビも同じです。しかし、これらの描画方法は、60年も前に考え出されたことを忘れるべきではありません。

グラフィックス環境 (2) と同じように、Windowsフォーム環境でも、デフォルトの座標体系は原点を左上隅に定めています。概略図を次に示します。

(2) 原点を下端に設定したOS/2プレゼンテーションマネージャでは、例外です。考えとしてはすばらしかったのですが、常に予想どおりに動作するとは限りませんでした。プログラマは、ダイアログボックス内のコントロールの位置を指定するときには、下端から開始される座標体系を使う必要がありました。たとえば、ダイアログボックスを下端から設計することを余儀なくされたのです。詳細ついては、Charles Petzold著の『Programming the OS/2 Presentation Manager』(Microsoft Press 1989年)や『OS/2 Presentation Manager Programming』(Ziff-Davis Press 1994年)を参照してください。

Dd314306.02-07(ja-jp,MSDN.10).gif

これはデフォルトの座標ですから、変更することもできます。詳細については第16章で取り上げます。

それでは、ここで座標について整理しておきましょう。座標は、クライアント領域の左上隅からの相対値です。クライアント領域は、フォームのキャプションバーやサイズ変更可能な境界線、あるいはメニューなどが占有していないフォーム内の領域です。PaintEventArgsクラスに定義されているGraphicsオブジェクトを使用するときには、クライアント領域外への描写はできません。これは、クライアント領域以外の画面への描画処理を気にする必要がないことを意味します。

DrawStringメソッドに渡される座標値は、表示テキスト文字列の1番目の文字の左上隅を示しています。座標(0, 0)を指定すると、テキスト文字列はクライアント領域の左上隅に表示されます。

それでは、これまでの説明を基に、次のようなPaintHelloサンプルプログラムを作成してみます。

                  
//-------------------------------------------
// PaintHello.cs (C) 2001 by Charles Petzold
//-------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class PaintHello
{
    public static void Main()
    {
        Form form      = new Form();
        form.Text      = "Paint Hello";
        form.BackColor = Color.White;
        form.Paint    += new PaintEventHandler(MyPaintHandler);
    Application.Run(form);
    }
    static void MyPaintHandler(object objSender, PaintEventArgs pea)
    {
        Form form = (Form)objSender;
        Graphics grfx = pea.Graphics;
        grfx.DrawString("Hello, world!", form.Font, Brushes.Black, 0, 0);
    }
}

                

このプログラムは、決して単純ではありませんが、フォーム上にテキストを表示する最初のプログラムです。テキストは、次に示すように、クライアント領域の左上隅から表示されます。

Dd314306.02-08(ja-jp,MSDN.10).gif

2.10 | Paintイベントプログラミング

Paintイベントハンドラ内に記述するコードには、注意してください。記述したメソッドは頻繁に呼び出されます。時には、予想もしないようなときにも呼び出されることがあります。クライアント領域を中断されることなく、再描画したいようなときに使用するのがベストです。

既に触れたように、単純なデバッグ作業を行う場合には、メッセージボックスを使用するとよいでしょう。しかし、Paintイベントハンドラ内では、MessageBox.Showメソッド呼び出しを記述しないようにしてください。メッセージボックスは、クライアント領域の一部を覆い、別のPaintイベントを発生させます。このイベント発生は、その後も継続して起こってしまうのです。また、Console.ReadやConsole.ReadLineメソッドも、Paintイベントハンドラやその他のハンドラに記述しないようにしてください。Console.WriteとConsoleWriteLineメソッドには、このような危険性はありません。

さらに、繰り返し値を変更するような処理を記述しないようにしてください。本章の最初の方で紹介したWindowsフォームプログラム内では、Fontプロパティにアクセスし、その大きさを2倍にするようなコードをPaintイベントハンドラ内に記述しています。その場合、Paintイベントが発生すると、その度、フォントの大きさが2倍になってしまいます。

すべての描画処理をPaintイベントハンドラ内で行うことは、かなり難しいかもしれません。事実、場面によっては無理です。このため、Windowsフォームは、描画処理がより柔軟に行えるように、2、3のメソッドを用意しています。

FormがControlから継承しているCreateGraphicsメソッドがあります。このメソッドをPaintイベントハンドラ外から呼び出すと、Graphicsオブジェクトを取得できます。2番目のメソッドは、Controlクラスに実装されているInvalidateです。このメソッドを使用すると、Paintイベントハンドラ内から、別のイベントハンドラを呼び出すことができます。これらのメソッドについての使用法は、第5章、第6章、第17章で取り上げます。

2.11 | 複数フォームと複数ハンドラ

Paintイベントハンドラをさらに理解するために、ここでは基本的な機能を、複数のプログラムで実現してみましょう。次に紹介するプログラムは、同一のPaintイベントハンドラを使用する2つのフォームを作成します。

                  
//----------------------------------------------
// PaintTwoForms.cs (C) 2001 by Charles Petzold
//----------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class PaintTwoForms
{
    static Form form1, form2;
    public static void Main()
    {
        form1 = new Form();
        form2 = new Form();
        form1.Text      = "First Form";
        form1.BackColor = Color.White;
        form1.Paint    += new PaintEventHandler(MyPaintHandler);
        form2.Text      = "Second Form";
        form2.BackColor = Color.White;
        form2.Paint    += new PaintEventHandler(MyPaintHandler);
        form2.Show();
        Application.Run(form1);
    }
    static void MyPaintHandler(object objSender, PaintEventArgs pea)
    {
        Form     form = (Form)objSender;
        Graphics grfx = pea.Graphics;
        string   str;
        if(form == form1)
            str = "Hello from the first form";
        else
            str = "Hello from the second form";
        grfx.DrawString(str, form.Font, Brushes.Black, 0, 0);
    }
}

                

コードを見るとわかりますが、FormオブジェクトはMainとPaintイベントハンドラからアクセスできるように、フィールドとして格納されています。Paintイベントハンドラ呼び出しは、プログラム内で作成された2つのフォームのいずれかに適応されます。イベントハンドラは、適応するフォームを、objSender引数(Formオブジェクトにキャストされます)と、フィールドとして格納されているFormオブジェクトを比較して決定しています。余分な作業をいとわなければ、ifとelse構文を次のような1行のステートメントで置き換えてもよいでしょう。

str = "Hello from the " + form.Text;

それでは次に、この逆の構造を持つサンプルプログラムを作ってみましょう。つまり、フォームは1つで、Paintイベントハンドラを2つ用意します。

                  
//-------------------------------------------------
// TwoPaintHandlers.cs (C) 2001 by Charles Petzold
//-------------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class TwoPaintHandlers
{
    public static void Main()
    {
        Form form      = new Form();
        form.Text      = "Two Paint Handlers";
        form.BackColor = Color.White;
        form.Paint    += new PaintEventHandler(PaintHandler1);
        form.Paint    += new PaintEventHandler(PaintHandler2);
        Application.Run(form);
    }
    static void PaintHandler1(object objSender, PaintEventArgs pea)
    {
        Form     form = (Form)objSender;
        Graphics grfx = pea.Graphics;
        grfx.DrawString("First Paint Event Handler", form.Font,
                        Brushes.Black, 0, 0);
    }
    static void PaintHandler2(object objSender, PaintEventArgs pea)
    {
        Form     form = (Form)objSender;
        Graphics grfx = pea.Graphics;
        grfx.DrawString("Second Paint Event Handler", form.Font,
                        Brushes.Black, 0, 100);
    }
}

                

このプログラムは、ハンドラとイベントのおもしろい関係を示しています。複数のハンドラがある場合、すべてのハンドラは順々に呼ばれます。プログラムの最初のハンドラ内のDrawStringは表示座標(0, 0)、2番目のハンドラ内のDrawStringは表示座標(0, 100)をそれぞれ受け取っています。私は、デフォルトのフォントが100ピクセルを超えないという前提でこのプログラムを作っていますが、まったく問題はないようです。

Dd314306.02-09(ja-jp,MSDN.10).gif

2.12 | フォームと継承

これまでは、フォームの作成方法、プロパティの設定方法(キャプションバーに表示するテキスト文字列と背景色の設定)、およびイベントハンドラのアタッチ方法を説明してきました。Paintイベントハンドラをアタッチしたように、キーボード、マウス、メニューなどにもハンドラをアタッチすることができます。

しかし、通常はイベントをこのように使うことはありません。

Formクラスに実装されているすべての機能を使い切るためには、フォームを単純に作成するだけでは十分ではありません。フォームに「なる」必要があります。ControlはScrollableControlを生み出し、ScrollableControlはContainerControlを生み出しています。さらに、ContainerControlはFormを生み出しています。そして、今度は、Formから私たちの独自のフォームを生み出す番です。

プログラム内では、Formを継承するクラスを定義することにより、私たちの独自のフォームを作成できます。まずは、次に紹介するサンプルプログラムをじっくり眺めてください。

                  
//-----------------------------------------------
// InheritTheForm.cs (C) 2001 by Charles Petzold
//-----------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class InheritTheForm: Form
{
    public static void Main()
    {
        InheritTheForm form = new InheritTheForm();
        form.Text = "Inherit the Form";
        form.BackColor = Color.White;
        Application.Run(form);
    }
}

                

ここでは、classステートメントに注目してください。

class InheritTheForm: Form

クラス名に続くステートメントの一部は、: Formとなっています。これは、Inherit TheFormがFormの子孫であり、Formのすべてのメソッドとプロパティを継承することを意味します。

このクラスは、Formから継承しているとはいえ、プログラムのエントリポイントである静的なMainメソッドを持っています。しかし、MainはFormではなく、InheritTheFormのインスタンスを新規に作成しています。InheritTheFormはFormから派生していますから、当然、TextとBackColorという名称を持つプロパティを持っています。これらのプロパティには、プログラム内で新しい値が設定されます。Form型のオブジェクトがApplication.Runに渡されたように、Formから派生する型のオブジェクトもApplication.Runに渡すことができます。

InheritTheFormプログラムは、フォームを作成し、初期化処理を行います(この場合、Textプロパティなどを設定しています)。初期化後、フォームオブジェクトをApplication.Runに渡します。プログラムを改善する場合には、次のように、初期化処理をクラスのコンストラクタに移してみるとよいでしょう。

                  
//-------------------------------------------------------
// InheritWithConstructor.cs (C) 2001 by Charles Petzold
//-------------------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class InheritWithConstructor: Form
{
    public static void Main()
    {
        Application.Run(new InheritWithConstructor());
    }
    public InheritWithConstructor()
    {
        Text = "Inherit with Constructor";
        BackColor = Color.White;
    }
}

                

ここで思い出してほしいのは、コンストラクタは戻り値を持てないことです。また、デフォルトのコンストラクタは引数リストが空になっています。

Formは、最終的にはObjectクラスから始まり、5個のクラス世代を経たクラスです。InheritWithConstructorオブジェクトがMainメソッド内で作成されると、まず、Objectクラスのデフォルトコンストラクタが呼び出されます。次に、MarshalBy RefObjectクラスのデフォルトコンストラクタが呼び出されます。さらに、デフォルトコンストラクタ呼び出しが続き、Formクラスのデフォルトコンストラクタが呼び出され、その後やっと、InheritWithConstructorクラスのデフォルトコンストラクタが呼び出されます。

ソースコードを見るとわかりますが、私はTextとBackColorプロパティの前にオブジェクト名を記述していません。このオブジェクトは、これまでのプログラムではformという名称を与えていました。これらの2つのプロパティは、InheritWith Constructorクラスのプロパティですから、その直前に何も記述する必要がないのです。つまり、このクラスはControlとFormから派生したクラスですから、(TextとBackColorは、確かにもともとはControlとForm内で定義されていても)継承後は、InheritWith Constructorクラスのプロパティと考えてよいのです。

これらのプロパティを何としても修飾したい場合には、次のように、thisキーワードを使用するとよいでしょう。

this.Text = "Inherit with Constructor";
this.BackColor = Color.White;

このthisキーワードは、現在のオブジェクトを明示するときに使用できます。

2.13 | OnPainメソッド

Formクラスのインスタンスを作成するのではなく、このクラスを継承して新たなクラスを作成することには、いったいどのような利点があるのでしょうか? Formに定義されているほとんどのメソッドとプロパティは、publicとして定義されていますが、いくつかの重要なものはprotectedと定義されています。このような属性を持つメソッドとプロパティは、Formクラスの子孫クラスからのみアクセスできるようになっています。たとえば、第3章で紹介するResizeRedrawプロパティは、このようなprotected属性を持っています。

Control経由でFormが継承しているprotectedメソッドの1つに、OnPaintがあります。このメソッドは、直接呼び出したくはないメソッドの1つです。つまり、オーバーライドした方がよいメソッドなのです。実は、オーバーライドすると、Paintイベントハンドラをインストールする必要がなくなるのです。OnPaintメソッドは、PaintEventArgs型のオブジェクトを1つ引数として受け取ります。この引数を使用すれば、Paintイベントハンドラ内で行ったように、Graphicsオブジェクトを取得できます。

それでは、最新のWindowsフォーム版hello-worldプログラムを見てみましょう。

                  
//-------------------------------------------
// HelloWorld.cs (C) 2001 by Charles Petzold
//-------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class HelloWorld: Form
{
    public static void Main()
    {
        Application.Run(new HelloWorld());
    }
    public HelloWorld()
    {
        Text = "Hello World";
        BackColor = Color.White;
    }
    protected override void OnPaint(PaintEventArgs pea)
    {
        Graphics grfx = pea.Graphics;
        grfx.DrawString("Hello, Windows Forms!", Font,
                        Brushes.Black, 0, 0);
    }
}

                

このプログラムこそ、正式に承認されたプログラマの作品です。Windowsフォームクラスライブラリを使用してC#プログラムを作成する場合、このプログラムは貴重なサンプルになるでしょう。このため、プログラム名には、HelloWorldと付けているのです。第3章では、背景とテキスト色のより高級な設定テクニックを紹介することになっています。OnPaint内では、Fontの前に何も記述していません。OnPaintメソッドは、常に現在のフォームオブジェクトを呼び出しますから、thisが暗黙で指定されているのです。もちろん、このメソッドはobjSenderも必要ありません。

Dd314306.02-10(ja-jp,MSDN.10).gif

ここで静かに手を挙げ、次のような質問をする人がいるはずです。

「テキストの表示位置をウィンドウ中央に設定することは可能ですか?」

可能です。詳細については第3章に譲りますが、3種類の方法が用意されています。

2.14 | Mainとは

HelloWorldのようなプログラムを見ていると、Mainメソッドとはいったい何だろうか、と考えてしまうことがあります。Mainは、HelloWorldクラス内のメソッドであり、HelloWorldクラスのインスタンスを作成するという性格も持っています。これは、どこか割り切れない感じがしませんか? まるで、プログラムが1人で自分自身を起動しているように見えます。それでは、HelloWorldクラスのインスタンスが作成されていないときには、Mainメソッドはどのように動作しているのでしょうか?

答えは、Mainメソッドがstaticとして定義されているということです。静的メソッドは、クラスから作成されるすべてのオブジェクトから独立しています。概念的には、オペレーティングシステムはプログラムをメモリにロードし、次のような呼び出しを行って、そのプログラムの実行を開始しています。

HelloWorld.Main();

Mainが静的として定義されていない場合には、この呼び出しは無効です。そして、Main定義部からstaticを削除した場合、コンパイラはエラーメッセージを表示し、エントリポイントがないと知らせてくるはずです。

しかし、一部の読者は、第1章で紹介したC#プログラムのように、Mainメソッド自体を1つのクラス内に定義したいと考える人もいるでしょう。こちらの方がすっきりするからです。もちろん、このアプローチでも問題ありません。事実、開発者の一部はこのような方法を採用しています。次に紹介するサンプルプログラムは、HelloWorldプログラムと機能上は同じですが、プログラム構造はかなり異なります。

                  
//---------------------------------------------
// SeparateMain.cs (C) 2001 by Charles Petzold
//---------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class SeparateMain
{
    public static void Main()
    {
        Application.Run(new AnotherHelloWorld());
    }
}
class AnotherHelloWorld: Form
{
    public AnotherHelloWorld()
    {
        Text = "Another Hello World";
        BackColor = Color.White;
    }
    protected override void OnPaint(PaintEventArgs pea)
    {
        Graphics grfx = pea.Graphics;
        grfx.DrawString("Hello, Windows Forms!", Font,
                        Brushes.Black, 0, 0);
    }
}

                

正直言えば、プログラムアーキテクチャがはっきりしている、このようなプログラムが私は好きです。このため、本書のすべてのプログラムに、このような構造を持たせることは簡単でした。しかし、すべてのプログラムに3行分のコードを追加することを、どうしても正当化することができませんでした。クラス名が増えてしまいますが、これも意味のあることとは思えませんでした。

2.15 | イベントとOn系メソッド

既に触れたように、Controlクラスやこのクラスから派生しているクラス(Formなど)のインスタンスを作成するときには、Paintイベントハンドラをインストールすることができます。インストール時には、次のように、同一の戻り値とPaintEvent Handlerデリゲート引数を受け取る静的メソッドを定義しています。

static void MyPaintHandler(object objSender, PaintEventArgs pea)
{
    // 描画コード挿入
}

定義後、次のようなコードを使って、特定のオブジェクト(たとえば、formなどの名称を持つ)にそのハンドラをインストールします。

form.Paint += new PaintEventHandler(MyPaintHandler);

しかし、Controlクラスから派生しているクラス内では、Paintイベントハンドラをインストールする必要はありません(できたとしても)。OnPaintというprotected属性を持つメソッドをオーバーライドするだけでした。

protected override void OnPaint(PaintEventArgs pea)
{
    // 描画コード挿入
}

さらに学習を進めるとわかりますが、Windowsフォーム内に定義されているすべてのイベントは、同じような構造を持っています。すべてのイベントは、対応するprotectedメソッドを持ちます。そして、それらのメソッドは、Onとイベント名で構成される名称を持ちます。これから取り上げるすべてのイベントの構造は、次のように整理できます。

▼ Controlイベント(抜粋)

イベント メソッド デリゲート 引数
PaintOnPaintPaintEventHandlerPaintEventArgs

この表は、イベントの名前、対応するメソッド、イベントハンドラをインストール時のデリゲート、イベントハンドラとメソッドに渡す引数を示しています。

私もそうだったのですが、OnPaintメソッドは、基本的には事前定義されたPaintイベントハンドラではないか、と考える人もいるでしょう。実は、それは間違いです。実装レベルではまったく異なります。ControlクラスのOnPaintメソッドは、実際にはインストールされているすべてのPaintハンドラを呼び出すように実装されているのです。

このあたりの事情を探ってみましょう。まず、Formクラスから派生しているHelloWorldクラスのようなクラスを作ってみます。クラス名は、HelloWorldから派生させることもあり、InheritHelloWorldとします。

                  
//--------------------------------------------------
// InheritHelloWorld.cs (C) 2001 by Charles Petzold
//--------------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class InheritHelloWorld: HelloWorld
{
    public new static void Main()
    {
        Application.Run(new InheritHelloWorld());
    }
    public InheritHelloWorld()
    {
        Text = "Inherit " + Text;
    }
    protected override void OnPaint(PaintEventArgs pea)
    {
        Graphics grfx = pea.Graphics;
        grfx.DrawString("Hello from InheritHelloWorld!",
                        Font, Brushes.Black, 0, 100);
    }
}

                

最初に基本的な問題を説明しておきます。このプログラムをVisual Studio .NETで作成したとき、いつものようにまずInheritHelloWorld.csという新規C#ファイルを作りました。しかし、プロジェクト内には、HelloWorld.csファイルも含める必要がありました。この追加作業は、[既存項目の追加]ダイアログボックスを表示し、[開く]ボタンの右側にあるドロップダウンリストから[リンクファイル]を指定して行いました。HelloWorld.csファイルのコピーを作りたくなかったからです。

ソースコードを見るとわかるように、Mainメソッド内ではnewキーワードが使われています。これは、すべての親クラス(HelloWorldなど)内に存在するMainメソッドを置き換えることを意味します。また、Visual Studio .NETに対して、プログラムのエントリポイントとなるMainを知らせる必要もあります。私は、この作業をプロジェクトのプロパティページダイアログボックスで行いました。[共通プロパティ]の[スタートアップオブジェクト]にInheritHelloWorldを指定しました。

コマンドラインでC#コンパイラを実行する場合、コマンドラインから2つのソースコードファイルを指定し、次のようなコンパイラスイッチを使ってください。

/Main:InheritHelloWorld

このスイッチにより、どのMainメソッドをプログラムのエントリポイントとして使用するかを明示できます。

既に触れたように、デフォルトコンストラクタを使って、派生クラスのオブジェクトを新規に作成するときには、すべての祖先クラスのデフォルトコンストラクタが呼び出されます。もちろん、最初に呼び出されるのは、Objectクラスのデフォルトコンストラクタです。このプロセスの最後の段階で、HelloWorldコンストラクタが呼ばれ、フォームのTextプロパティを「Hello World」に設定します。そして最後に、InheritHelloWorldクラスのコンストラクタが実行され、Textプロパティを次のように設定します。

Text = "Inherit " + Text;

このプログラムのキャプションバーは「Inherit Hello World」となっていますから、イベントシーケンスには問題ないことがわかります。

InheritHelloWorld内のOnPaintメソッドは、HelloWorld内のOnPaintメソッドをオーバーライドします。InheritHelloWorldが動作を開始すると、座標(0, 100)から「Hello from InheritHelloWorld!」と表示します。これは、HelloWorld内のOnPaintメソッドが実行されていないことを意味します。HelloWorld内のOnPaintメソッドは、オーバーライドされているわけです。

それでは、ここでちょっと異なる処理を行うプログラムを見てみましょう。このプログラムは、HelloWorldを継承するクラスを定義していません。その代わり、HelloWorldクラスのインスタンスを直接作成しています。

                  
//------------------------------------------------------
// InstantiateHelloWorld.cs (C) 2001 by Charles Petzold
//------------------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class InstantiateHelloWorld
{
    public static void Main()
    {
        Form form   = new HelloWorld();
        form.Text   = "Instantiate " + form.Text;
        form.Paint += new PaintEventHandler(MyPaintHandler);
        Application.Run(form);
    }
    static void MyPaintHandler(object objSender, PaintEventArgs pea)
    {
        Form     form = (Form)objSender;
        Graphics grfx = pea.Graphics;
        grfx.DrawString("Hello from InstantiateHelloWorld!",
                        form.Font, Brushes.Black, 0, 100);
    }
}

                

ソースコードをじっくり見てみましょう。まず、InstantiateHelloWorldクラスは、HelloWorldやFormから派生していません。この場合、Objectクラスから派生し、そのクラスの機能を継承するといえます。クラス定義は次のようになっています。

class InstantiateHelloWorld

継承ではなく、このクラスは次のようにHelloWorldのインスタンスを新規に作成し、form変数に保存しています。これは、本章の前半で紹介したプログラムが、Formクラスのインスタンスを作成していたのと同じです。

Form form = new HelloWorld();

このプログラムでは、HelloWorldオブジェクトをForm型の変数に保存しています。このようなことが可能なのは、HelloWorldクラスがFormクラスから派生しているからなのです。HelloWorldオブジェクトを作成するとき、HelloWorldコンストラクタが呼ばれ、フォームのTextプロパティにテキスト文字列「Hello World」を設定します。次のステートメントは、Textプロパティの前にInstantiateという語を挿入します。その後、Paintイベントハンドラをフォームにインストールします。

ところで、InstantiateHelloWorldのクライアント領域に表示されているのは、「Hello from InstantiateHelloWorld!」というテキスト文字列ではありません。表示されているのは、「Hello, Windows Forms!」です。この文字列は、HelloWorldクラス内のOnPaintメソッドが表示するものです。どうしたというのでしょうか?

Controlクラスに定義されているOnPaintメソッドは、インストールされているPaintイベントハンドラを呼び出す機能を持っています。HelloWorldクラスは、OnPaintメソッドをオーバーライドしていますから、その呼び出し処理は行われなくなります。このため、.NETのドキュメントでは、Onプレフィックスの付いたprotectedメソッドをオーバーライドするときには、次のように、基本クラス内のOnメソッドを呼び出すことを推奨しています。

base.OnPaint(pea):

HelloWorldクラスのOnPaintメソッドの先頭にこの行を追加し、Instantiate HelloWorldを再ビルドしてみてください。今度は、予想どおりのテキスト文字列が表示されるはずです。InstantiateHelloWorldは、「Hello from InstantiateHelloWorld!」を表示し、続いて「Hello, Windows Forms!」という文字列も表示してくるはずです。

発生するイベントシーケンスは次のようになります。

  • クライアント領域が無効になると、OnPaintメソッドが呼び出されます。これは、HelloWorldクラス内で定義された、すべての祖先クラスのOnPaintメソッドをオーバーライドするOnPaintメソッドです。
  • HelloWorldクラス内のOnPaintメソッドは、基底クラス内のOnPaintメソッドを呼び出します(ここでは、base.OnPaint呼び出しコードを追加した改訂版HelloWorldを取り上げていますから、注意してください)。この呼び出しにより、Formクラスに実装されているOnPaintメソッドが呼ばれます。しかし、FormはOnPaintメソッドをオーバーライドしているとは思えないので、実際に呼び出されるOnPaintメソッドはControlクラスに実装されているものになります。
  • Controlクラス内のOnPaintメソッドは、すべてのインストール済みPaintイベントハンドラを呼び出します。この呼び出し処理では、InstantiateHelloWorldのMyPaintHandlerメソッドが呼び出されることになります。座標(0, 100)からテキストが表示されるはずです。
  • インストールされているすべてのPaintイベントハンドラが呼び出されると、Control内のOnPaintメソッドは制御をHelloWorldに戻します。
  • HelloWorld内のOnPaintメソッドは、座標(0, 0)にテキスト文字列を表示します。

Windowsフォームドキュメントは、Onメソッドをオーバーライドしたときには、基底クラスのOnメソッドを呼び出すように推奨しています。しかし、ほとんどの場合、この推奨は次のような場面でのみ有効です。

  • 自分でクラスを定義し、そのインスタンスを作成している場面
  • インスタンス内部で、オーバーライドしたOnメソッド用のイベントハンドラをインストールしている場面

このような場面は、それほど発生することはないでしょう。しかし、Onメソッド内で基底クラスの対応メソッドを呼び出す必要も時には発生します。第3章で紹介するOnResizeメソッドは、このようなメソッドの1つです。


Page view tracker