ダイアログ・アプリのコンボボックスとエディットボックスの異常について

[戻る]
ぴょぴょ 2010/02/08(月) 10:30:17
今年もお世話になります。
今年より開発環境を Windows Vista SP2 に移行しました。

そこで簡単なダイアログ・アプリを作成したら不具合が出ました。
いろいろと調べたら次のようなことが分かりました。

[正常時]
1.ダイアログにコンボボックスとエディットボックスを貼り付ける
2.WinMain()内で DialogBox() 関数で呼び出し実行する
3.コンボボックスの[▼]ボタンを押すとリストが開いたり、閉じたりする
4.エディットボックスにフォーカスを合わせ[半角/全角]キーを押すと漢字が正常に入力
できて変換前の平仮名も表示される

[異常時]
1.ダイアログにコンボボックスとエディットボックスを貼り付ける
2.ダイアログに CLASS "TestWndClass" とクラス名を付ける
3.RegisterClassEx() で DLGWINDOWEXTRA を指定して "TestWndClass" をクラス名とし
て登録する
4.WinMain()内で CreateDialog() 関数でウインドウを作成し、メッセージ・ループで処
理させる
5.コンボボックスの[▼]ボタンを押すとリストが開きっぱなしで閉じない
6.コンボボックスのリストにマウスを乗せても項目のハイライトが移動しない
7.コンボボックスからフォーカスが外れてもリストが閉じない
8.エディットボックスにフォーカスを合わせ[半角/全角]キーを押すと漢字は入力できる
が、変換前の平仮名が表示されない
9.ダイアログ・アプリがアクティブのとき、タスクバーにある IME の表示が消えるが非
アクティブになると IME の表示が現れる

[質問]
なぜ。
ダイアログで正常に動いたものが、ダイアログ・アプリ(クラス名を指定してウインドウ
を作成)するだけで[異常時]の5〜9が発生するのか原因を知りたいです。そして対策方法
も知りたいです。

[実現したいこと]
ダイアログ・アプリで正常にコンボボックスのリストの開け閉めや、エディットボック
スの平仮名入力の表示を行いたいです。
どうすれば直せますか?
そもそも私の環境だけなのでしょうか?

簡単なテスト・プログラムを作成して上記の事が判明しました。
時間のある方は試してくれませんか?
今回はソースを載せませんが、必要な場合は載せるようにします。
よろしくお願いします。

仲澤@失業者 2010/02/08(月) 15:33:07
プライベートダイアログクラスを指定してDLGを作っている方がいるんですね。

んで、ちなみにコンパイラのバージョンは2008ですか(質問1)。
また、TestWndClassを使ってDialogBox() したときはどうなりますか(質問2)

ロマ 2010/02/08(月) 15:54:20
Vista無いので原則的なこと

rcファイル
IDD_MYDIALOG DIALOG
CLASS "testWndClass"
.....

ソースファイル
LRESULT testWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  ....
  return DefDlgProc(hwnd, message, wParam, lParam);
}

BOOL testDlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  switch(message)
  {
    case WM_COMMAND:
    ....
  }
  return FALSE;
}

// WinMain
WNDCLASSEX wcex;
wcex.lpfnWndProc = testWndProc;
wcex.cbWndExtra = DLGWINDOWEXTRA;
wcex.lpszClassName = testWndClass";
....
RegisterClassEx(&wcex);

CreateDialog(hInstance, MAKEINTRESOURCE(IDD_MYDIALOG), 0, testDlgProc);
 
while(GetMessage(..))
{
  if(!IsDialogMessage(..))
  {
     TranslateMessage(..);
     DispatchMessage(..);
  }
}
-----------------------
testDlgProcが無い場合は、CreateDialogの4番目の引数はNULL
testWndProcが無い場合は、wcex.fnWndProc=DefDlgProc;
いずれかの関数の中でPostQuitMessageを呼ぶこと

ぴょぴょ 2010/02/09(火) 04:46:31
仲澤@失業者さんへ。

>んで、ちなみにコンパイラのバージョンは2008ですか(質問1)。
VC2008(無料版のEE)です。

>また、TestWndClassを使ってDialogBox() したときはどうなりますか(質問2)
この場合はダイアログは表示されません。
ダイアログに CLASS を付けると DialogBox() 関数ではウインドウは作成されないので
す。
だからプロセスそのものがタスクマネージャに現れません。

