WindowsのプログラムをCで作成するとき、クラスを使わない方法(SDK:Software Development Kit)と使う方法(MFC:Microsoft Foundation Class Library)がありますが、それぞれに必要な知識を比較すると次のようになるのではないでしょうか。
| 知識 | SDK | MFC |
1 | C 言語 | ○ | ○ |
2 | Windowsの仕組み(イベントドリブン等) | ○ | ○ |
3 | Windows API | ○ | ○ |
4 | オブジェクト指向の考え方 | | ○ |
5 | C++ 言語 | | ○ |
6 | クラスライブラリ(MFC) | | ○ |
この表のようにMFCはSDKに比べ、多くの知識が必要になります。
GARAさんも最初にWindowsプログラムを作ろうとしたとき、クラスを使うとプログラムが簡単に作れるという宣伝を真に受けクラス(TURBO C++)を使おうとしました。
解説書に書いてあるとおりにプログラムを作ると、確かに動くプログラムができるのですが、それに自分が作りたい機能を実装しようとすると、どこに手を加えればよいのかが全くわからず、結局クラスの使用をあきらめてSDKを使うことになりました。Windowsの仕組みやそれをプログラムから利用する方法を理解するにはSDKのほうがよいという話はネットでもよく見かけます。
SDKでのプログラム作成に慣れればそれで間に合うことが多いので、Windowsプログラムは全てSDKで作っている人も多いと思います。
このTipsではSDKで作成したWindowsプログラムをMFCのプログラムに変換する方法をまとめてみました。
SDKのプログラムをとりあえずMFCのプログラムに変換するための作業はそれほど多くありません。
オブジェクト指向やC++について何となくわかっていれば、以下のTipsと
変換サンプルを参考にすれば
変換要領を1日で修得することも可能だと思います。
WndProc() で行っていたメッセージ処理(
WM_CREATE、WM_PAINT、WM_COMMAND 等の処理)はメッセージハンドラ関数で行います。メッセージマップはメッセージとメッセージハンドラ関数の対応を定義します。
1.WndProc() の中で使用しているメッセージをもとにマップを作成します。
2.メッセージマップは「 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) 〜 END_MESSAGE_MAP() 」で囲みます。
BEGIN_MESSAGE_MAP() の第1引数:このメッセージマップを所有するクラスの名前、第2引数:クラスの基本クラス名
3.メッセージハンドラ関数は基底クラスのメンバ関数をオーバーライドする場合と、独自の関数を作成する場合があります。
4.オーバーライドする場合は、対応する関数名が決まっているのでマップエントリの引数は空。
ON_WM_CREATE()、ON_WM_PAINT()、ON_WM_LBUTTONDOWN( )、ON_WM_DESTROY()
5.独自のメッセージハンドラ関数の作成が必要なのは、メッセージに含まれるコントロールIDや通知コードをもとに処理を行う場合で、マップエントリの引数にコントロールIDや通知コードと独自関数名を記述します。
ON_COMMAND_RANGE(BTN_ID1, BTN_ID4, OnBtn)、ON_NOTIFY(TCN_SELCHANGE, TAB_ID1, OnTab)
以下は
Sample_B2.cpp のメッセージマップの定義です。
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) // 第1引数:メッセージマップを所有するクラス名、第2引数:基本クラス名
ON_WM_PAINT() // オーバーライド
ON_COMMAND(IDM_ITEM01, OnMenu_01) // メニュー IDM_ITEM01 に関数 OnMenu_01 が対応
ON_COMMAND(IDM_ITEM02, OnMenu_02) // メニュー IDM_ITEM02 に関数 OnMenu_02 が対応
ON_COMMAND_RANGE(IDM_ITEM11, IDM_ITEM14, OnMenu) // メニュー IDM_ITEM11〜IDM_ITEM14 に関数 OnMenu が対応
ON_WM_DESTROY() // オーバーライド
END_MESSAGE_MAP()
メッセージとメッセージハンドラ関数の対応の詳細についてはMSDNの次の箇所を参照して下さい。
目次 → MSDNライブラリ Visual Studio6.0 → Visual C++ ドキュメント → リファレンス → Microsoft Foundation Classe リファレンス → Microsoft Foundation Class ライブラリ → クラスライブラリリファレンス → 構造体、スタイル、コールバック関数とメッセージマップ → メッセージマップ
(Microsoft の HP は
こちら)
ウィンドウクラスの宣言の中のメンバ関数宣言にメッセージハンドラ関数を加えます。
以下は
Sample_B2.cpp のメッセージハンドラ関数の宣言です。
afx_msg void OnPaint(); // WM_PAINT に対応
afx_msg void OnMenu_01(); // WM_COMMAND に対応(1対1)
afx_msg void OnMenu_02(); // WM_COMMAND に対応(1対1)
afx_msg void OnMenu(UINT); // WM_COMMAND に対応(4対1)
afx_msg void OnDestroy(); // WM_DESTROY に対応
DECLARE_MESSAGE_MAP()
1.頭にオマジナイの afx_msg をつけます。
2.メッセージハンドラ関数は基底クラスのメンバ関数をオーバーライドする場合と、独自の関数を作成する場合があります。
3.オーバーライドする関数の名前は
WM_xxxx から類推できる名前になっています。(Microsoft の HP は
こちら)
WM_CREATE → OnCreate()、 WM_PAINT → OnPaint()、 WM_LBUTTONDOWN → ON_WM_LBUTTONDOWN()
4.独自のメッセージハンドラ関数
コントロールID(ボタンID等)や通知コードをもとにした処理を行うときは独自のメッセージハンドラ関数を作成します。
独自の関数を作成するといっても、関数の型や引数をどうすればよいのでしょうか。これを調べるには MSDNをキーワード(
ON_COMMAND や
WM_NOTIFY 等)で検索してサンプルを探し、それに合わせます。(もっとうまい調べ方があるのかもしれませんが)
ただ、ほとんどのものは下記の3つのパターンを参考にすれば対応できます。
@コントロールID(メニューIDやボタンID)毎にメッセージハンドラを定義する(1対1で定義する)
以下は WM_COMMAND の例です。
ON_COMMAND(BTN_ID1, OnBtn_1) ← メッセージマップ
afx_msg void OnBtn_1(); ← メンバ関数宣言
void CMainFrame::OnBtn_1() {....} ← 関数
A複数のコントロールID(メニューIDやボタンID)をまとめてメッセージハンドラを定義する(N対1で定義する)
以下は WM_COMMAND の例です。
ON_COMMAND_RANGE(BTN_ID1, BTN_ID5, OnBtn) ← メッセージマップ
afx_msg void OnBtn(UINT); ← メンバ関数宣言
void CMainFrame::OnBtn(UINT wParam) {....} ← 関数
OnBtn() の引数名を wParam にしておくと、メニュー項目IDやボタンIDはこれまでと同じ LOWORD(wParam) になります。
B通知メッセージコードとコントロールIDに対応するメッセージハンドラを定義する
以下は WM_NOTIFY の例です。
ON_NOTIFY(TCN_SELCHANGE, TAB_ID1, OnTab_1) ← メッセージマップ
afx_msg void OnTab_1(NMHDR *, LRESULT *); ← メンバ関数宣言
void CMainFrame::OnTab_1(NMHDR *pNMHDR, LRESULT *pResult) {....} ← 関数
ON_NOTIFY_RANGE もあります。
5.メッセージハンドラ関数の宣言の最後に「 DECLARE_MESSAGE_MAP() 」を記述します。
WndProc() をメッセージハンドラ関数に変換します。従来のプログラムのコードを若干手直しするだけで変換できます。
1.メッセージ毎にメッセージハンドラ関数に変換します。
switch(uMsg) { ← 廃止
case WM_PAINT ← void CMainFrame::OnPaint() に変更
...
... ← この部分は下記3、4を参照
...
break; ← return; に変更
↓
このようになります
void CMainFrame::OnPaint()
{
...
...
...
return;
}
2.OnCreate() の戻り値は 0 とします。
3.基底クラス(CWnd)のメッセージハンドラ関数をオーバーライドする場合、WndProc() のときと引数(wParam lParam)が変わることがあるのでメッセージハンドラ関数の引数に合わせてプログラムを変更します。
case WM_LBUTTONDOWN: // マウス左ボタンのクリック
...
TextOut(hDC, LOWORD(lParam), HIWORD(lParam), "Hello World", 11);
...
break;
↓
引数が変わるのでそれに合わせます
void CMainFrame::OnLButtonDown(UINT wParam, CPoint pt) // OnLButtonDown() のオーバーライド
{
...
TextOut(hDC, pt.x, pt.y, "Hello World", 11);
...
return;
}
4.API関数と同じ名前の関数が基底クラス(
CWnd、
CDialog)のメンバ関数に存在するときはコンパイルエラー(C2660)になるので、エラーになったAPI関数の頭にグローバルスコープ解決演算子(
::)をつけます。
GetDC(hWnd) → ::GetDC(hWnd)、PostMessage(hWnd, ...) → ::PostMessage(hWnd, ...)
@エラーになるAPI関数の例(全てを網羅しているのではありません)
GetDC()、ReleaseDC()、BeginPaint()、EndPaint()、PostMessage()、SendMessage()、MoveWindow()、DestroyWindow()、
EnableWindow()、MessageBox()、GetClientRect()、GetWindowRect()、SetFocus()、SetDlgItemText()、CheckRadioButton()、
EndDialog()、GetParent()
AAPI関数を基底クラスのメンバ関数に変更してもOKです。第1引数を削除するだけでメンバ関数に変更できるものも沢山あります。
::MessageBox(hWnd, "Hello", "Msg", MB_OK); → MessageBox("Hello", "Msg", MB_OK);
1.Afx
MFCのサンプルプログラムを見ると頭に Afx がついた関数(AfxMessageBox() 等)をよくみかけます。これらの関数はグローバル関数で、クラスのメンバ関数ではないためプログラム内のどこからでも使用できます。
2.インスタンスハンドル(HINSTANCE)
インスタンスハンドルが必要なときは AfxGetInstanceHandle() で取得します。
3.ウィンドウハンドル(HWND)
ウィンドウハンドルが必要なときは m_hWnd を使用するか、GetSafeHwnd() で取得します。(どちらも CWnd のメンバ)
// 子ウィンドウの作成
hInst = AfxGetInstanceHandle();
hWnd = m_hWnd; // hWnd = GetSafeHwnd(); でもよい
CreateWindowEx(...., hWnd, hInst, NULL);
4.キャスト
.cpp のコンパイラは型チェックが厳密なので、.c ではコンパイルエラーにならなくても .cpp では型チェックエラーになることがよくあります。エラーになったところは必要なキャストを行います。
5.関数
関数はそのままでOKです。(オブジェクト指向の考え方に従えば、ウィンドウクラスのメンバ関数にするか、新しいクラスを作りそのメンバ関数にするべきなのでしょうが)
6.グローバル変数
グローバル変数はそのままでOKです。(オブジェクト指向の考え方に従えば、ウィンドウクラスのメンバ変数にするか、新しいクラスを作りそのメンバ変数にするべきなのでしょうが)
7.関数をウィンドウクラスのメンバ関数にするとき
@関数のプロトタイプ宣言をウィンドウクラス宣言のメンバ関数宣言部に移動します。
A関数定義の関数名の頭にクラス名:: を追加します。
int MyFunc_1(char *Param) → int CMainFrame::MyFunc_1(char *Param)
Bメンバ関数にするとメンバ関数内で使用しているAPI関数と同じ名前の関数が基底クラス(
CWnd)のメンバ関数に存在するとコンパイルエラーになるので、エラーになったAPI関数の頭にグローバルスコープ解決演算子(
::)をつけます。
(
こちらを参照)
8.CALLBACK関数
サブクラス化(コントロール(ボタン等)に独自処理を追加)やタイマプロシージャのための CALLBACK 関数はそのまま(グローバル関数)でOKです(
Sample_C2.cpp、
Sample_C3.cpp)。 CALLBACK 関数をクラスのメンバ関数にするときは以下のようにします。
Sample_E1.cpp は CALLBACK 関数をクラスのメンバ関数にする例です。
@CALLBACK 関数をクラスのメンバ関数にすると SetWindowLong() や SetTimer() でコンパイルエラー(C2440)が発生。
→ CALLBACK 関数を静的メンバ関数にします。具体的にはメンバ関数宣言に static をつけます。
A以上で C2440 はなくなりますが CALLBACK 関数内からメンバ変数を参照していると、まだコンパイルエラー(C2597)が発生。これは静的メンバ関数内からは静的メンバ変数しか参照できないためです。
→ 参照するメンバ変数を静的メンバ変数にします。具体的にはメンバ変数宣言に static をつけます。
B以上で C2597 はなくなりますが CALLBACK 関数内からメンバ関数を呼ぶと、まだコンパイルエラー(C2352)が発生。これは静的メンバ関数内からは静的メンバ関数しか呼べないためです。
→ メンバ関数でなくSDKのAPI関数を使用します。独自関数はグローバル関数にするかクラスの静的メンバ関数にします。
C以上でコンパイルはOKになりますが、まだリンクエラーになります。(LNK2001, LNK1120)
→ 以下のように静的メンバ変数を定義します。
// 静的メンバ変数
char CMainFrame::Msg_1[] = "Hello"; // Hello で初期化
char CMainFrame::Msg_2[80];
int CMainFrame::Count_1 = 0; // 0 で初期化
int CMainFrame::Count_2;
静的メンバ変数と関数については
こちらに簡単な説明があります。
D CALLBACK 関数内で親ウィンドウのハンドルが必要なときは GetParent() で親ウィンドウのハンドルを取得します。
9.ダイアログボックス
CDialog から派生したダイアログボックスを終了するとき、API関数の
EndDialog() で終了すると、ダイアログ作成側で
EndDialog() の第2引数を取得できません。このため終了するときは基底クラス(
CDialog)のメンバ関数の
EndDialog() を使用します。
Sample_D3.cpp はダイアログボックスにクラスを使用した例です。
10.スレッド
スレッド関数(制御関数)はそのまま(グローバル関数)でOKです(
Sample_F2.cpp)。スレッド関数をクラスのメンバ関数にするときは、CALLBACK 関数の場合と同様で以下のようにします。
Sample_F3.cpp はスレッド関数をクラスのメンバ関数にする例です。
@スレッド関数をクラスのメンバ関数にすると CreateThread() でコンパイルエラー(C2440)が発生。(_beginthread() ではコンパイルエラー (C2664) が発生)
→ スレッド関数を静的メンバ関数にします。具体的にはメンバ関数宣言に static をつけます。
Aクラスのメンバ変数、メンバ関数の使用についても CALLBACK 関数の場合と同じです。(8−A、B、C)
Bスレッドの作成方法を
CreateThread()、
_beginthread() から
AfxBeginThread() に変更する場合、スレッド関数の型と引数を
AfxBeginThread() 指定の型と引数に変更しないとコンパイルエラーになります。
Sample_F4.cpp は
AfxBeginThread() の使用例です。
スレッドの作成方法とスレッド関数の型と引数
CreateThread → DWORD WINAPI func(void *); (他の型、引数でもOK)
_beginthread → void func(void *);
AfxBeginThread → UINT func(void *);
スレッドの作成方法については
こちらに簡単な説明があります。
11.DLL
@DLLを実行時動的リンクで使用している場合、下図上段のようなコーディングになっているとコンパイルで関数の型チェックエラーになります。下図下段のようにDLL内関数の型を typedef で宣言して下さい。
// DLL内関数が int MyDllFunc(int, int); の場合
{
FARPROC lpFunc;
....
lpFunc = GetProcAddress(hLibrary, "MyDllFunc");
z = lpFunc(x, y); // ← コンパイルエラー(C2197)
↓
このように変更
typedef int(* LPDLLFUNC1)(int, int); // 型 LPDLLFUNC1 を宣言する
{
LPDLLFUNC1 lpFunc;
....
lpFunc = (LPDLLFUNC1)GetProcAddress(hLibrary, "MyDllFunc");
z = lpFunc(x, y)
Aマルチスレッドプログラムの場合、LoadLibrary()、FreeLibrary() の代わりに AfxLoadLibrary()、AfxFreeLibrary() を使用します。
BDLLをロード時リンクで使用している場合、ビルド時にリンクエラーが発生することがあります。このときは下記(12-@)を確認して下さい。
12.静的ライブラリ(スタティックライブラリ)
@C(.c)で作成されたライブラリ
変換前のプログラムで使用していた静的ライブラリがC(.cpp でない)で作成されているときは、リンク時、「error LNK2001: 外部シンボル○○○は未解決です(○○○は関数名)」のエラーになります。この場合は変換したプログラム(.cpp)の中のライブラリ関数のプロトタイプ宣言の頭に「 extern "C" 」をつけます。
int LibFunc(int, int); → extern "C" int LibFunc(int, int);
Aリンク時のワーニング
変換前のプログラムで使用していた静的ライブラリをリンクすると「warning LNK4098: defaultlib "LIBC" は他のライブラリの使用と競合しています; /NODEFAULTLIB:library を使用してください 」のワーニングがでます。これはランタイムライブラリの選択の指定が変換したプログラムと静的ライブラリで一致しなくなったためです。対応は以下のa)、b)のどちらかによります。
a)静的ライブラリのプロジェクトの設定を「マルチスレッド(DLL)」に変更してビルドし直す。
プロジェクト → 設定 → C/C++ → コード生成 → 使用するランタイムライブラリ → 「マルチスレッド(DLL)」
b)変換したプログラムのリンクオプションに /nodefaultlib:"LIBC" を追加する。
MFCとランタイムライブラリの対応は下の表のようになりますが、リンクする各モジュールに指定されているランタイムライブラリが一致していないとワーニングがでます。表を参考にライブラリを選択して下さい。
MFCの使用とランタイムライブラリの対応
| 選択されるランタイムライブラリ | ライブラリ名 | オプション |
MFCを使用しない | 標準のライブラリ(マルチスレッドに非対応) | LIBC.LIB(LIBCD.LIB) | /ML(/MLd) |
共有DLLでMFCを使用 | マルチスレッド対応かつDLL対応のライブラリ | MSVCRT.LIB(MSVCRTD.LIB) | /MD(/MDd) |
MFCのスタティックライブラリを使用 | マルチスレッド対応のライブラリ | LIBCMT.LIB(LIBCMTD.LIB) | /MT(/MTd) |
(/ML /MD /MT は設定されるプロジェクト(コンパイル)のオプションです。 d なし:Release d 付:Debug)
13.メッセージ処理をアプリケーションクラスに実装
メッセージをトリガに実行される機能にはウィンドウクラスの中に実装するよりも、アプリケーションクラスの中に実装する方が適切なものもあります。
Sample_B3.cpp はメッセージ処理の一部をアプリケーションクラスに実装する例です。
@アプリケーションクラスの中でウィンドウのハンドルが必要なときは m_pMainWnd->m_hWnd または m_pMainWnd->GetSafeHwnd() で取得します。
Aアプリケーションクラスの中からウインドウクラスのメンバ関数を使用するときは m_pMainWnd->メンバ関数 でコールします。
[簡単な説明]
1)静的メンバ変数と関数
静的メンバ(変数、関数)はクラスに属するメンバであり、オブジェクトに属するメンバでありません。このため1つのクラスから複数のオブジェクトを生成しても、静的メンバは1つしか存在しません。また、静的メンバはオブジェクトを生成しなくても使用が可能です。以上から静的メンバ変数(関数)はアクセスコントロールが可能なグローバル変数(関数)と考えてもいいでしょう。
2)スレッドの作成方法
スレッドの作成方法は次の3つの方法があります。
@CreateThread()
SDKのスレッド。SDKのAPI関数と再入可能なCランタイムライブラリ関数のみ使用できます。
A_beginthread()
SDKのスレッド。SDKのAPI関数とCランタイムライブラリ関数を使用できます。
BAfxBeginThread()
MFCの CWinThread クラスからスレッドを生成します。
Sample_F4.cpp は
AfxBeginThread() の使用例です。
Cランタイムライブラリ関数とは LIBC.LIB に含まれる関数で、具体的には
sprintf()、memcpy()、strcpy()、malloc()、fopen()、system() 等の関数です。これらの関数には同等の機能を持つAPI関数があるものもあります。同等関数についてはMSDNの「C ランタイム関数の Win32 で対応する関数一覧」をご覧下さい。(Microsoft の HP は
こちら)
以上により変換したプログラムを Microsoft Visual C++ 6.0 を使用してビルドする手順は以下のとおりです。
1.プロジェクトの作成
メニューのファイル → 新規作成 → 「Win32 Application」を選択して「プロジェクト名」を入力する → 「空のプロジェクト」
2.プロジェクトにソースファイルを追加
メニューのプロジェクト → プロジェクトへ追加 → ファイル → ソースファイル一式(.cpp、.h、.rc、.ico 等)を追加する
3.プロジェクトの設定
メニューのプロジェクト → 設定 → 一般 → 「MFCを使用しない」を「共有DLLでMFCを使用」に変更する
(変換前のサンプルプログラム(.c)をビルドするときはここはディフォルトの「MFCを使用しない」のまま)
4.ビルド(コンパイルとリンク)
メニューのビルド → ビルド
ビルドできたのに動かない
@アプリケーションの生成「 CMyApp MyApp; 」の記述を忘れてもビルドでエラーになりません。しかし、プログラムを実行後すぐにアボートし何故だろうと悩むことになります。
Aメッセージマップの記述を忘れてもビルドでエラーになりません。しかし、プログラムを実行してもそのメッセージ処理は動きません。 → メッセージマップに「 ON_WM_TIMER()」がないと OnTimer() は動きません。