MDIアプリケーションの子ウィンドにスクロールバーをつける

[戻る]
YM555 2009/12/22(火) 10:35:31
Visual Studio 2005
WindowsXP SP3

MFCを使用してMDIアプリケーションを作成しております。
子ウィンドにグラフを表示して、スクロールバーによって画面の一部を
スクロールさせようと考えております。
最初はCScrollViewを継承した子ウィンドを考えておりましたが
1)スクロールバーを出しっぱなしにできない
2)画面の一部のみスクロールさせることができない
ようですので、子ウィンドのスタイルを変更してスクロールバーを表示
させようとしまして、下記の設定を行いました。
なお、CMyViewはCViewクラスから継承しております。
BOOL CMyView::PreCreateWindow(CREATESTRUCT& cs)
{
    // TODO: この位置で CREATESTRUCT cs を修正して Window クラスまたはスタイル
を
    //  修正してください。
    cs.style|= WS_HSCROLL;    // ←これを追加
    cs.style|= WS_VSCROLL;    // ←これを追加

    return CView::PreCreateWindow(cs);
}
これにてスクロールバーの表示とOnHScroll()、OnVScroll()を使って
グラフのスクロールはできているのですが、子ウィンドの画面右下すみを
別のウィンドで隠した後、ゆっくり動かすと正しく再描画されず青く残る
現象が発生し、これを修正できません。
これを修正する方法をご存知のかたはおりませんか?

仲澤@失業者 2009/12/22(火) 13:11:08
まったくしょうもないバグをほってありますよね。
詳しくは書きませんが、MDI子ウインドウがWM_NCPAINTを正しく
処理できない仕組みになっているのが原因だと思われます。

対症療法しかありませんが、自分の場合は
 1.当該ウインドウのWM_PAINT=OnPaint()をオーバーライドする
 2.OnPaint()内でGetWindowLongPtr()を使用してスタイルを取得し、それが
 3.(WS_HSCROLL | WS_VSCROLL)スタイルを含むのなら
   3.1 当該ウインドウのサイズYを1増やしてSetwindowPos()する
   3.2 サイズYを元に戻してSetwindowPos()する
 4.派生元のOnPaint()処理を行う

でやってます。

ちなみに、OnNcPaint()をオーバーライドしてがんばっても、
多分徒労に終わると思います。

あと、SetwindowPos()で使う座標は親のクライアント座標なので
GetWindowRect()とScreenToClient()を使って何とかしてください。

YM555 2009/12/22(火) 16:56:48
仲澤@失業者様、ありがとうございます。

早速試してみまして、たしかに解消されることを確認しました。

しかし自分のアプリケーションがドキュメント−ビュウを使用しているためか
OnPaintで処理しますと画面が表示されず、また子ウィンドの書き換えを延々と
行うらしくCPU使用率が100%にはりつきます。
下記のソフトにて試したのですが、これで指摘していただいた通りの動きに
なるのでしょうか?
void CMyView::OnPaint()
{
    CPaintDC dc(this); // device context for painting
    // TODO: ここにメッセージ ハンドラ コードを追加します。
    // 描画メッセージで CView::OnPaint() を呼び出さないでください。

    RECT  tRect;

    this->GetWindowRect( &tRect );
    this->ScreenToClient( &tRect );
    this->SetWindowPos( NULL, tRect.left,
                              tRect.top, 
                              tRect.right-tRect.left,
                              tRect.bottom-tRect.top+1, 
                              SWP_NOZORDER );
 
   this->SetWindowPos( NULL, tRect.left,
                              tRect.top, 
                              tRect.right-tRect.left,
                              tRect.bottom-tRect.top+1, 
                              SWP_NOZORDER );
}

>>まったくしょうもないバグをほってありますよね。

古くからあるバグなのでしょうか。
ためしにVisual C++ Ver6.0でもやってみましたが同じ症状を確認しました。
また、Visual Studio2005付属のSpy++アプリケーションでも同様の現象が
発生することを確認しました。
Exel2000ではアクティブでないウィンドのスクロールバーを消してますね。

>>ちなみに、OnNcPaint()をオーバーライドしてがんばっても

これも試してみましたが、やはり徒労に終わりました。

