Home / DialogBox

DialogBox

2015-3-28 少し整理、追記。
ブログより。とりあえず転記。

モードレス ダイアログボックス

意外に作れない人が多い。
俗に言うフォームと同じに思うが、終了部分が難しく思われているらしい(某も難しいと思っていたような…)
昔のMFC サンプルから抜粋すると肝心な部分は次のようになっている。

void CAdderDialog::OnCancel()
{
    ((CMainDlg*)m_pParent)->BoxDone();
    DestroyWindow();
}

void CAdderDialog::PostNcDestroy()
{
    delete this;
}

自身でどう破棄するのかがミソであり、サンプルは Windows メッセージと共に破棄しようと言うもの。
PostNcDestroy 以降にメッセージは来ないので、それで破棄する、と。
まぁ、「そんなの嫌だ、メッセージに頼らず破棄するのだ!」となれば MFC ならデタッチする手がある。
仮に OnCancel() で破棄するには次のようにしても問題は無い。

void CAdderDialog::OnCancel()
{
    ((CMainDlg*)m_pParent)->BoxDone();
    DestroyWindow();
    Detach();
    delete this;
}

void CAdderDialog::PostNcDestroy()
{
    //delete this;
}

VB なんかではウィンドウの破棄とフォーム?の破棄とを連動させていたり、と何かにつけドツボに嵌められるのでからくりとしては知っておいて損は無い。

ちらつかない描画

古典的な Windows 描画でリサイズなんかでちらつく、となれば WM_ERASEBKGND。
これの対策についてググると「裏画面で書いておいて一気に転送」等の回答が定番なようだ。
でも描画が本題でないアプリではやり過ぎな感がするし、そもそも背景がクリアされる事の要因が大きい場合多し。

そういう時に、まずやるべきはクリッピング。
以下、古いMSDNからコピペ。

BOOL CPictureView::OnEraseBkgnd(CDC* pDC)
{
    // get client rectangle
    CRect rcClient;
    GetClientRect(&rcClient);

    // get image rectangle
    CRect rcImage;
    GetImageRect(rcImage);

    // create clipping region
    CRgn clipRgn;
    clipRgn.CreateRectRgnIndirect(&rcClient);
    pDC->SelectClipRgn(&clipRgn);
    pDC->ExcludeClipRect(&rcImage);

    CBrush brush(RGB(0,0,0)); // black
    pDC->FillRect(&rcClient, &brush);
    pDC->SelectClipRgn(NULL);
    return TRUE;
}

豆知識の類らしいが、ならば仕組みとして用意しておいて欲しい位。

(2015-2-4)
ダイアログボックスに於いて、コントロールを重ねて配置したい場合がたまにある。
兄弟が重なると後で描画された内容が優先されてしまう。
これ、必ずしもZ軸の問題じゃなく、描画順の問題。
ならば上に配置しているコントロールを UpdateWindow しても良いのだが、何だかなぁ、と思う時にコレ。テクニックとして覚えておいて損は無い。

なお、WS_CLIPSIBLINGS がまさしくそれをやってくれそうな説明なんだけど、うまくいった試しが無い。オーナードローには効かない、とかだろうか…

今更な MFC:プロパティシートっぽい事を自前でやる

最新の技術に比べれば手間が掛かるとかあるが、枯れた感があるし、動作速度は速い。

ありきたりな入力フォームではなく、専用の入力画面だと、どうしてもコントロール類を並べて、レトロなDOS画面、いや今風のWEB?なレイアウトになってしまう。

コントロールをごてごて並べるのはイカン、と言う事で、Windows 95 からプロパティシートっつう物がある訳ですが、これがまた変に使い方が限定的になっていて、ダイアログリソースを切り替えるだけの目的には適さない(タブでのページ切り替えが余計なんだよね)。

そこで他の方法を考えていると、ダイアログを子ウィンドウにしているプログラムを見掛けた。プロパティシートも似たような手法だと思われるので、驚くことも無いが、如何せん MFC だと...

で、ダイアログベースアプリでやってみる。 ページ用のダイアログリソースをチャイルドで用意し、OnInitDialog() で
a.Create(IDD_DIALOG1, this), a.ShowWindow(SW_SHOW);
b.Create(IDD_DIALOG2, this), b.ShowWindow(SW_SHOW);

適当なイベントで a, b の表示/非表示を切り替えてみるが、不信な動きは無さそう。 ESC でページが消えたりするが後回し。

プロパティシートのように、何かページを切り替えるコントロールを置ける(親の)領域を確保しようと Move しようかと思った処で手抜きな方法を見つけた。 ダイアログリソースには x, y 座標が書ける訳で、これを(あらかじめ)セットしておけば上や左の offset が自動的に出来る。右や下はサイズによりけり。

今まで x, y 座標は有り難味を感じなかったけど、初めて有効に使えて嬉P!

予め問題点

元々、単一のダイアログリソースが前提な物を組み合わせる訳なので、ダイアログマネージャの動作が邪魔な場面が見えて来る。

1. シートに置いたボタンが強調表示として残ってしまう。 ページの方にもボタンを置くと、これも強調表示され、ガイドをよく知るユーザーからはケチが付くだろう。

2. タブオーダーはダイアログリソースを超えて管理されない。 --> と思ったら [コントロール]スタイルを指定すれば、あたかも単一のリソースダイログのように振舞うらしい。と言ってもタブオーダーに関してのみ、そう振舞うのであって、ID管理は個別のままと思う(そうでないと困る)。

