CMFCMenuBarが勝手に複数段になる

[戻る]
NOR 2011/02/16(水) 18:31:30
Visual C++ 2008 MFCです。

2008のCMFCMenuBarで微妙な動作があり、
環境によるものなのかMFCのバグなのかわからないことがあります。

デフォルトの設定のままSDIのプロジェクトをビルドして実行させ、
タスクバーに表示される自分自身の部分をマウスで連打していると、
アプリケーションが最小化と復元を繰り返しますが、
そのうちメニューが勝手に複数段になってしまいます。

タイトルバーなどをドラッグすると、元に戻ります。

自分の環境はXPで、最小化時にアニメーションが入る設定ですが、
このような動作に遭遇されたかたはいらっしゃいませんでしょうか。
また、他の環境でも発生するか試していただけませんでしょうか。

hirocco 2011/02/16(水) 23:08:43
CMFCMenuBarって具合悪いよね
動いちゃうのが嫌い
そのままじゃ使いずらいねぇ

えーと 2011/02/17(木) 14:54:59
VS2010 + XPでも発生するみたいですね。
#他のOSではテストしていません。

ちなみに、Office2007スタイルだと大丈夫みたいです。

MFCのソースを眺めてみましたが、
以下のようにすれば一応対処はできている感じです。

CMainFrame::OnSize()を処理し、

void CMainFrame::OnSize(UINT nType, int cx, int cy)
{
    CFrameWndEx::OnSize(nType, cx, cy);

    CMFCVisualManager* pManager = CMFCVisualManager::GetInstance();
    if( !pManager->IsOwnerDrawCaption() &&
        (GetRibbonBar() == NULL ) &&
        ( nType == SIZE_RESTORED ) &&
        ( !m_bWasMaximized )){
        RecalcLayout();
    }
}

MFCのソースを見てみると、最大⇔最小ではならないようになっていました。

原因を特定するために、MFCの中身を追いかけてみると、
CFrameWndEx::OnSize()内でAdjustDockingLayout()が復元時に呼び出されますが、
メニューバーの配置情報がツールバーの高さを含んでしまってます。
#必ずなるとは限らないが、MFC内部ではPostMessage()でメッセージを置いて
#くる処理がたくさんあるので、今回のような連打による状態変更では
#置いてきたメッセージが先に処理されていないので不具合が発生したのかと
#思います。

Office2007のテーマを使用している場合、(WindowsXPかVista/7/2008でDWMをOFF)
その処理の前でm_Impl.OnWindowPosChanging(&wndpos);
が呼び出されており、そのおかげでその後のAdjustDockingLayout()で
領域が正しく計算されているようです。

本当はm_Impl.OnWindowPosChanging()を直接呼び出したいとこなのですが、
protectのメンバですし、この関数内でもリボン+Office2007以外は処理しない
ようなコードになっていました。

なので、AdjustDockingLayout()した後にRecalcLayout()と2度手間になりますが、
これが確実のようです。

hirocco 2011/02/17(木) 16:00:26
VS9+XPではなりますねぇ
ちなみに
VS9+Vista
VS9+7
ではかなり連打してみましたけどなりませんねぇ

でも,なぜメニューバーを移動可能なものにしたんでしょうね
VS自体メニューは動かせないのにねぇ
XPだとフォントもおかしいしねぇ
そういうのはあくまでオプション的な位置づけで
デフォルトはいわゆる王道で維持してほしいですね

ロマ 2011/02/17(木) 17:50:51
横から失礼します。

> #必ずなるとは限らないが、MFC内部ではPostMessage()でメッセージを置いて
> #くる処理がたくさんあるので、今回のような連打による状態変更では
> #置いてきたメッセージが先に処理されていないので不具合が発生したのかと
> #思います。

これ、確認しました。
タスクバーからPostされるWM_SYSCOMMANDとアプリケーション内からのPostMessageは
どちらが先に来るか、わからないということですか、面白いです。

ということは、一般的に言って、
WM_SYSCOMMANDの終了時には、メッセージキューを空にする必要がありますね。

NOR 2011/02/18(金) 11:48:37
hiroccoさん、えーとさん、ロマさん、試していただきありがとうございます。

えーとさんに提案していただいたCMainFrame::OnSize()の処理を入れてみたところ、
メニューが正しく復元されるようになりました。
MFCの詳しい流れも教えていただき、ありがとうございます。

ちなみに、上記のSDI形式だとクラスビューなどのペインが作られますが、
これらのペインも同じ操作でレイアウトが壊れてしまいます。