ITO 2009/12/22(火) 17:00:01
うーーん
 僕なら、「CFormView」を選択しますね。

 1.PictureControlで適当なグラフ画面を作成する。
 2.PictureControにグラフ画面を直接描画する。
 3.専用のX,Yのスクロールバーを置く。
 4.OnHScroll()、OnVScroll()を使って移動量を設定する。
 5.場合によっては、Invalidate();
  6.OnDraw()内で、スクロールの移動量に合わせたグラフ描画を行なう。

の手順で行ないますね。

仲澤@失業者 2009/12/22(火) 17:50:00
>OnPaintで処理しますと画面が表示されず、また子ウィンドの書き換えを延々と
>行うらしくCPU使用率が100%にはりつきます。

OnPaint()で当該の処理を行う場合に、再入禁止フラグを
用意して、再入しないようにしましょう。
OnSize()で、Invalidate() Update()をしている場合も
当該フラグで重複した処理をしないようにしてみてください。
これでもだめな場合、WM_WINDOWPOSCHANGEDをオーバーライドして、
何も処理しない(基本クラスを呼ばない)にしてみてください。

仲澤@失業者 2009/12/22(火) 17:59:53
おっと、ソースもちょっと変かな。

if( FALSE == m_DontReEntrant ){ // 再入禁止でないなら
    m_DontReEntrant = TRUE;   // 再入を禁止
  GetWindowRect( &tRect );
    GetParent()->ScreenToClient( &tRect ); // ここは親のHWNDで
    this->SetWindowPos( NULL, tRect.left,
                              tRect.top, 
                              tRect.right-tRect.left,
                              tRect.bottom-tRect.top+1, 
                              SWP_NOZORDER );
 
    this->SetWindowPos( NULL, tRect.left,
                              tRect.top, 
                              tRect.right-tRect.left,
                              tRect.bottom-tRect.top, // ここは元に戻す
                              SWP_NOZORDER );
    m_DontReEntrant = FALSE;   // 再入を許可
}

gak 2009/12/22(火) 22:06:26
もしかしてこの現象ってシステムレベルのバグ?

MFCの使用/不使用やSDI/MDIに関係無く
・Window に WS_HSCROLL | WS_VSCROLL が指定
・WindowsXP で テーマが適用されている
で発生してる感じがする。

幾つかのフリーソフトでも発生してたし、エクスプローラでも症状が出てた。
ちなみに、同exeを Vista(x64)環境で実行しても症状は起きなかった。

露見し難いワケでも無く修正されていても良いバグだと思うけど…何で放置されているん
だろう?

仲澤@失業者 2009/12/24(木) 09:33:34
単に、右隅のポツポツした「グリップ」様のものを描画し忘れている
だけなんですけど、この部分は

 1.単にボタン背景色で塗りつぶす
 2.斜線で表現した「グリップ」様にする
 3.ポツポツで表現した「グリップ」様にする

等のバージョンがあり、3.で描画し忘れが発生するようです。
私見ですが、コモンコントロール由来であることが疑われます。

YM555 2009/12/24(木) 16:09:30
仲澤@失業者様 回答ありがとうございます。
ソースを修正してみましたが、現状でも画面書き換えを延々と
行ってしまいます。
再入をしているのかトレースしてみましたが

トレース1
if( FALSE == m_DontReEntrant ){
トレース2
    m_DontReEntrant = TRUE;
  GetWindowRect( &tRect );
    GetParent()->ScreenToClient( &tRect );
    this->SetWindowPos( NULL, tRect.left,
                              tRect.top, 
                              tRect.right-tRect.left,
                              tRect.bottom-tRect.top+1, 
                              SWP_NOZORDER );
 
    this->SetWindowPos( NULL, tRect.left,
                              tRect.top, 
                              tRect.right-tRect.left,
                              tRect.bottom-tRect.top,
                              SWP_NOZORDER );
    m_DontReEntrant = FALSE;
}
トレース3

上記”トレース”の部分でログをファイルに書き出して調べまして
トレース1→トレース2→トレース3の順番で実行されていることを
確認しましたので、再入は起こっていないのかと思われます。
また、WM_WINDOWPOSCHANGEDをオーバーライドして基本クラスの呼び出しを
コメントアウトしましたが効果がありませんでした。
もう少し色々と調べてみます。


ITO様 回答ありがとうございます。
CFormViewはCScrollViewの派生クラスみたいですね。
サンプルScribbleを試したところ、CScrollViewを子ウィンドにしたMDI
アプリケーションではこの異常が発生しないので、CFormViewでも発生
しないかもしれませんね。