参考:ダイアログの重ね合わせ時のタブオーダ

リソースではこうなる STYLE DS_CONTROL | WS_CHILD 因みに MFC CPropertySheet クラスの使い方にはページを次のようにせよ、と書いてある。

・[一般] タブの [キャプション] ボックスに、タブに表示する文字列をセット
・[スタイル] タブの [スタイル] ボックスを [チャイルド]
・[スタイル] タブの [境界線] ボックスを [細枠]
・[スタイル] タブの [タイトル バー] チェック ボックスがオン
・[その他のスタイル] タブの [無効] チェック ボックスがオン

[タイトルバー] はどうせ見えなくなるが、キャプションをタブのテキストに流用する為と思われる。
[細枠] は何故なのか不明。必要なら自分で指定すればいいのに…
ユーザーがダイアログエディタでデザインするのに余裕を持たせたいだけなんじゃないかと思われる。

マイクロの悪い癖は、大した理由も無いくせに、それでいて説明が無い処だねぇ。
どうしてこんな体質なのか? 日本人から見ると不思議に思う。

ダイアログリソース x, y 座標

手抜き出来ると書いたが、ダイアログ単位はフォントに左右され、pixel 単位より粗いので微妙な位置決めは出来ない。move するにしても気持ちだけ x, y 座標を指定するのは良い習慣だろう。

ふぅ、また次回。

シートに置いたボタンの強調表示

フォーカスと強調表示は商用ソフトでも結構いい加減に済ませているのも見かけるし、拘るのもあれだが、枯れたMFCと言う以上、頑張ってみる。 まず、ダイアログ重ね合わせでは、切り替え後に現れたページにフォーカスを当てたい。 ::SetFocus() だと強調表示を考慮してくれないので、CDialog::NextDlgCtrl() を使う。 が、これは元々フォーカスが無いページに対しては効果が無いようだ。 となるとコントロールを狙う形式の CDialog::GotoDlgCtrl を使う事になるが、やって欲しいのはページ内のTABSTOPを持つ最初のコントロールにフォーカスを当てたい訳で、もどかしい。 --> そこで仕方無く自前でコードを書く(何で今さらな感があるが)

void FirstDlgCtrl(HWND hDlg)
{
    for (HWND h = ::GetWindow(hDlg, GW_CHILD);
         h != 0;
         h = ::GetWindow(h, GW_HWNDNEXT))
    {
        DWORD style = ::GetWindowLong(h, GWL_STYLE);
        if (style & WS_TABSTOP)
        {
            ::PostMessage(hDlg, WM_NEXTDLGCTL, (WPARAM)h, 1L);
            break;
        }
    }
}
これで強調表示も一見うまく処理出来ているかのように見えたが、元の強調表示なボタンが無効になると強調表示が解除されない。 他のボタンがアクティブになれば結局は解除されるので、MSの仕様/実装のバグだと思われる。やれやれ... ここでまた強調表示を解除するコードを書く。これは MFC に無いようだ。BS_DEFPUSHBUTTON 定数を解除せねばならない。(スタイルじゃなくステートが相応しいんだけど...)
xxxxx(HWND hBtn)
{
    UINT st = GetButtonStyle(hBtn);
    if ((st & 0x0F) == BS_DEFPUSHBUTTON)
    {
        st &= ~0x0F | BS_PUSHBUTTON;
        SetButtonStyle(hBtn, st, TRUE);
    }
}
※BS_XXXX はビット論理和と値とが混在している。  CPropertySheet::OnCommand などを見ると 0x0F でマスクしている為、同じようにした。

WM_NEXTDLGCTL を送信するルーチンで、

正しくは可視と有効状態を見るべき、と考えて ::IsWindowVisible(h) と ::IsWindowEnabled(h) とを噛ましたのだが、そうするとページの初期フォーカス設定で引っ掛かかってしまった。ページ切り替えボタンハンドラでは問題無いので、タイミングの問題。 ページ表示は ShowWindow(SW_SHOW) で表示しているので、::IsWindowVisible() は成立すると思ったが、そうではない。 こう言う場合、無闇な遅延コードを入れてもさしたる問題にはならないが、闇に包まれた Windows のせいだと考えると気に入らない。::IsWindowVisible() の擬似コードでも解ればいいのだが... 仕方無いので対策(不具合)を考える。 想像するに MS の事だから ::IsWindowVisible() は少なくとも 1度は Paint が走っている事を条件にしている疑いがある。 それがユーザーの為と考えているだろうから。ダイアログではないフレームウィンドウの初期化も ShowWindow の後に UpdateWindow を呼ぶのが定石になっている。 でも、ここでは描画されていようがいまいが、フォーカスを当てるべきコントロールか否かが問題なのでウィンドウのスタイルを見るようにした(描画は二の次だ!)。 FirstDlgCtrl 関数のループ内を
    const DWORD dw = WS_VISIBLE | WS_TABSTOP;
    DWORD style = ::GetWindowLong(h, GWL_STYLE);
    if ((style & dw) == dw) && ::IsWindowEnabled(h)
    break;
とする事で望むようになった。 ::IsWindowVisible は WS_VISIBLE スタイルを見ている他に、何か余計な判定が入っている、と言う事だ。この辺をAPI ドキュメントに、ほんのちょっと書き加えるだけで解る事なのだが、MS (の開発者)に期待するだけ無駄と言うものだろう。
Home / DialogBox


© 2008 usskim    http://usskim.web.fc2.com/
inserted by FC2 system