子ダイアログ内の複数行エディットボックス

[戻る]
いしだ 2011/09/07(水) 11:06:45
MFCアプリで、ダイアログ内に複数の子ダイアログを持ち、
タブ形式やリスト形式やツリー形式などで表示を切り替える、
アプリケーションのオプション設定などでよく見られる機能を作成しています。

子ダイアログにDS_CONTROLを設定することで、
タブキーやアクセスキーで親子間を行き来でき、ESCキーを押した際には、
親ダイアログのキャンセル処理が呼ばれるようになるのですが、
ここで一つ、問題が発生しました。

子ダイアログ内に複数行のエディットボックスがあると、
その上でESCキーを押した際に、子ダイアログだけが閉じられてしまいます。

不思議に思ってMFC内のソースを追いかけてみたところ、
CDialog::PreTranslateMessageに以下のような記述がありました。

    // fix around for VK_ESCAPE in a multiline Edit that is on a Dialog
    // that doesn't have a cancel or the cancel is disabled.
    if (pMsg->message == WM_KEYDOWN &&
        (pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_CANCEL) &&
        (::GetWindowLong(pMsg->hwnd, GWL_STYLE) & ES_MULTILINE) &&
        _AfxCompareClassName(pMsg->hwnd, _T("Edit")))
    {
        HWND hItem = ::GetDlgItem(m_hWnd, IDCANCEL);
        if (hItem == NULL || ::IsWindowEnabled(hItem))
        {
            SendMessage(WM_COMMAND, IDCANCEL, 0);
            return TRUE;
        }
    }

VC6の頃から上記のような記述が入っているようなのですが、
DS_CONTROLを持ったダイアログに複数行のエディットボックスを持たせる場合、
ESCキーで最上位のダイアログのキャンセル処理に辿り着くには、
どのような方法が使われてきたのでしょうか。

子ダイアログのOnCancelの中で
GetParent()->SendMessage(WM_COMMAND, IDCANCEL, 0);
を入れればいいだけかと思ったのですが、親子階層の実装方法によっては
1つ上の親が最上位のダイアログである保証は無いですし、
子ダイアログ自身がどのダイアログの子で、
どのような階層になっているのかなどには依存しないようにしたいです。

そもそも複数行エディットボックスを使わない限りは、
DS_CONTROLを付けるだけでよかった問題です。
なにかよい方法はありませんでしょうか。

仲澤@失業者 2011/09/07(水) 13:32:11
え〜と、悩む必要はまったくありませんよねぇ。

CDialog::PreTranslateMessage()はvirtualなので、
「派生先で好きに変更してね」って書いてあるのと同じです。
エドモンドの連中も、こういった問題は起こるだろうなぁと思っていた
というこです。

当該のDLGクラスでPreTranslateMessage()をオーバーロードして
好きなように変更しましょう。

仲澤@失業者 2011/09/07(水) 13:32:56
もとい。orz.
オーバーライドの間違いですね。

いしだ 2011/09/07(水) 14:40:01
> 当該のDLGクラスでPreTranslateMessage()をオーバーロードして
> 好きなように変更しましょう。

CDialog::PreTranslateMessageの中で呼ぶ処理の順番には意味がありそうで、
どうオーバーライドすればよいのかがいまいち不明なのです。

VC6の時代から、この手の親子ダイアログはよくあったと思うのですが、
実際に作成されて、この問題に遭われたかたはいらっしゃいませんでしょうか。

ひょっとしたら、念のためにと子ダイアログ側でオーバーライドして
空処理化しておいたOnCancelがこっそり呼ばれていて、
複数行エディットボックスだけはESCキーを押しても
ダイアログが閉じなくなっているケースもあるのではと思うのですが。

Kelly 2011/09/07(水) 16:01:09
こんな感じでいかがですか?

void CChildDialog::OnCancel()
{
    if (GetStyle() & WS_CHILD)
        GetTopLevelParent()->SendMessage(WM_COMMAND, IDCANCEL);
    else
        CDialog::OnCancel();
}

いしだ 2011/09/07(水) 17:06:20
> こんな感じでいかがですか?

子ダイアログからGetTopLevelParentを使ってみたところ、
戻り値が親ダイアログを通り越してCMainFrameのポインタになってしまいました。

親ダイアログのポインタが取れればこの方法でいけるかもしれませんが、
CWndのメンバにはそれらしい関数が見あたりませんね。