gak様 回答ありがとうございます。
>>ちなみに、同exeを Vista(x64)環境で実行しても症状は起きなかった。
興味深いですね。
こちらでもVista(32bit)や7(32bit)でどうなるか後ほど試してみたいと思います。

ロマ 2009/12/30(水) 17:53:08
SDK版のMultiPadでは発生していませんでした(BC5添付のMS謹製のサンプル)。
MDIFrameとMDIChildFrameで
WNDCLASSのhbrBackground=(HBRUSH)(COLOR_WINDOW + 1)を指定し、
CreateWindowのdwStyleにWS_CLIPCHILDRENを含めない
となっていました。

ただし、MDIChildのグリップ部分はイポなしのフラットになる場合があります。
ちらつきも多いです。

憶測ですが、
WM_NCPAINTはグリップ部分の描画を親ウィンドウに任せる場合があるようです。

ロマ 2010/01/03(日) 19:34:36
WM_NCPAINTで出来るようなのですが。

VC6付属のMFC版MultipadでCEditViewを継承して、以下追加。

void CMyEditView::OnNcPaint() 
{
  HDC hdc = ::GetWindowDC(*this);//クリップを無視しています。
  RECT rc;
  ::GetClientRect(*this, &rc);
  rc.left = rc.right; // エッヂ幅を無視しています。少しごみが残ります。
  rc.top = rc.bottom;
  rc.right += GetSystemMetrics(SM_CXVSCROLL);
  rc.bottom += GetSystemMetrics(SM_CYHSCROLL);
  // themeは無視していますので、斜め線のグリップになります
  DrawFrameControl(hdc, &rc, DFC_SCROLL, DFCS_SCROLLSIZEGRIP);
  
  CEditView::OnNcPaint();
}

YM555 2010/01/05(火) 14:37:58
ロマさま 返信ありがとうございます。
Multipadサンプルを探すことができませんでした。
VC6のサンプルのどのあたりにあるのでしょうか?
教えていただけませんでしょうか。

色々と試してみたのですが、まずWS_HSCROLL | WS_VSCROLL を
指定したMDI子ウィンドでもWindows2000やWindows7(32bit)で
試したところ、この現象は起きませんでした。

また、スクロールバーをOnUpdate()で
 CView::ShowScrollBar( SB_BOTH );
を呼び出して表示させた場合はこの現象は起きません。
ただし、CView::SetScrollInfo()でパラメータを
設定すると、現象が再び発生します。

CScrollViewクラスのサンプルScribbleではこの現象が発生しないため
CScrollViewがどのようにしてスクロールバーを表示しているのか
知りたいと考えてソースを追いかけていますが、どなたか情報をお持ちの
かたがおりましたら教えてください。

ロマ 2010/01/05(火) 20:39:30
MSDNライブラリMicrosoft Visual Studio 6.0の検索からMultipadは探せると思うんです
が。
cdのsamples\vc98\mfc\general\multipadです。

> また、スクロールバーをOnUpdate()で
>  CView::ShowScrollBar( SB_BOTH );
> を呼び出して表示させた場合はこの現象は起きません。

これはおもしろいですね。

> CScrollViewクラスのサンプルScribbleではこの現象が発生しないため

Scribble step7のスクロールバーとグリップハンドルはコントロールです。
Spy++で確認願います。

ロマ 2010/01/06(水) 22:10:05
私の調査結果を報告します。

1)誰が非クライアント領域を描画するのか
 WM_NCPAINT以外にもWM_NCACTIVATEが描く。
 タイトル部分は、WM_SETTEXTも。

2)GetDCEx
 WM_NCPAINT内では、GetWindowDC()の方が簡単。
 GetDCExを使う場合は謎のフラグが必要らしい(DCX_USESTYLE?)。

3)リージョン
 WM_NCPAINTの(wParam > 1)の場合、wParamはクリップリージョン。
 このリージョンはスクリーン座標系、GetWindowDC()はウィンドウ座標系。

4)クラシックスタイル
 * manifest付きでも、ShowScrollBar()でスクロールバーを表示すると
   クラシックスタイルになる。
  * WM_NCACTIVATE内でSendMessage(WM_NCPAINT)すると、クラシックスタイルになる。
 この2点は謎です。 

参考) C++ Q & A -- Microsoft Systems Journal January 1997

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