ちなみに DialogBox() 関数の戻り値は -1 が戻ってきます。
また、GetLastError() は 1407=[ウィンドウ クラスを見つけることができません。]が
設定されています。
このエラーはダイアログのクラスが[#32770]だから、自分で設定した TestWndClass と
は違うということでしょう。

このような結果になりました。

ぴょぴょ 2010/02/09(火) 04:47:12
ロマさんへ。

一部間違いが発覚しました。
ウインドウ・プロシージャ内で DefDlgProc() 関数ではなく DefWindowProc() 関数を呼
んでいました。
そこで DefDlgProc() 関数に修正しましたが、同じ異常な動作をします。
全く治っていない様子です。
コンボボックスの不具合は発生したまま。
さらにエディットボックスでは[半角/全角]キーが効かなくなって漢字の入力が不可能に
なりました。

ただ、次のサイトに一部間違いがあることが分かった。
http://wisdom.sakura.ne.jp/system/winapi/win32/win89.html
このサイトでは DefWindowProc() 関数を呼び出しているようだ。
これで長年の疑問が1つ解決した。
それはダイアログ・アプリを作成すると WM_CREATE 内でボタンなどのコントロールがま
だ作成されていないから、初期化するときに困っていた点です。
この解決策としてメッセージ・ループに入る前に SendMessage( hWnd, WM_INITDIALOG, 
0, 0 ); と記述しておき、
ウインドウ・プロシージャ内の WM_INITDIALOG でコントロールの初期化を行っていた。

でも今回 CreateDialog() 関数の4つ目の引数にダイアログ・プロシージャを渡したら
ダイアログ・プロシージャ内で WM_INITDIALOG が届くのでコントロールの初期化が上手
くいった。
この部分に関してずっと間違っていたようだ。
ロマさんありがとう。

あとロマさん書き込みより3パターン試しました。
1.ウインドウ・プロシージャ+ダイアログ・プロシージャの場合
2.ウインドウ・プロシージャのみ
3.ダイアログ・プロシージャのみ
その結果、1、2でも同じ不具合が発生し、3 は CreateDialog() 関数でエラー、
GetLastError() は 0 でした。

ロマさんへ。
ダイアログ・プロシージャのみでウインドウって作成できますか?
extern BOOL CALLBACK mainDialogProc( HWND hDlg, UINT uMsg, WPARAM wParam, 
LPARAM lParam );

WNDCLASSEX wcex = { 0 };
wcex.cbSize        = sizeof(WNDCLASSEX);
wcex.lpfnWndProc   = (WNDPROC)mainDialogProc;
wcex.cbWndExtra    = DLGWINDOWEXTRA;
wcex.lpszClassName = lpClassName;
RegisterClassEx( &wcex );
lpfnWndProc に WNDPROC でキャストして代入しています。
これであっていますか?

ロマ 2010/02/09(火) 09:12:39
> wcex.lpfnWndProc   = (WNDPROC)mainDialogProc;
ここは、wcex.lpfnWndProc = DefDlgProc;です。
あと、念のため、WNDCLASSEXの他のメンバーも正しく設定してください
(特にwcex.hInstanceなど)。

ところで、CreateDialogを使いたいだけなら、ご提示のサイトに反して、
CLASSを作る必要はありません。

rcファイル
IDD_MYDIALOG DIALOG
//CLASS "testWndClass"  <---削除
.....

ソースファイル
BOOL testDlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  switch(message)
  {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    case WM_COMMAND:
    ....
  }
  return FALSE;
}

// WinMain

CreateDialog(hInstance, MAKEINTRESOURCE(IDD_MYDIALOG), 0, testDlgProc);
 
while(GetMessage(..))
{
  if(!IsDialogMessage(..))
  {
     TranslateMessage(..);
     DispatchMessage(..);
  }
}

-----------------
CLASSを作るのは、
ダイアログでWM_INITDIALOGの前のメッセージを処理したい場合(たとえばWM_CREATE)、
DWL_MSGRESULTを使いたくない場合、
メッセージのデフォルト処理の後に何かさせたい場合だと思います。

仲澤@失業者 2010/02/09(火) 09:44:25
>また、GetLastError() は 1407=[ウィンドウ クラスを見つけることができません。]が
>設定されています。

それは、CS_GLOBALCLASS フラグが足りないせいですね。

もっとも、ロマさんのおっしゃる通り、単にモードレスダイアログを
表示したいだけならウインドウクラスの定義、登録、及び、
クラスコールバックの実装は必要ありませんので、とりあえずそれを
試してみたらどうでしょう。ただし、ダイアログコールバックは必須です。

ぴょぴょ 2010/02/09(火) 11:00:50
ロマさんへ。