Kelly 2011/09/07(水) 17:22:42
> 子ダイアログからGetTopLevelParentを使ってみたところ、
> 戻り値が親ダイアログを通り越してCMainFrameのポインタになってしまいました。

ダイアログベースでしか確認してませんでしたm(_ _)m

GetParentOwner()ならば大丈夫そうです。

仲澤@失業者 2011/09/07(水) 17:47:37
>CDialog::PreTranslateMessageの中で呼ぶ処理の順番には意味がありそうで、
>どうオーバーライドすればよいのかがいまいち不明なのです。

う〜む。そこまで説明が必要ですか(vv;)。
 1.CDialog::PreTranslateMessage()全体をコピペして、MyDLGのメンバにする。
 2.具合の悪いコードを修正する。
  本件の場合、MyDLGは、当該条件が成立しても、自身にIDCANCELを送らない
  こととする。
 4.当然だが、派生元のCDialog::PreTranslateMessage()は呼ばない。
だけですよね。

ちなみに、CDialog::PreTranslateMessageのコメント("fix around for・・)は、
"マルチラインのエディットがあったときに、そこでESCされても
 DLGがキャンセルされないバグを直しました(意訳)。"
と言う意味ですよねぇ。
まんまですわな(笑)。

仲澤@失業者 2011/09/07(水) 18:11:44
ついでなのでもうちょっと説明

if (pMsg->message == WM_KEYDOWN &&     // キーが押された場合で、
   (pMsg->wParam == VK_ESCAPE    // そのキーがESC 又は
   || pMsg->wParam == VK_CANCEL) &&  // キャンセルの場合で、
   (::GetWindowLong(pMsg->hwnd, GWL_STYLE) & ES_MULTILINE) &&
      // 送信者のウインドウスタイルがES_MULTILINEを持っている、かつ
   _AfxCompareClassName(pMsg->hwnd, _T("Edit")))
      // 送信者のウインドウクラスが"Edit"の場合
{
   HWND hItem = ::GetDlgItem(m_hWnd, IDCANCEL); // キャンセルボタンを取得
   if (hItem == NULL || ::IsWindowEnabled(hItem))
    // そのボタンが存在しないか、又は、有効(活性状態)ならば
   {
      SendMessage(WM_COMMAND, IDCANCEL, 0); // 自分自身にIDCANCELを送付する
      return TRUE; // 以降の処理はすっ飛ばす
   }
}

ですね、なので、
// that doesn't have a cancel or the cancel is disabled.
このコメントは間違ってますね、よくあることですが。

いしだ 2011/09/07(水) 18:17:07
> GetParentOwner()ならば大丈夫そうです。

こちらのテストプログラムでも大丈夫でした。
GetParentOwnerってこういう意味だったのですね。

Ownerという単語が入っているので、
CWnd::SetOwnerで設定したものが関わってくるものだと思ってました。
このへんの命名規則ってゴチャゴチャしていますね。

>  1.CDialog::PreTranslateMessage()全体をコピペして、MyDLGのメンバにする。

実際にはCDialogExから派生しているのですが、
このCDialogEx::PreTranslateMessageがフレンドクラスと連携していて、
派生クラスからはコピペで定義し直すことができないようなのです。

CDialogEx::PreTranslateMessageを呼んでしまうと、
結局はCDialog::PreTranslateMessageまで呼ばれてしまいます。

> "マルチラインのエディットがあったときに、そこでESCされても
>  DLGがキャンセルされないバグを直しました(意訳)。"

本来ならその位置に、Kellyさんに提案していただいたような
「子ダイアログなら」という判定が入っていればよかったのですよね?

なぜVC6の頃から修正されていないのでしょうか。
子ダイアログに複数行のエディットボックスを使うこと自体が想定外?

いしだ 2011/09/08(木) 13:06:02
[[解決]]
CDialogExからさらにクラスを派生させ、その中で以下のような処理を入れ、
自作のダイアログはすべてここから派生させることにしました。

これなら、モーダルにもモードレスにも子ダイアログにも基本クラスとして使え、
複数行のエディットボックスを持った子ダイアログのみ
この処理に来てくれるはずと考えていますが、
なにかツッコミどころがあれば指摘していただければと思います。

void CBaseDialog::OnCancel()
{
    if (GetStyle() & WS_CHILD)
    {
        CWnd* pWnd = GetParentOwner();
        if (pWnd != NULL)
        {
            pWnd->SendMessage(WM_COMMAND, IDCANCEL, 0);
        }
        else
        {
            ASSERT(FALSE);
        }
    }
    else
    {
        CDialogEx::OnCancel();
    }
}

また、親子ダイアログを作られた際にこの問題はどのように対処されてきたのか、
引き続き情報いただけたらと思います。

ryo 2011/09/09(金) 13:09:17
CPropertySheet(+CPropertyPage)で試してみたところ

エディットボックス+マルチライン  →ESC押しても消えない
エディットボックス         →閉じる

という動作になっていました。

また、VCのプロジェクトのプロパティで
[構成プロパティ]-[C/C++]-[コマンドライン]を開き
追加のオプションの項目もマルチラインだったのでここでも試してみましたが
やっぱり閉じることはありませんでした。

マルチラインのときは閉じないのがデフォルトなのかも知れません

仲澤@失業者 2011/09/09(金) 15:11:10
ryoさんのおっしゃる通りなのですが、これらのDLG(マルチラインエディットの親)
には、マルチラインエディットでESCが押下されてもWM_KEYDOWNが届いていません。
これは、当該のマルチラインエディットが、ES_WANTRETURNだからかも
しれませんねぇ(vv;)。

当然ですが、当該のDLGはWM_CLOSEとWM_COMMANDでIDCANSELをこの順番で
受信しますが、無視してます。本来「子DLG」として動作すべきDLGは、
WM_CLOSEやID_CANSELで閉じてはいけないのでこうするのが普通です。

振り返って、今回の問題とは
 1.本質的に異なるはずの通常DLGと子DLGのコードを共通にしたい。
ために、
 2.WM_CLOSEとIDCANSELの処理をデフォルトのままにしなければならない。
という、茨の道を選んだ結果なのですね。

そもそも子DLGなんだから、かってに閉じちゃだめなんであって。
本件での一番簡単な解決方法は、VSのDLG等がやっているように、
MyDLG::OnCansel(){
 /*なんもしない*/
}
MyDLG::OnClose(){
 /*なんもしない*/
}
とすれば済む問題なんですね(vv;)。

ロマ 2011/09/10(土) 10:38:16
APIのみで実験してみました(xp)。

WS_POPUPのモーダルダイアログにEditを貼り付け、
Editはサブクラス化して WM_KEYDOWN,WM_CHARをOutputDebugStringでトレースする。

Editにフォーカスを置き、ESCキーを押すと
single-lineの場合
  WM_KEYDOWNは、EditにDispatchされない。
 引き続き、DialogにWM_COMMAND(IDCANCEL)がSendされる。
ES_MULTILINEの場合
  WM_KEYDOWN,WM_CHARはEditにDispatchされる。
 引き続き、DialogにWM_CLOSEがPostされる。

  Editのサブクラス化でWM_KEYDOWN(VK_ESCAPE)のみEditのWindowProcに渡さない場合
 WM_CLOSEは発生しない。

ということから、
ES_MULTILINEだと、EditがWM_CLOSEを発生させているようです。

ロマ 2011/09/11(日) 04:58:39
私の9/10投稿は子ダイアログがない場合です。

引き続き、APIで子ダイアログを実験しました。

モーダルダイアログに子ダイアログを作り、
子ダイアログにEditがあり、このEditがフォーカスを持ち、ESCキーが押されると
single-lineならば
  WM_KEYDOWNはEditにDispatchされない
  モーダルダイアログにWM_COMMAND(IDCANCEL)がSendされる
  (IsSialogMessageが発行しているのだと思います)
ES_MULTILINEならば
  WM_KEYDOWN,WM_CHARはEditにDispatchされる
  Editは子ダイアログにWM_CLOSE()をPostする
  子ダイアログはWM_CLOSEを受けて自身にWM_COMMAND(IDCANCEL)をPostする
  子ダイアログはWM_COMMAND(IDCANCEL)のハンドラがないので何もしない

結論
私の9/10投稿の通り、ES_MULTILINEの場合、
MSは、ESCでダイアログを閉じるように工夫しています。
子ダイアログの場合、DS_CONTROLに限定して、何か細工をしたほうが良いと思います。
# ESCキーで閉じたい子ダイアログもあります。

--------------
ところで
>    // fix around for VK_ESCAPE in a multiline Edit that is on a Dialog
>    // that doesn't have a cancel or the cancel is disabled.
このコメントは引き続くコードと相違していますね

ロマ 2011/09/11(日) 10:14:21
連投すまんです。

ryoさんの実験について

http://support.microsoft.com/kb/125644
"[FIX] プロパティ シートで編集が Esc キーを閉じる"
[現象]
フォーカスが複数行エディット コントロール内にあるときに Esc キーを押してプロパ
ティ シートを IDCANCEL ボタンがある場合でも (OnCancel) を閉じます発生しませ
ん。 この問題も発生自体フォーカスが WM_GETDLGCODE メッセージに対する返信で、
DLGC_WANTALLKEYS または DLGC_WANTMESSAGE の値を返すその他の子コントロールである
ときにします
[状況}
Microsoft は、この資料の冒頭に記載した Microsoft 製品にかかわる問題であることを
確認済みです。 この問題は、Windows NT の Visual C++ バージョン 2. 10 で修正され
ました。

修正されたはずなのですが、復活ですか。

いしだ 2011/09/20(火) 13:52:07
> また、VCのプロジェクトのプロパティで
> [構成プロパティ]-[C/C++]-[コマンドライン]を開き
> 追加のオプションの項目もマルチラインだったのでここでも試してみましたが
> やっぱり閉じることはありませんでした。
>
> マルチラインのときは閉じないのがデフォルトなのかも知れません

たしかにVisualStudioのプロパティやオプションのダイアログも
複数行のエディットボックスでは閉じないようです。

本来は閉じないのがデフォルトだったけど、
MS自身がCDialogのクラス内で気を利かせて閉じるようにコードを付け加えた結果、
CDialogを子ダイアログとして使うとそのままでは副作用が起きてしまう
ということですよね?

> そもそも子DLGなんだから、かってに閉じちゃだめなんであって。
> 本件での一番簡単な解決方法は、VSのDLG等がやっているように、
> MyDLG::OnCansel(){
>  /*なんもしない*/
> }
> MyDLG::OnClose(){
>  /*なんもしない*/
> }
> とすれば済む問題なんですね(vv;)。

それだと、親ダイアログ上に直接複数行エディットボックスを置いているときと
違う動きになってしまいます。

DS_CONTROLの子ダイアログで複数行エディットボックスが無い場合には、
OnCancel()のオーバーライド自体が不要ですよね。
親ダイアログのキャンセル処理に行きますから。

ところが、エディットボックスを複数行にしたとたんに
MyDLG::OnCancel()に来るようになってしまうのが問題なのです。

> 子ダイアログの場合、DS_CONTROLに限定して、何か細工をしたほうが良いと思います。
> # ESCキーで閉じたい子ダイアログもあります。

これは、
if (GetStyle() & WS_CHILD)
の部分にさらにDS_CONTROLも判定したほうがよいということですか?

ロマ 2011/09/20(火) 14:34:50
> if (GetStyle() & WS_CHILD)
> の部分にさらにDS_CONTROLも判定したほうがよいということですか?

と思います。


MDIのChildFrameのようなダイアログを作りたい場合は、
子ダイアログのIDCANCELで閉じたいです。

逆の例として トップレベルウィンドウの一部分に子ダイアログを貼り付けたような
場合(ドッキングしたように見えるやつ)、ESCでメインが閉じるのはまずいとおもいま
す。

ただし、
DS_CONTROLで判定すべきか、WS_EX_CONTROLPARENTの方が良いのかはよくわかりません。
識者の方よろしく。

ロマ 2011/09/20(火) 14:49:52
> 逆の例として トップレベルウィンドウの一部分に子ダイアログを貼り付けたような
> 場合(ドッキングしたように見えるやつ)、ESCでメインが閉じるのはまずいとおもいま
> す。

すみません、親にWM_CLOSEを送って閉じるのと混同していました。
この部分取り消します。

いしだ 2011/10/05(水) 09:31:40
[[解決]]
>> if (GetStyle() & WS_CHILD)
>> の部分にさらにDS_CONTROLも判定したほうがよいということですか?
>
> と思います。
>
> MDIのChildFrameのようなダイアログを作りたい場合は、
> 子ダイアログのIDCANCELで閉じたいです。

MDIアプリを作成したことが無いため、
そのようなケースがあることは考えていませんでした。

「WS_CHILDかつDS_CONTROLなら」という判定にしておきます。
ありがとうございます。

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