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を付けるだけでよかった問題です。
なにかよい方法はありませんでしょうか。
え~と、悩む必要はまったくありませんよねぇ。
CDialog::PreTranslateMessage()はvirtualなので、
「派生先で好きに変更してね」って書いてあるのと同じです。
エドモンドの連中も、こういった問題は起こるだろうなぁと思っていた
というこです。
当該のDLGクラスでPreTranslateMessage()をオーバーロードして
好きなように変更しましょう。
もとい。orz.
オーバーライドの間違いですね。
> 当該のDLGクラスでPreTranslateMessage()をオーバーロードして
> 好きなように変更しましょう。
CDialog::PreTranslateMessageの中で呼ぶ処理の順番には意味がありそうで、
どうオーバーライドすればよいのかがいまいち不明なのです。
VC6の時代から、この手の親子ダイアログはよくあったと思うのですが、
実際に作成されて、この問題に遭われたかたはいらっしゃいませんでしょうか。
ひょっとしたら、念のためにと子ダイアログ側でオーバーライドして
空処理化しておいたOnCancelがこっそり呼ばれていて、
複数行エディットボックスだけはESCキーを押しても
ダイアログが閉じなくなっているケースもあるのではと思うのですが。
こんな感じでいかがですか?
void CChildDialog::OnCancel()
{
if (GetStyle() & WS_CHILD)
GetTopLevelParent()->SendMessage(WM_COMMAND, IDCANCEL);
else
CDialog::OnCancel();
}
> こんな感じでいかがですか?
子ダイアログからGetTopLevelParentを使ってみたところ、
戻り値が親ダイアログを通り越してCMainFrameのポインタになってしまいました。
親ダイアログのポインタが取れればこの方法でいけるかもしれませんが、
CWndのメンバにはそれらしい関数が見あたりませんね。
> 子ダイアログからGetTopLevelParentを使ってみたところ、
> 戻り値が親ダイアログを通り越してCMainFrameのポインタになってしまいました。
ダイアログベースでしか確認してませんでしたm(_ _)m
GetParentOwner()ならば大丈夫そうです。
>CDialog::PreTranslateMessageの中で呼ぶ処理の順番には意味がありそうで、
>どうオーバーライドすればよいのかがいまいち不明なのです。
う~む。そこまで説明が必要ですか(vv;)。
1.CDialog::PreTranslateMessage()全体をコピペして、MyDLGのメンバにする。
2.具合の悪いコードを修正する。
本件の場合、MyDLGは、当該条件が成立しても、自身にIDCANCELを送らない
こととする。
4.当然だが、派生元のCDialog::PreTranslateMessage()は呼ばない。
だけですよね。
ちなみに、CDialog::PreTranslateMessageのコメント(fix around for・・)は、
マルチラインのエディットがあったときに、そこでESCされても
DLGがキャンセルされないバグを直しました(意訳)。
と言う意味ですよねぇ。
まんまですわな(笑)。
ついでなのでもうちょっと説明
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.
このコメントは間違ってますね、よくあることですが。
> GetParentOwner()ならば大丈夫そうです。
こちらのテストプログラムでも大丈夫でした。
GetParentOwnerってこういう意味だったのですね。
Ownerという単語が入っているので、
CWnd::SetOwnerで設定したものが関わってくるものだと思ってました。
このへんの命名規則ってゴチャゴチャしていますね。
> 1.CDialog::PreTranslateMessage()全体をコピペして、MyDLGのメンバにする。
実際にはCDialogExから派生しているのですが、
このCDialogEx::PreTranslateMessageがフレンドクラスと連携していて、
派生クラスからはコピペで定義し直すことができないようなのです。
CDialogEx::PreTranslateMessageを呼んでしまうと、
結局はCDialog::PreTranslateMessageまで呼ばれてしまいます。
> マルチラインのエディットがあったときに、そこでESCされても
> DLGがキャンセルされないバグを直しました(意訳)。
本来ならその位置に、Kellyさんに提案していただいたような
「子ダイアログなら」という判定が入っていればよかったのですよね?
なぜVC6の頃から修正されていないのでしょうか。
子ダイアログに複数行のエディットボックスを使うこと自体が想定外?
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();
}
}
また、親子ダイアログを作られた際にこの問題はどのように対処されてきたのか、
引き続き情報いただけたらと思います。
CPropertySheet(+CPropertyPage)で試してみたところ
エディットボックス+マルチライン →ESC押しても消えない
エディットボックス →閉じる
という動作になっていました。
また、VCのプロジェクトのプロパティで
[構成プロパティ]-[C/C++]-[コマンドライン]を開き
追加のオプションの項目もマルチラインだったのでここでも試してみましたが
やっぱり閉じることはありませんでした。
マルチラインのときは閉じないのがデフォルトなのかも知れません
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;)。
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を発生させているようです。
私の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.
このコメントは引き続くコードと相違していますね