MFC(Microsoft Foundation Class)についての知見がまとまってきたので、メモを残しておく. 主観や間違いが多分に含まれていると思うので、話半分に見てください.
MFCとは
MFC(Microsoft Foundation Class)とは、WindowsAPIをC++のクラスの形で提供するためのフレームワークである. MFCの先輩であるWindows SDKではWindowsAPIはC言語の関数として提供されていた.*1 Windows SDKではWindowsAPIをC言語の関数として直接*2呼び出していたが、VC++で扱いやすいようにクラス化したものがMFCである.
ぶっちゃけオブジェクト指向でプログラミングできるからWindows SDKより扱いやすいというのは幻想だと思うが、MFCの最たる利点はVisual Studioとの親和性にあると思う. 実際、1つのウィンドウをただ表示するだけならWindows SDKを使った方がソースコードは少なくて済む. しかし、ライブラリの呼び出しやウィンドウを生成するための儀式は冗長で、関数の引数にとりあえずNULLを指定するということもしばしば.
そこでMFC、もといVisual Studioの出番というわけである. Visual Studioのウィザードを通してプロジェクトを生成すればスケルトンプログラムを用意してもらえるので、コードを一から組み立てるというより、コードを追加していけばそれなりのGUIアプリケーションを作成することが出来る.ではWindows APIを全く知らなくてもMFCを扱えるかというとむしろ逆で、むしろMFCの方がOS特有の罠に陥りやすいのではないかとも思う.
例えばCClientクラスはコンストラクタ内でGetDC関数を実行し、デストラクタ内でReleaseDC関数を実行する. この2つの関数の出自はもちろんWindows APIである. CClientクラスの生成と破棄時にどのようなことが行われているかは秘匿化され、リファレンスを読むかプログラムの実行中に問題が生じない限りは気にも留めないだろう. MFCプログラムの実行中に問題が生じたとき、それを解決するためには結局のところWindows APIの知識が必要になる. 結局のところMFCはWindows API(SDK) の薄いラッパークラスライブラリに過ぎないのである.
さらにMFCをややこしくしているのは、クラスライブラリを謳っておきながら結構なグローバル関数、グローバル変数を含んでいることである. MFCアプリケーションのprintfことAfxMessageBox関数などが良い例だと思う. まあこれはMFCというよりC++/VC++の特徴なのかもしれない.
などとMFCをディスってしまったが、なんやかんや良いフレームワークだと思う. 例えば先ほど泥をかぶってもらったAfxMessageBox関数だが、デフォルト引数を持っているので必須の引数は1個だけである. あれ、やっぱりMFCというよりC++/VC++の仕様じゃん.
参考
MFCとイベントドリブン
MFCが得意とするのはいわゆるイベント駆動プログラムである. 数値計算のようなプログラムはソースコードの書かれている順番がそのままプログラムの実行順になる. つまり、上から順に実行され、一番下まで来るとプログラムが終了してしまう. しかし、電卓のようなGUIアプリケーションがこのような仕様だと、電卓のイコールを押して計算し終えるとアプリケーションが終了してしまい、結果を確認することがほぼ不可能になってしまう.そこでイベント駆動プログラムの出番である. イベント駆動プログラムでは初期化を終えるとメッセージループに入り、メッセージを受信したときにメッセージの種類に応じた関数(イベントハンドラ)を実行し、その処理が終えたら再びメッセージループに入るという動作をする.
メッセージというのは具体的にはMSG構造体(winuser.h)のインスタンスのことで、ユーザーがマウスを動かしたりキーボード入力をするたびに、これらのデバイスは入力をMSG構造体のインスタンスに変換し、システムメッセージキューにプッシュしていく. キューからメッセージをポップした後、MSG構造体に含まれる宛先ウィンドウを調べ、適切なウィンドウにメッセージを送る. ここまではWindowsが自動的に行ってくれるので、アプリケーションを作るプログラマーはメッセージを受信したときのメッセージごとのハンドラをプログラムすればよい.
メッセージはシステム側で用意されているものと、ユーザーが定義できるものがあるがいずれも実態はUINT型の数値である. 前者をシステムメッセージとでも呼ぶと、システムメッセージのうちWM_という接頭辞で始まるメッセージに対するハンドラはすでに名前が決まっている(CWndクラス内で定義されている). 有名どころだと次のようなシステムメッセージとハンドラの組がある.
メッセージ名 | HEX | 意味 | ハンドラ名 |
---|---|---|---|
WM_SIZE | 0x0005 | ウィンドウサイズが変更されたとき | OnSize |
WM_PAINT | 0x000F | ウィンドウに無効領域が発生したとき(再描画が必要な時) | OnPaint |
WM_CHAR | 0x0102 | キーボード入力したとき | OnChar |
システムメッセージは基本的には受信することだけ考えていればいいが、任意のタイミングで任意のウィンドウに送信することもできる. 例えば、ウィンドウの右上の×ボタンを押すとアプリケーションは自分自身にWM_CLOSEメッセージを発信する.
また、他プロセスのプロセスハンドルを取得すれば、自プロセス以外のウィンドウにメッセージを送ることも可能である.
参考
MFCの命名規則
ハンガリアン記法なので変数名から型が推定できる.
接頭辞 | 型 |
---|---|
b | ブーリアン |
h | ハンドル |
p, lp | ポインタ |
n | DWORD |
型の接頭辞でLやWがついているときがあるが、16-bit のMS-DOSの時の名残らしく現在はLもWも32bitを表しているのであまり気にしなくてよい.
メソッドはパスカルケースで、クラスのメンバーにはm_という接頭辞がついている
変数 | 型 | 意味 |
---|---|---|
CWnd::m_hWnd | HWND | CWndインスタンスにアタッチされているウィンドウのハンドル |
CWinThread::m_hThread | HANDLE | 現在のスレッドへのハンドル |
CWinThread::m_pMainWnd | CWnd* | アプリケーションのメインウィンドウへのポインター |
CWinApp::m_hInstance | HINSTANCE | アプリケーションの現在のインスタンス |
HWND(ウィンドウハンドル)やHINSTANCE(インスタンスハンドル)は結局のところすべてHANDLE型のtypedefとして定義されていて、HANDLE型自体はvoid型へのポインタとして定義されているらしい. MFCの命名規則とは関係ないけど結構重要.
型 | 説明 |
---|---|
HWND | ウィンドウハンドル(メッセージの送り先などで利用) |
HHOOK | フックハンドル(イベントをアプリケーションがインターセプトできる) |
HEVENT | イベントハンドル(WaitForSingleObject等で利用) |
HDC | デバイスコンテキスト(描画等で利用) |
HGLRC | OpenGLレンダリングコンテキスト(デバイスコンテキストとはまた別物) |
HINSTANCE | インスタンスハンドル(アプリケーションやDLLの指定に利用) |
参考
MSG構造体とメッセージ
イベントドリブン型アプリケーションのプログラムは、メッセージの通知とハンドラを追加していくのが主な作業となる. メッセージの実体であるMSG構造体はリファレンスによると以下のように定義されている.
typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; DWORD lPrivate; } MSG, *PMSG, *NPMSG, *LPMSG;
だが、winuser.hを見てみると以下のように定義されている.
/* * Message structure */ typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; #ifdef _MAC DWORD lPrivate; #endif } MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
メンバ名 | 型 | 説明 |
---|---|---|
hwnd | HWND | メッセージを受信するウィンドウへのハンドル. メッセージがスレッドメッセージの場合NULL |
message | UINT | WM_XXなどのメッセージ |
wParam | WPARAM | メッセージについての追加情報. |
lParam | LPARAM | メッセージについての追加情報 |
time | DWORD | メッセージが投稿された時刻 |
pt | POINT | メッセージが投稿された時のカーソル位置 |
lPrivate | DWORD | リファレンスに記述無し |
調べたところによると_MACはmac製品のためのdefineらしく、Windowsでは有効になってない模様. 実際Visual Studio上ではlPrivateメンバにアクセスできなかった. 公式リファレンスにもlPrivateの詳しいことは書いてないので、Windows環境ではないものと考えてよい.
Windows上でマウスを動かしたりキーボードを打ったりウィンドウを移動したり縮小したり....、といろいろなタイミングでこのMSG構造体がやり取りされている.
これらのメッセージはシステムのみならず、アプリケーションから送ることもできる.そのためにはSendMessage()またはPostMessage関数を利用する. 両者の違いはメッセージ送信元にコントロールが戻るタイミングが違うだけなので、SendMessage関数の使い方を見ていく. リファレンスには次のように書かれている.
LRESULT SendMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam );
MSG構造体の一部を引数にとることが分かる. hWndをHWND_BROADCAST((HWND)0xffff)にするとシステム内のすべてのメインウィンドウに送信される(ブロードキャスト). wParamとlParamの使い方はMsgによって異なるので、その都度調べるのが良いと思う.
受信側はWndproc関数を使う. リファレンスによる定義はこちら.
LRESULT Wndproc( HWND unnamedParam1, UINT unnamedParam2, WPARAM unnamedParam3, LPARAM unnamedParam4 )
各引数の意味はSendMessage関数と同じようなので省略. Wndproc関数内でswitch~case文を用いて届いたメッセージごとの処理を実装する.
参考
MFCで重要なクラス
MFCで特に重要なCWndクラス、CDialogクラス、CWinAppクラス、は押さえておきたい.
CWndクラス
CWndクラスは名前の通りウィンドウオブジェクトを表すためのオブジェクトクラスである. ダイアログ、ボタン、ピクチャーコントロールなど目に見えるものはすべてこのクラスから派生している. ここで疑問なのが、(Windowsでは)目に見えるもの=ウィンドウオブジェクトとうことらしい. ウィンドウオブジェクトといわゆるウィンドウとはまた少し違う概念のようで、いわゆるウィンドウのことはフレームウィンドウというらしい. CClientDCクラスのコンストラクタにCWnd型へのポインタを渡したり、CWndクラスでOnPaint関数が定義されていることからも、CWnd型=目に見えるものという考えでよいのだと思う.
CWndクラスには多くのメッセージハンドラが実装されているが、メッセージハンドラのように特定のタイミングで呼び出される関数も定義されている. 両者の違いはプロトタイプ宣言でafx_msgというキーワードがついているかで区別することが出来る. afx_msgキーワードがついている関数はメッセージマップに対応するメッセージを登録しなければならない. リファレンスによればafx_msgキーワードはvirtualキーワードの効果を示唆しているが実際には仮想関数ではないらしい. いずれにせよオーバーライドのようなことが出来る.
プロトタイプ宣言 | 呼ばれるタイミング | メッセージマップへの登録 |
---|---|---|
afx_msg void OnPaint() | ウィンドウの一部を再描画する要求が投稿されたとき | 必要 |
virtual void PreSubclassWindow() | ウィンドウがサブクラス化される前 | 不要 |
CDialogクラス
CWndクラスを継承したダイアログボックスを表すクラス. CWndはウィンドウオブジェクト全般を表すようだが、CDialogクラスはダイアログ、いわゆる普通のウィンドウを表すために使用される. ダイアログにモーダルとモードレスの2種類がある. モーダルはダイアログボックスを閉じるまでアプリケーションが停止するが、モードレスダイアログはダイアログボックスを開いてもアプリケーションが停止することがない.
モーダルダイアログを開くときは次のようにする.
CDialog dlg; INT_PTR nResponse = dlg.DoModal(); // これより下にはモーダルを削除しないと進まない
モードレスダイアログを開くときは次のようにする
CDialog dlg; // CWnd* pParentWndは省略可 dlg.Create(IDD_XX, pParentWnd); dlg.ShowWindow(SW_SHOW);
基本的には親子関係を持たせるために親となる作成元のCWnd型へのポインタを渡してダイアログを作成するが、必須ではない模様. 継承の概念が分かっていたら言うまでもないが、CWnd型へのポインタを渡さなければならない場合、CWnd型を継承した型へのポインタを渡しても問題ない.
CWinApp
アプリケーションそのものを表すクラス. おそらく一つのプロジェクトに一つしかない. mfcライブラリによってインスタンス化される. 語弊を恐れずに言うと、MFCアプリケーションにはエントリポイントが存在しない. 正確に言えば、MFCアプリケーションとはmfcvxx.dll (or .lib)をリンクしたアプリケーションで、このライブラリの中にエントリポイントが含まれているらしい.まあでもC言語のアプリケーションもライブラリにあるスタートアップルーチンをリンクしてビルドしているので、MFCの場合スタートアップルーチンの中にt_mainないしmain関数まで定義されていると考えておけば十分な気がする. mfcライブラリ内のメイン関数にはCWinAppをインスタンス化し、InitInstance関数を呼び出すという処理がすでに実装されている. したがってMFCアプリケーションの実質的なエントリポイントはCWinApp::InitInstance関数である.
参考
ダイアログエディターの使い方
ダイアログの編集はダイアログエディターを利用してグラフィカルに行う*3.
コントロールには必ず一意のIDをつけなければならない. このIDはリソースエディタだけでなくソースファイルを含めたプロジェクト全体で共有される. コントロールを追加するとResource.hにそのコントロールのIDに対応した数値を自動でdefineしてくれる. ダイアログエディターから追加したコントロールは1000から順に数値が割り振られる様子.
// Resource.h // プロジェクト生成時から定義されている #define IDR_MAINFRAME 128 #define IDM_ABOUTBOX 0x0010 #define IDD_ABOUTBOX 100 #define IDS_ABOUTBOX 101 #define IDD_HATENATEST_DIALOG 102 // ダイアログエディタから自動追加 #define IDC_BUTTON1 1000 // プロジェクト生成時から定義されている #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 129 #define _APS_NEXT_CONTROL_VALUE 1000 #define _APS_NEXT_SYMED_VALUE 101 #define _APS_NEXT_COMMAND_VALUE 32771 #endif #endif
不思議なことにIDOK(1として#defineされている)とIDCANCEL(2として#defineされている)のdefineの定義がResource.hにされていないが、どこかで別途定義されているらしい.
再びダイアログエディタに戻り、ダイアログ上を右クリックするといくつかの項目が表示されている. この中でもよく使う3つを取り上げる. その3つの項目とはすなわち次である.
- イベントハンドラーの追加
- クラスの追加
- 変数の追加
イベントハンドラーの追加はコントロールを右クリックしないと表示されないので注意. ウィザードではイベントハンドラを紐づけるダイアログのクラス、コントロールがダイアログに発信するメッセージの種類、イベントハンドラ名を決めることが出来る. イベントハンドラを追加すると、以下の3点が自動的に追加される.
逆に言うと、イベントハンドラを消去したい場合は上記の3点を削除すればよい.
クラスの追加だが、なぜかコントロールではなくダイアログのクラスの追加しかできない. コントロール独自のクラスを実装したい場合は、リソースエディタからではなくソリューションエクスプローラー->右クリック->追加->クラス からウィザードを使ってソースファイルを生成したのちに変数の追加を行うとよい.
変数の追加も通常コントロール上で右クリックすれば表示される. ウィザードでは変数として追加するコントロールのID、変数の型、変数名を指定することが出来る. 変数の型は通常いじらなくてもよいが、自作したクラスの型として定義したい場合は自分で選択する必要がある.
また、WMから始まるメッセージのハンドラをダイアログに追加したい場合は、ダイアログのプロパティ->メッセージを編集すればよい.
あるあるなのだが、リソースエディタでコントロールを追加したのにIDが定義されていないといわれることがある. その時はだいたいResource.hをincludeし忘れている.
参考
まとめ
ネタがたまったらより詳細なMFCに関する記事を書いていきたい
MFC、思ってたよりも奥が深い. でもやっぱり肝となるのはWindowsAPIだなと感じた.