元となったBCGSoftのライブラリのサンプルでは、
メニューは壊れませんでしたが、各ペインは同じように壊れました。

自分でもソースを追ってみているのですが、場所を見つけられない状況です。
こちらのほうもCMainFrame自身で対応できそうなものでしょうか?

>> hiroccoさん
> でも,なぜメニューバーを移動可能なものにしたんでしょうね
> VS自体メニューは動かせないのにねぇ

これに関しては、自分の昔のソースを見てみたところ、
メニューバーのEnableDocking()を呼ばずに、
ドッキングの順番を以下のようにしていました。

//m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
DockPane(&m_wndMenuBar);
EnableDocking(CBRS_ALIGN_ANY);
DockPane(&m_wndToolBar);

> XPだとフォントもおかしいしねぇ

これはCMFCMenuBar::SetMenuFont()で直しています。

hirocco 2011/02/18(金) 11:58:11
>> でも,なぜメニューバーを移動可能なものにしたんでしょうね
>> VS自体メニューは動かせないのにねぇ
>
>これに関しては、自分の昔のソースを見てみたところ、
>メニューバーのEnableDocking()を呼ばずに、
>ドッキングの順番を以下のようにしていました。
>
>//m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
>m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
>DockPane(&m_wndMenuBar);
>EnableDocking(CBRS_ALIGN_ANY);
>DockPane(&m_wndToolBar);
忘れたけど,グリップ残りませんでしたっけ?
→うるおぼえ/うろおぼえ


>> XPだとフォントもおかしいしねぇ
>
>これはCMFCMenuBar::SetMenuFont()で直しています。
メイリオが邪魔くさいのよねぇ

ロマ 2011/02/18(金) 12:10:55
> ということは、一般的に言って、
> WM_SYSCOMMANDの終了時には、メッセージキューを空にする必要がありますね。

これ、間違いでした。
SC_RESTOREとSC_MINIMIZEがPostされた場合のみでいいのかなぁ、
よくわからなくなりました。