>ここは、wcex.lpfnWndProc = DefDlgProc;です。
修正しましたが不具合は消えませんでした。

>あと、念のため、WNDCLASSEXの他のメンバーも正しく設定してください
>(特にwcex.hInstanceなど)。
次のように設定しています。

// マクロ関数
#define apiLoadImage(h,id,type,cast)    ((cast)LoadImage(h,MAKEINTRESOURCE
(id),type,0,0,(LR_DEFAULTSIZE | LR_SHARED)))
#define apiLoadIcon(h,id)               apiLoadImage(h,id,IMAGE_ICON,HICON)
#define apiLoadCursor(h,id)             apiLoadImage(h,id,IMAGE_CURSOR,HCURSOR)

// ダイアログ・クラスの登録
static ATOM funcDialogClass( HINSTANCE hInstance, LPCTSTR lpClassName )
{
    WNDCLASSEX wcex = { 0 };
    
    wcex.cbSize         = sizeof(WNDCLASSEX);
    wcex.style          = (CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS);
    wcex.lpfnWndProc    = mainWindowProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = DLGWINDOWEXTRA;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = apiLoadIcon( NULL, IDI_APPLICATION );
    wcex.hIconSm        = apiLoadIcon( NULL, IDI_APPLICATION );
    wcex.hCursor        = apiLoadCursor( NULL, IDC_ARROW );
    wcex.hbrBackground  = GetSysColorBrush( COLOR_3DFACE );
    wcex.lpszMenuName   = NULL;
    wcex.lpszClassName  = lpClassName;
    return RegisterClassEx( &wcex );
}

// メイン関数
extern int WINAPI _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, 
LPTSTR lpCmdLine, int nCmdShow )
{
    LPCTSTR     lpClassName = TEXT("TestWndClass");
    HWND        hWnd;
    BOOL        bRet;
    MSG         Msg;
    
    if ( funcDialogClass(hInstance,lpClassName) == 0 ){
        return -1;
    }
    if ( (hWnd = CreateDialog(hInstance,MAKEINTRESOURCE
(IDD_DIALOG02),NULL,mainDialogProc)) == NULL ){
        return -2;
    }
    while ( (bRet = GetMessage(&Msg,hWnd,0,0)) != FALSE ){
        if ( bRet < 0 ){
            break;
        }
        if ( !IsDialogMessage(hWnd,&Msg) ){
            TranslateMessage( &Msg );
            DispatchMessage( &Msg );
        }
    }
    return Msg.wParam;
}

>ところで、CreateDialogを使いたいだけなら、ご提示のサイトに反して、
>CLASSを作る必要はありません。
多重起動防止や EnumWindow 関数で列挙するのでクラス名を指定する必要があります。

ぴょぴょ 2010/02/09(火) 11:13:24
仲澤@失業者さんへ。

>もっとも、ロマさんのおっしゃる通り、単にモードレスダイアログを
>表示したいだけならウインドウクラスの定義、登録、及び、
>クラスコールバックの実装は必要ありませんので、とりあえずそれを
>試してみたらどうでしょう。ただし、ダイアログコールバックは必須です。
これから作ろうとしているツールはモードレス・ダイアログではダメだと思います。
理由は EnumWindow 関数でクラス名を調査し、そのウインドウに WM_APP の独自メッセ
ージを送信する簡単なプロセス間通信を行わせるからです。
それも単純な多重起動防止ではなく同じクラス名が複数あっても WM_APP の独自メッセ
ージより起動可能、不可能を切り分けてる仕様です。
つまり、クラス名が TestWndClass でも WM_APP_FUNCTYPE を送信して AAAA 値、BBBB 
値、CCCC 値を調べて同機能なら起動不可能。

[オプション]   [クラス名]   [メッセージの戻り値]
Test.exe /A → TestWndClass AAAA値を SendMessage() より取得
Test.exe /B → TestWndClass BBBB値を SendMessage() より取得
Test.exe /C → TestWndClass CCCC値を SendMessage() より取得

このような感じで同じオプション付きの場合は Test.exe の起動を阻止する仕様です。
分かりますかね。

