今朝も朝の3時に突然「あっ」が降りてきました。いやはや、お恥ずかしい。
「できました」と書きましたが、WM_LBUTTONDOWNとIDM_ROLLポップアップメニューについてコントロール(子ウィンドウ)にとって一番大事な「親への通知」を忘れていました。(私と同じで親不孝ですね。)
さて、では親へ通知しようということでコードを追加しようとして不図考えました。
【Win3.x (16 bit) 時代】
"WM_COMMAND"が使われ、(当初はWPARAM(WORDが16ビット)にコントロールコードが入り、LPARAM(LONGが32ビット)にコントロールIDとウィンドウハンドルが入っていたそうですが、後の32 bit時代になり)WPARAM(32ビット)にコントロールID(LOWORD)とコントロールコード(HIWORD)が入り、LPARAM(32ビット)にウィンドウハンドルが入ります。
【Win32時代】
"WM_NOTIFY"とNMHDR構造体(注1)が使われ、(特にコモンコントロールなど)複雑な動作をするコントロールになると金魚の糞(注2)を付けて処理することになりました。要すればNMHDR構造体にWin3.x時代の情報(HWND、CtrlID、Msg)をいれ、その他の情報は「金魚の糞」にして渡す、という方法です。以下の様にして使います。
" case WM_NOTIFY:
if(idControl == wParam) { // コントロールID
*NMHDR lpnmh = (*NMHDR)lParam; // NMHDR構造体のアドレス
}"
更にコモンコントロールでも下位互換を考えてWM_COMMANDと併用しているものもあるようです。(注3)
注1:以下がNMHDR構造体ですが、
typedef struct tagNMHDR {
HWND hwndFrom; // コントロールのハンドル
UINT idFrom; // コントロールID
UINT code; // 通知コード
} NMHDR;
この"NMHDR"の意味ってwebを漁っても出てこないですね。恐らく"Notification Message of HanDleR"ではないかと思っています。他の「金魚の糞コントロール」ではNMLISTVIEW構造体のようにNotification Message of ListViewのようにコントロール名を付けるものもあれば、日付コントロールのNMSELCHANGE構造体のように動作(Notification Message of Selection Change)を表すものや、ツールバーのTOOLTIPTEXT構造体のように読んで字のごとしでNMが無いものもあり、名づけ規則が一貫していませんね。
注2:以下はリストビューの例
typedef struct tagNMLISTVIEW {
NMHDR hdr; //ここを展開すると↑と同じ
int iItem; //ここ以下が金魚の糞
int iSubItem;
UINT uNewState;
UINT uOldState;
UINT uChanged;
POINT ptAction;
LPARAM lParam;
} NMLISTVIEW, *LPNMLISTVIEW;
注3:そういえば両方使えるものがあるな、とおもってググったら、同様の疑問を持った方がいたので助かりました。また、命名規則についての印象も同じですね。(「追記」で「負の遺産」と書かれているのは、ポジティブには「下位互換性」といえるでしょう。)
今回の"DICE"コントロールにおける親へ「サイコロを振らせられて、この目をだしたよ」という通知を行うイベントは、WM_COMMAND(ポップアップメニューのIDM_ROLL)とWM_LBUTTONDOWN<この二つユーザー操作により、共にコントロールの「サイコロの一個の音」を出す>、およびメインウィンドウからSendMessageで処理する「WM_USER + 1(注4)で定義したDM_ROLLメッセージ」の3つあり、親に渡したい情報はいずれも「サイコロの目(このDLLの内部ではゼロベースの int g_Dice + 1)」です。
注4:未定義のウィンドウメッセージに独自の定義を与えて利用するにはWM_USERとWM_APPの二つあり、その違いをMicrosoftはこう定義しています。(タグ"WM_USER"とタグ"WM_APP"の内容は同じですね。)
この問題は結構誤解している人が多いので気を付けてください。簡単に言うと、
0~WM_USER(0x8000) - 1 → OSで使います。
WM_USER~WM_APP(0xC000)→ ユーザーコントロール(「たとえばBUTTON、EDIT、LISTBOX、COMBOBOXなどの」「アプリケーションによって定義され(た)プライベートウィンドウクラス内)でメッセージを送信するために使用できます。」(Microsoft Doc)従って既存のコントロールの改造(カスタムコントロール)の場合に使ってはいけませんが、新規のコントロール(ユーザーコントロール)の場合にはここで独自のメッセージを定義しなければなりません。
WM_APP(0xC000)~0xFFFF →これは例えば特別な処理のためにメインウィンドウのメッセージを作ったり、既存のコントロールをサブクラス化して新しい処理を新しいメッセージで加えたりするような場合(そのアプリケーション限りのメッセージ)に使います。「アプリケーションがプライベートメッセージとして使用するには、3 番目の範囲 (0x8000 から 0xBFFF) のメッセージ番号を使用できます。この範囲内のメッセージは、システム メッセージと競合しません。」(Microsoft Doc)
ということで、コードの比較をしてみましょう。
【WM_NOTIFY-Win32】
先ず出来合いのNMHDRではサイコロの目の情報を入れるメンバーが無いので、新しい構造体を定義して外部変数を宣言しましょう。
<NMDICE構造体を定義する>
typedef struct tagNMDICE {
NMHDR hdr; // コントロールのハンドル
int dice_num; // サイコロの目
} NMDICE, *LPNMDICE;
<NMDICE構造体の外部変数を宣言する>
NMDICE g_NMdice;
<コールバック変数のSwitch(msg)処理>
case WM_COMMAND:
if(LOWORD(wParam) != IDM_ROLL)
return DefWindowProc(hWnd, Msg, wParam, lParam);
case WM_LBUTTONDOWN:
PlaySound("IDS_DICE", g_hInstance, SND_RESOURCE | SND_ASYNC);
case DM_ROLL:
.
.
.
//親へWin32版メッセージを送る
g_NMdice.hdr.hwndFrom = hWnd;
g_NMdice.hdr.idFrom = GetDlgCtrlID(hWnd);
g_NMdice.hdr.code = 通知コード;
g_NMdice.dice_num = g_Dice + 1;
SendMessage(GetParent(hWnd), WM_NOTIFY, (WPARAM)GetDlgCtrlID(hWnd), (LPARAM)&g_NMdice);
//親からのDM_ROLLメッセージの戻り値として賽の目を返す
return g_Dice + 1; //g_DiceはDLLの外部変数でサイコロの目 - 1(ビットマップハンドル配列に対応)である
WM_NOTIFYのWPARAMは、コモンコントロールなどではイベントが発生したコントロールのコントロール IDが指定されますが、ユニークなものであることが保証されていないそうで、コントロールIDはNMHDR(上記の場合hdrメンバー)情報からとるべきだそうです。また通知コードも対象が3つもある共用処理である為、ここにサイコロの目を入れてもよさそうです。となると、NMDICE構造体もいらなくなり、出来合いのNMHDRで行けそうです。以下に上記のコードを書き換えた改造版を載せます。
<NMHDR構造体の外部変数を宣言する>
NMHDR g_Hdr;
<コールバック変数のSwitch(msg)処理>
case WM_COMMAND:
if(LOWORD(wParam) != IDM_ROLL)
return DefWindowProc(hWnd, Msg, wParam, lParam);
case WM_LBUTTONDOWN:
PlaySound("IDS_DICE", g_hInstance, SND_RESOURCE | SND_ASYNC);
case DM_ROLL:
.
.
.
//親へWin32版メッセージを送る
g_Hdr.hwndFrom =hWnd;
g_Hdr.idFrom = GetDlgCtrlID(hWnd);
g_Hdr.code = g_Dice + 1;
SendMessage(GetParent(hWnd), WM_NOTIFY, (WPARAM)GetDlgCtrlID(hWnd), (LPARAM)&g_Hdr);
//親からのDM_ROLLメッセージの戻り値として賽の目を返す
return g_Dice + 1; //g_DiceはDLLの外部変数でサイコロの目 - 1(ビットマップハンドル配列に対応)である
【WM_COMMAND-Win3.x】
何をいまさら16 bit時代の方法で、という感もありますが、Microsoftも下位互換性を確保しているのでおかしいというほどのことはないです。メリットは処理が簡素なコントロールではかんたんであることです。
<コールバック変数のSwitch(msg)処理>
case WM_COMMAND:
if(LOWORD(wParam) != IDM_ROLL)
return DefWindowProc(hWnd, Msg, wParam, lParam);
case WM_LBUTTONDOWN:
PlaySound("IDS_DICE", g_hInstance, SND_RESOURCE | SND_ASYNC);
case DM_ROLL:
.
.
.
//親へWin3.x版メッセージを送る
SendMessage(GetParent(hWnd), WM_COMMAND, MAKELONG(GetDlgCtrlID(hWnd), 通知コード), (LPARAM)hWnd);
//親からのDM_ROLLメッセージの戻り値として賽の目を返す
return g_Dice + 1; //g_DiceはDLLの外部変数でサイコロの目 - 1(ビットマップハンドル配列に対応)である
しかし、個々でも同様に返したいサイコロの目は親からのSendMessage("DICE"コントロールハンドル, DM_ROLL, 0, 0,);にしか対応していない為、また通知コードも対象が3つもある共用処理である為、ここは(毒や害にならないので)「サイコロの目(g_Dice + 1)」を通知コードに替えて返しちゃいましょう。以下は変更部分のみ記述します。
//親へWin3.x版メッセージを送る
SendMessage(GetParent(hWnd), WM_COMMAND, MAKELONG(GetDlgCtrlID(hWnd), g_Dice + 1), (LPARAM)hWnd);
で、どちらにするかといえば...
簡単な方に決まり!
ということでWM_COMMANDで渡します。(「いやだ、私はWM_NOTIFYで行く」という人がいれば、↑のコードを参考にして真正版<サイズが大きくなり、コード量も増えます>か簡易版<NMHDRのみで行きます>か選択して変更をお願いします。)