ぷるぷるの雑記

低レイヤーがんばるぞいなブログ. 記事のご利用は自己責任で.

MFCについてのメモ

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の知識が必要になる. 結局のところMFCWindows API(SDK) の薄いラッパークラスライブラリに過ぎないのである.

さらにMFCをややこしくしているのは、クラスライブラリを謳っておきながら結構なグローバル関数、グローバル変数を含んでいることである. MFCアプリケーションのprintfことAfxMessageBox関数などが良い例だと思う. まあこれはMFCというよりC++/VC++の特徴なのかもしれない.

などとMFCをディスってしまったが、なんやかんや良いフレームワークだと思う. 例えば先ほど泥をかぶってもらったAfxMessageBox関数だが、デフォルト引数を持っているので必須の引数は1個だけである. あれ、やっぱりMFCというよりC++/VC++の仕様じゃん.

参考

ja.wikipedia.org

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メッセージを発信する.

また、他プロセスのプロセスハンドルを取得すれば、自プロセス以外のウィンドウにメッセージを送ることも可能である.

参考

wiki.winehq.org

chokuto.ifdef.jp

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の指定に利用)

参考

learn.microsoft.com

kaitei.net

www.inasoft.org

www.wdic.org

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 リファレンスに記述無し

調べたところによると_MACmac製品のためのdefineらしく、Windowsでは有効になってない模様. 実際Visual Studio上ではlPrivateメンバにアクセスできなかった. 公式リファレンスにもlPrivateの詳しいことは書いてないので、Windows環境ではないものと考えてよい.

WindowsではlPrivateメンバは有効になってない

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文を用いて届いたメッセージごとの処理を実装する.

参考

learn.microsoft.com

stackoverflow.com

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関数である.

参考

www.bnote.net

ダイアログエディターの使い方

ダイアログの編集はダイアログエディターを利用してグラフィカルに行う*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から始まるメッセージのハンドラをダイアログに追加したい場合は、ダイアログのプロパティ->メッセージを編集すればよい.

WMから始まるメッセージのハンドラはプロパティから追加できる

あるあるなのだが、リソースエディタでコントロールを追加したのにIDが定義されていないといわれることがある. その時はだいたいResource.hをincludeし忘れている.

参考

www.bnote.net

まとめ

ネタがたまったらより詳細なMFCに関する記事を書いていきたい

MFC、思ってたよりも奥が深い. でもやっぱり肝となるのはWindowsAPIだなと感じた.

*1:ここでいうWindows SDKVisual Studio 6.0 などに同梱されていたいわゆる古いSDKを指す. 文脈によっては.NETと統合された新しいSDKのことを指すかもしれない..

*2:実体はkernel.dll(or .lib)やuser.dll(or .lib)などのライブラリ

*3:.rc というファイルにはコントロールの位置などの情報がテキスト形式で書かれているので、位置などを微調整したいときはこのファイルを直接いじるのもあり?