ロマ 2011/02/18(金) 16:38:47
連投すまんです。
暫定案(ただしAPI)
フレームウィンドウで
case WM_SYSCOMMAND:                 // WM_SYSCOMMANDが来た時に
  if(GetQueueStatus(QS_POSTMESSAGE))// もしほかにPostされたメッセージがあれば
    return 0;                       // 無視する
  else
    return CallWindowProc(          // そうでなければ基底のハンドラを呼ぶ

えーと 2011/02/18(金) 17:28:24
ロマさんの案でやってみたんですが、駄目でした。
良い案だと思ったんですが。。。

ほかにPostしたものがなくても通知が来ているようです。

そのときのウィンドウの状態をみると、連打していくうちに
SC_MINIMIZEが来たときに、ウィンドウは既にアイコン状態だったり
SC_RESTOREが来たときに、ウィンドウはアイコン状態でなかったりと
アニメーションしている間で処理が実行されるのが問題なような...

>元となったBCGSoftのライブラリのサンプルでは、
>メニューは壊れませんでしたが、各ペインは同じように壊れました。
確かにペインの幅が小さくなってますね。

で、あれこれMFCのソースを追っていったのですが、
良く思えば、復元するときに配置を再計算する必要があるの?
ということに気がつきました。

なので、
OnSize()の処理を先程提示した処理ではなく、

    if( nType == SIZE_RESTORED ){
        CDockingManager::m_bDisableRecalcLayout = TRUE;
        CFrameWndEx::OnSize(nType, cx, cy);
        CDockingManager::m_bDisableRecalcLayout = FALSE;
    } else {
        CFrameWndEx::OnSize(nType, cx, cy);
    }
としてみてください。
かなり連打してみましたが、大丈夫なようです。

やっているうちに、そもそもこのように連打するユーザがいるの?
と思ってしまいました。(笑)

NOR 2011/02/18(金) 18:24:49
ロマさん、えーとさん、
提案していただきありがとうございます。

> OnSize()の処理を先程提示した処理ではなく、
> 
>     if( nType == SIZE_RESTORED ){
>         CDockingManager::m_bDisableRecalcLayout = TRUE;
>         CFrameWndEx::OnSize(nType, cx, cy);
>         CDockingManager::m_bDisableRecalcLayout = FALSE;
>     } else {
>         CFrameWndEx::OnSize(nType, cx, cy);
>     }
> としてみてください。
> かなり連打してみましたが、大丈夫なようです。

上記の方法で、通常状態時の最小化&復元処理では壊れることはなくなりました。

ただ、最大化状態で当初の連打処理をやると、
やはりペインのサイズがおかしくなってしまいました。

また、最大化から元のサイズに復元した際に、
各ペインの配置が再計算されないようです。

> やっているうちに、そもそもこのように連打するユーザがいるの?
> と思ってしまいました。(笑)

連打自体はまず無いでしょうけど(笑)、テスト環境で、

タスクバー内で自アプリに切り替えようとしてクリック
↓
実はすでにアクティブな状態だったため、最小化の動作が始まった
↓
慌てて元に戻すために、最小化アニメーション中に再クリック

という動作を行った際にこの問題が発生した、という経緯でした。
実際、連打しなくても、最小化アニメーション中に再クリックすると、
結構な確率で一発で発生するようです。

この程度の操作は通常でも起こりえるかなと思ったので、
対応できるならしておきたいと思いました。

Feature Packのクラスはこの手の問題を多く抱えていますね…。

ロマ 2011/02/18(金) 19:28:16
えーとさん
試していただきありがとうございます。
確かにアニメーション時の連打はダメですね。
(私はアニメーション効果が嫌いなので、テストしていませんでした)

アニメーション時には何がおきているのでしょうか。
Spy++では見づらいし、よくわかりませんでした。

えーと 2011/02/21(月) 14:16:28
>ただ、最大化状態で当初の連打処理をやると、
>やはりペインのサイズがおかしくなってしまいました。

>また、最大化から元のサイズに復元した際に、
>各ペインの配置が再計算されないようです
もう少しテストしたら良かったです。ToT

一応、改良してみたのですが
CMainFrame::OnSize()を処理し、
if(( nType == SIZE_RESTORED && !m_bWasMaximized ) ||
   ( nType == SIZE_MAXIMIZED && m_bIsMinimized )){
    CDockingManager::m_bDisableRecalcLayout = TRUE;
    CFrameWndEx::OnSize(nType, cx, cy);
    CDockingManager::m_bDisableRecalcLayout = FALSE;
} else {
    CFrameWndEx::OnSize(nType, cx, cy);
}
でいけませんか?
こちらでテストしてみた限りでは、これらの問題も発生していないようですが、
そちらでも試してみてください。

>連打自体はまず無いでしょうけど(笑)、テスト環境で、
 :
>結構な確率で一発で発生するようです。
なるほど。
こちらで10回程試したら全然起きなくて、10秒ぐらい連打すると発生した
ものですから...
マシンによって起きやすいなら対処したくなりますね。

>アニメーション時には何がおきているのでしょうか。
>Spy++では見づらいし、よくわかりませんでした
こちらでもいろいろ見ているのですが、私もわからないです。
これを抑制したほうが良いのですが...

> Feature Packのクラスはこの手の問題を多く抱えていますね…。
BCGSoftの方達もXP上でのテストはおざなりになっているのでしょう。
今回の件でも他のOSでは発生していないようですし...
これまでもXP上(もしくはDWMがOFF)で発生するようなバグが多かった
ように思えます。

NOR 2011/02/21(月) 18:10:29
[[解決]]
> 一応、改良してみたのですが
> CMainFrame::OnSize()を処理し、
> if(( nType == SIZE_RESTORED && !m_bWasMaximized ) ||
>    ( nType == SIZE_MAXIMIZED && m_bIsMinimized )){
>     CDockingManager::m_bDisableRecalcLayout = TRUE;
>     CFrameWndEx::OnSize(nType, cx, cy);
>     CDockingManager::m_bDisableRecalcLayout = FALSE;
> } else {
>     CFrameWndEx::OnSize(nType, cx, cy);
> }
> でいけませんか?

いろんなパターンで相当テストしてみましたが、
メニューやペインが崩れることは無さそうです。

こちらでもm_bWasMaximizedやm_bIsMinimizedを参照して
CMainFrame::OnSize()内でいろいろ試していたのですが、
この組み合わせでよかったのですね。

本当にありがとうございます。

NOR 2011/02/23(水) 08:59:50
何度も申し訳ありません。
えーとさんに提案していただいた方法を正式に組み込んで
いろいろテストしていたのですが、一つ動かないケースがでてしまいました。

これは連打問題とは関係無い流れに対しての副作用なのですが、
最大化していない状態から最小化状態とし、
タスクバーを右クリックして「最大化」をいきなり選ぶと、
状態がおかしくなってしまいました(内部領域が広がらない)。

原因自体は自分でもよくわかっているつもりですが、
そのときのOnSize()を見ると、nType=2(SIZE_MAXIMIZED)、
m_bIsMinimized=TRUE、m_bWasMaximized=FALSEとなっていて、
あらかじめ最大化されている状態で最小化→最大化をやったときと
同じ状態になっていました。

非最大化状態→最小化→最大化という流れのときには、
CDockingManager::m_bDisableRecalcLayoutをいじらずに、else内の
CFrameWndEx::OnSize(nType, cx, cy);
のほうだけを呼ぶようにすればよいのだろうと思われるのですが、
これはCFrameWndExの状態などから区別できますでしょうか?

えーと 2011/02/23(水) 13:55:21
NORさん

>これは連打問題とは関係無い流れに対しての副作用なのですが、
>最大化していない状態から最小化状態とし、
>タスクバーを右クリックして「最大化」をいきなり選ぶと、
>状態がおかしくなってしまいました(内部領域が広がらない)。

投稿した後に気づきました。
更に対応したものを作ろうかと思っていたところです。

もう1つ問題があります。

最大化→最小化→解像度変更→最大化
通常→最小化→解像度変更→復元
など、アイコン状態で解像度変更してから、復元や最大を
行う時にも発生します。


>これはCFrameWndExの状態などから区別できますでしょうか?

残念ながら登録されているメンバ変数などからは判定できません。
ですが、m_bWasMaximized変数をアイコンになるとき、
その前の状態を保持するという意味にすれば対処できますし、
別のメンバを宣言して、前の状態を保持しておいてもいいかと思います。


残念ながらOnSize()だけでは対処できませんでしたが、以下のように
すればなんとかいけそうです。
そちらでもテストしてみてください。

1.CMainFrameのメンバに
  BOOL m_bResetFrame;
  を追加し、コンストラクタでFALSEを指定
  ※変数名は自由に決めてください。
2.OnSize()を処理し
    // 復元および通常操作 ※アイコン状態で解像度が変更されていない場合
    if(( !m_bResetFrame ) && 
       (( nType == SIZE_RESTORED  && !m_bWasMaximized ) ||
        ( nType == SIZE_MAXIMIZED && m_bIsMinimized && m_bWasMaximized ))){
        CDockingManager::m_bDisableRecalcLayout = TRUE;
        CFrameWndEx::OnSize(nType, cx, cy);
        CDockingManager::m_bDisableRecalcLayout = FALSE;
    // アイコン化の場合、前の状態(最大化)をm_bWasMaximizedに設定
    } else if( nType == SIZE_MINIMIZED ){
        BOOL bMaximized = m_bWasMaximized;
        CFrameWndEx::OnSize(nType, cx, cy);
        m_bWasMaximized = bMaximized;
    // アイコン状態で、解像度変更後1回目はデフォルト処理とする
    } else {
        CFrameWndEx::OnSize(nType, cx, cy);
    }
    m_bResetFrame = FALSE;

3.OnDisplayChange()を処理し、(WM_DISPLAYCHANGEを手動で登録)
    if( !(m_bResetFrame = IsIconic()) ){
    // 解像度変更で最大->最大は既に再計算されているので、通常の大きさのときのみ
行う
        if( !m_bWasMaximized ){
            RecalcLayout();
        }
    }

えーと 2011/02/23(水) 14:00:58
OnDisplayChange()の
処理の最後に
return __super::OnDisplayChange( wParam, lParam );
を追加してください。

貼り付けし忘れました。

えーと 2011/02/23(水) 14:06:21
更にコメント
アイコンを最小に置き換えてください。

また、
// アイコン状態で、解像度変更後1回目はデフォルト処理とする
を
// 最小から状態変更で、解像度変更後1回目
// および、最大化(最小からの復元以外)
です。

何度もすみません。T.T)

NOR 2011/02/23(水) 16:45:12
[[解決]]
えーとさん、本当にありがとうございます。
教えていただいたルーチンで、前述の問題も発生しなくなりました。

> 最大化→最小化→解像度変更→最大化
> 通常→最小化→解像度変更→復元
> など、アイコン状態で解像度変更してから、復元や最大を
> 行う時にも発生します。

これは全く考えていませんでした。
教えていただきありがとうございます。

この処理は今後作成するアプリケーションでも必要になってくるでしょうから、
CFrameWndExを派生させたクラスを用意し、その中に今回の処理を入れ、
CMainFrameは常にこのクラスから派生するようにしようかと思います。

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