ロマ 2010/02/09(火) 11:32:51
> while ( (bRet = GetMessage(&Msg,hWnd,0,0)) != FALSE ){
while( (bRet = GetMessage(&Mes, 0, 0, 0)) != FALSE) {
です。

ロマ 2010/02/09(火) 11:36:10
たいぽ
&Mes --> &Msg

ぴょぴょ 2010/02/09(火) 12:07:29
[[解決]]
ロマさんへ。

> while ( (bRet = GetMessage(&Msg,hWnd,0,0)) != FALSE ){> while ( (bRet = GetMessage(&Msg,NULL,0,0)) != FALSE ){
修正しましたら不具合が消えました。
コンボボックスのリストも開け閉めでき、漢字入力時の平仮名も表示されました。
ありがとうございます。

不具合は治りましたので[解決]マークは付けておきます。
でも、どうして GetMessage() 関数の第2引数に NULL を指定するのですか。
hWnd と NULL 指定では意味が違って hWnd ではいけない理由があるのでしょうか?
ご存知でしたら教えてください。
お願いします。

仲澤@失業者 2010/02/09(火) 13:25:00
解決しちゃいましたけど、

>理由は EnumWindow 関数でクラス名を調査し、そのウインドウに WM_APP の独自メッセ
>ージを送信する簡単なプロセス間通信を行わせるからです。

自作のウインドウを他のウインドウと区別するのにクラス名称を取得するような、
遠回りをする必要はまったくありません。
ウインドウならGWLP_USERDATAダイアログならDWLP_USERに
SetWindowLongPtr()で識別データを埋め込んでおけば済むことです。
こいつの良いところは識別データがHWNDと一意につながっている点で、
クラス名称だけでテストするより簡単でより細かく区別する仕組みを
実装することが可能です。
こんな便利な仕組みがあるのに、案外使われないですよね(vv;)。

ロマ 2010/02/09(火) 13:35:17
> hWnd と NULL 指定では意味が違って hWnd ではいけない理由があるのでしょうか?

コンボボックス向けにポストされたメッセージや
コンボボックスへのキーボードイベント、マウスイベントなどのメッセージは
メッセージキューにたまっています。

hWndを指定すると、これらのメッセージをGetMessageで取り出すことは出来ません。
NULLを指定すると、このスレッドに属する全てのウィンドウのメッセージを取得でき、
DispatchMessageで各ウィンドウのプロシージャにメッセージを渡たすことが出来ます。

subaru 2010/02/09(火) 13:49:04
>でも、どうして GetMessage() 関数の第2引数に NULL を指定するのですか。
>hWnd と NULL 指定では意味が違って hWnd ではいけない理由があるのでしょうか?

第2引数がhWndの場合、そのhWndと子ウインドウのメッセージだけが取得されます。

今回問題が発生してるのはコンボボックスのドロップダウンリストの部分と
エディットボックスのIMEウインドウのようですが、
これらは第2引数で識別されるダイアログでも
ダイアログ上のコントロールの子ウインドウでもないので
メッセージを取得する対象になっていないのでしょう。

ぴょぴょ 2010/02/09(火) 13:53:26
仲澤@失業者さんへ。

>こんな便利な仕組みがあるのに、案外使われないですよね(vv;)。
別のデータ構造体を設定して利用していました。

でも
>こいつの良いところは識別データがHWNDと一意につながっている点で、
>クラス名称だけでテストするより簡単でより細かく区別する仕組みを
>実装することが可能です。
という発想と使い方には気づきませんでした。
ありがとうございます。

ぴょぴょ 2010/02/09(火) 14:01:11
[[解決]]
ロマさんとsubaruさんへ。

>hWndを指定すると、これらのメッセージをGetMessageで取り出すことは出来ません。
>NULLを指定すると、このスレッドに属する全てのウィンドウのメッセージを取得でき、
>DispatchMessageで各ウィンドウのプロシージャにメッセージを渡たすことが出来ま
す。
hWnd と NULL 指定の違いが分かりました。

>今回問題が発生してるのはコンボボックスのドロップダウンリストの部分と
>エディットボックスのIMEウインドウのようですが、
>これらは第2引数で識別されるダイアログでも
>ダイアログ上のコントロールの子ウインドウでもないので
>メッセージを取得する対象になっていないのでしょう。
さらに詳しい説明で分かりやすかったです。
ドロップダウンリストの部分、IMEもウインドウの1つであることを思い出しました。

>第2引数がhWndの場合、そのhWndと子ウインドウのメッセージだけが取得されます。
納得しました。

仲澤@失業者さん、ロマさん、subaruさんのおかげでいろいろとお勉強になりました。
本当にありがとうございました。

ロマ 2010/02/14(日) 23:23:18
>第2引数がhWndの場合、そのhWndと子ウインドウのメッセージだけが取得されます。
 
遅ればせながら、subaruさん、ご指摘に感謝します。
こんな基本APIは古いAPIリファレンスから変わっていないだろうと思ってました。
ずーっとだまされていました。

古いスレッドには追加発言できません。