33

ユーザーがサイズ変更可能なウィンドウの隅をつかんで移動すると、ウィンドウは最初にウィンドウの内容を移動し、次にサイズ変更されているウィンドウにWM_SIZEを発行します。

したがって、さまざまな子コントロールの動きを制御したい、ちらつきをなくしたいダイアログでは、ユーザーは最初に、ウィンドウがどのように見えるかをOSがどのように考えるかを確認します(AFAICT、OSは移動にbitbltアプローチを使用しているため) WM_SIZEを送信する前にウィンドウ内にあるもの)-その後、ダイアログが子コントロールの移動やサイズ変更などを処理できるようになります。その後、強制的に再描画する必要があります。これにより、ちらつきが発生します(非常に少しでも)。

私の主な質問は次のとおりです。ウィンドウにこの愚かなbitbltのことを行わないように強制する方法はありますか? ウィンドウのサイズが変更されると移動するコントロール、または親のサイズが変更されるとサイズが変更されるコントロールを備えたウィンドウの場合は、間違いなく間違いになります。いずれにせよ、OSにプレペイントを行わせることは、作品をねじ込むだけです。

しばらくの間、CS_HREDRAWおよびCSVREDRAWクラスフラグに関連しているのではないかと思いました。ただし、実際には、OSからウィンドウの消去を要求されたくないのです。OSが最初にウィンドウの内容を変更せずに、自分で再描画を実行したいだけです(つまり、表示を元の状態にしたいと思います)。ユーザーがサイズ変更を開始する前-OSからのビットブリットなし)。また、OSがすべてのコントロールに再描画する必要があることを通知したくありません(サイズ変更によって実際に隠されたり表示されたりしたものでない限り)。

私が本当に欲しいもの:

  1. 画面上で何かが更新される前に、子コントロールを移動およびサイズ変更します。
  2. 移動またはサイズ変更されたすべての子コントロールを完全に描画して、新しいサイズと場所にアーティファクトなしで表示されるようにします。
  3. 子コントロール自体に影響を与えることなく、子コントロールの間にスペースを描画します。

注:手順2と3は逆にすることができます。

上記の3つのことは、DeferSetWindowPos()をWS_CLIPCHILDRENとしてマークされたダイアログリソースと組み合わせて使用​​すると正しく発生するように見えます。

上記をメモリDCに対して実行し、WM_SIZEハンドラーの最後で1つのbitbltのみを実行できれば、さらに小さなメリットが得られます。

私はこれでしばらく遊んでいますが、2つのことから逃れることはできません。

  1. 私はまだWindowsが「予測bitblt」を実行するのを抑制することができません。 回答:この動作を無効にするためにWM_NCCALCSIZEをオーバーライドするソリューションについては、以下を参照してください。

  2. 子コントロールがダブルバッファに描画するダイアログを作成する方法がわかりません。回答:ダイアログをダブルバッファリングするようにWindows OSに要求する方法については、以下のJohnの回答(回答としてマーク)を参照してください(注:ドキュメントによると、これにより、ペイント操作間のGetDC()は許可されません)。


私の最終解決策(特にジョンK.に貢献してくれたすべての人に感謝します):

多くの汗と涙の後、私は次のテクニックがAeroとXPの両方で、またはAeroが無効になっている場合でも問題なく機能することを発見しました。フリックは存在しません(1)。

  1. ダイアログプロシージャをフックします。
  2. WM_NCCALCSIZEをオーバーライドして、Windowsにクライアント領域全体を検証させ、bitbltは行わないようにします。
  3. WM_SIZEをオーバーライドして、表示されているすべてのウィンドウに対してBeginDeferWindowPos / DeferWindowPos/EndDeferWindowPosを使用してすべての移動とサイズ変更を行います。
  4. ダイアログウィンドウがWS_CLIPCHILDRENスタイルであることを確認します。
  5. CS_HREDRAW | CS_VREDRAWは使用しないでください(ダイアログは使用しないため、通常は問題になりません)。

レイアウトコードはあなた次第です-レイアウトマネージャーのCodeGuruまたはCodeProjectで例を見つけたり、独自の例を作成したりするのは簡単です。

以下に、ほとんどの方法で使用できるコードの抜粋を示します。

LRESULT ResizeManager::WinProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    switch (msg)
    {
    case WM_ENTERSIZEMOVE:
        m_bResizeOrMove = true;
        break;

    case WM_NCCALCSIZE:
        // The WM_NCCALCSIZE idea was given to me by John Knoeller: 
        // see: http://stackoverflow.com/questions/2165759/how-do-i-force-windows-not-to-redraw-anything-in-my-dialog-when-the-user-is-resiz
        // 
        // The default implementation is to simply return zero (0).
        //
        // The MSDN docs indicate that this causes Windows to automatically move all of the child controls to follow the client's origin
        // and experience shows that it bitblts the window's contents before we get a WM_SIZE.
        // Hence, our child controls have been moved, everything has been painted at its new position, then we get a WM_SIZE.
        //
        // Instead, we calculate the correct client rect for our new size or position, and simply tell windows to preserve this (don't repaint it)
        // and then we execute a new layout of our child controls during the WM_SIZE handler, using DeferWindowPos to ensure that everything
        // is moved, sized, and drawn in one go, minimizing any potential flicker (it has to be drawn once, over the top at its new layout, at a minimum).
        //
        // It is important to note that we must move all controls.  We short-circuit the normal Windows logic that moves our child controls for us.
        //
        // Other notes:
        //  Simply zeroing out the source and destination client rectangles (rgrc[1] and rgrc[2]) simply causes Windows 
        //  to invalidate the entire client area, exacerbating the flicker problem.
        //
        //  If we return anything but zero (0), we absolutely must have set up rgrc[0] to be the correct client rect for the new size / location
        //  otherwise Windows sees our client rect as being equal to our proposed window rect, and from that point forward we're missing our non-client frame

        // only override this if we're handling a resize or move (I am currently unaware of how to distinguish between them)
        // though it may be adequate to test for wparam != 0, as we are
        if (bool bCalcValidRects = wparam && m_bResizeOrMove)
        {
            NCCALCSIZE_PARAMS * nccs_params = (NCCALCSIZE_PARAMS *)lparam;

            // ask the base implementation to compute the client coordinates from the window coordinates (destination rect)
            m_ResizeHook.BaseProc(hwnd, msg, FALSE, (LPARAM)&nccs_params->rgrc[0]);

            // make the source & target the same (don't bitblt anything)
            // NOTE: we need the target to be the entire new client rectangle, because we want windows to perceive it as being valid (not in need of painting)
            nccs_params->rgrc[1] = nccs_params->rgrc[2];

            // we need to ensure that we tell windows to preserve the client area we specified
            // if I read the docs correctly, then no bitblt should occur (at the very least, its a benign bitblt since it is from/to the same place)
            return WVR_ALIGNLEFT|WVR_ALIGNTOP;
        }
        break;

    case WM_SIZE:
        ASSERT(m_bResizeOrMove);
        Resize(hwnd, LOWORD(lparam), HIWORD(lparam));
        break;

    case WM_EXITSIZEMOVE:
        m_bResizeOrMove = false;
        break;
    }

    return m_ResizeHook.BaseProc(hwnd, msg, wparam, lparam);
}

サイズ変更は、実際には次のようにResize()メンバーによって行われます。

// execute the resizing of all controls
void ResizeManager::Resize(HWND hwnd, long cx, long cy)
{
    // defer the moves & resizes for all visible controls
    HDWP hdwp = BeginDeferWindowPos(m_resizables.size());
    ASSERT(hdwp);

    // reposition everything without doing any drawing!
    for (ResizeAgentVector::const_iterator it = m_resizables.begin(), end = m_resizables.end(); it != end; ++it)
        VERIFY(hdwp == it->Reposition(hdwp, cx, cy));

    // now, do all of the moves & resizes at once
    VERIFY(EndDeferWindowPos(hdwp));
}

そして、おそらく最後のトリッキーなビットは、ResizeAgentのReposition()ハンドラーで見ることができます。

HDWP ResizeManager::ResizeAgent::Reposition(HDWP hdwp, long cx, long cy) const
{
    // can't very well move things that no longer exist
    if (!IsWindow(hwndControl))
        return hdwp;

    // calculate our new rect
    const long left   = IsFloatLeft()   ? cx - offset.left    : offset.left;
    const long right  = IsFloatRight()  ? cx - offset.right   : offset.right;
    const long top    = IsFloatTop()    ? cy - offset.top     : offset.top;
    const long bottom = IsFloatBottom() ? cy - offset.bottom  : offset.bottom;

    // compute height & width
    const long width = right - left;
    const long height = bottom - top;

    // we can defer it only if it is visible
    if (IsWindowVisible(hwndControl))
        return ::DeferWindowPos(hdwp, hwndControl, NULL, left, top, width, height, SWP_NOZORDER|SWP_NOACTIVATE);

    // do it immediately for an invisible window
    MoveWindow(hwndControl, left, top, width, height, FALSE);

    // indicate that the defer operation should still be valid
    return hdwp;
}

「トリッキー」とは、破壊されたウィンドウをいじるのを避け、表示されていないウィンドウに対してSetWindowPosを延期しようとしないことです(これは「失敗する」と文書化されているため)。

私は、いくつかのコントロールを非表示にし、かなり複雑なレイアウトを使用して優れた成功を収めている実際のプロジェクトで上記をテストしました。ダイアログウィンドウの左上隅を使用してサイズを変更した場合でも、Aeroがなくてもちらつきはありません(1)(IE、FireFoxなどのハンドルをつかむと、ほとんどのサイズ変更可能なウィンドウで最もちらつきと問題が発生します)。

十分な関心があれば、CodeProject.comまたは同様の場所の実際の実装例を使用して調査結果を編集するように説得することができます。私にメッセージを送ってください。

(1)かつてそこにあったものの上に、1回のドローを避けることは不可能であることに注意してください。変更されていないダイアログのすべての部分について、ユーザーには何も表示されません(ちらつきはまったくありません)。しかし、状況が変わった場合、ユーザーに見える変化があります。これは避けることは不可能であり、100%の解決策です。

4

8 に答える 8

16

サイズ変更中のペイントを防ぐことはできませんが、ちらつきの原因となる再ペイントを(注意して)防ぐことができます。まず、bitbltです。

bitbltを停止する方法は2つあります。

トップレベルウィンドウのクラスを所有している場合は、それをCS_HREDRAW | CS_VREDRAWスタイルに登録するだけです。これにより、ウィンドウのサイズを変更すると、どのビットが変更されないかを推測してビットブリングするのではなく、クライアント領域全体が無効になります。

クラスを所有していないが、メッセージ処理を制御する機能がある場合(ほとんどのダイアログボックスに当てはまります)。のデフォルトの処理WM_NCCALCSIZEは、クラスがスタイルCS_HREDRAWを設定して処理される場所です。デフォルトの動作は、クラスにが含まれている場合に処理CS_VREDRAWから戻ることです。 WVR_HREDRAW | WVR_VREDRAWWM_NCCALCSIZECS_HREDRAW | CS_VREDRAW

したがって、インターセプトできる場合は、他の通常の処理を実行するようWM_NCCALCSIZEに呼び出した後、これらの値を強制的に返すことができます。DefWindowProc

ウィンドウのサイズ変更がいつ開始および停止するかを聞いて知ることができWM_ENTERSIZEMOVEWM_EXITSIZEMOVEそれを使用して、描画やレイアウトコードの動作を一時的に無効にしたり変更したりして、点滅を最小限に抑えることができます。このコードを変更するために正確に何をしたいかは、通常のコードが通常何をするかによって異なりWM_SIZE WM_PAINTますWM_ERASEBKGND

ダイアログボックスの背景をペイントするときは、子ウィンドウの背後にペイントする必要はありません。ダイアログWS_CLIPCHILDRENでこれが解決されていることを確認してください。これで、これはすでに処理されています。

子ウィンドウを移動するときは、必ずBeginDeferWindowPos/を使用EndDefwindowPosして、すべての再描画が一度に行われるようにしてください。そうしないと、各ウィンドウが各呼び出しで非クライアント領域を再描画するときに、大量の点滅が発生しSetWindowPosます。

于 2010-01-29T23:27:16.123 に答える
4

私がその質問を正しく理解していれば、それはまさにレイモンドが今日取り組んだ質問です。

于 2010-01-30T00:58:49.760 に答える
2

これが2018年のアップデートです。あなたとまったく同じガントレットを実行したばかりだからです。

質問の「最終的な解決策」とそれに関連する回答は、Windows XP / Vista / 7でのトリックに言及し、サイズ変更中にクライアント領域を混乱させることを防ぐのWM_NCCALCSIZEに役立ちます。同様のトリックに言及することも役立つかもしれません:インターセプト(最初にそれをに渡す)と設定を行うことができます。これにより、ウィンドウのサイズ変更中にWindowsが行う内部呼び出しの内部が無効になります。これは、をスキップするのと同じ最終的な効果があります。CS_HREDRAW|CS_VREDRAWBitBltWM_WINDOWPOSCHANGINGDefWindowProcWINDOWPOS.flags |= SWP_NOCOPYBITSBitBltSetWindowPos()BitBlt

そして、一部の人々は、あなたのWM_NCCALCSIZEトリックがWindows 10では機能しなくなったと述べました。これは、作成した2つの長方形(および)をWindowsで使用するために、作成したコードが返さWVR_ALIGNLEFT|WVR_ALIGNTOPれるはずのときに返されるためかもしれません。およびのMSDNページの非常に露出度の高いdoxに。Windows10がその戻り値についてより厳密である可能性があります。やってみます。WVR_VALIDRECTSnccs_params->rgrc[1]nccs_params->rgrc[2]WM_NCCALCSIZENCCALCSIZE_PARAMS

BitBltただし、Windows 10を内部で実行しないように説得できると仮定してもSetWindowPos()、新しい問題があることがわかります...

Windows 10(および場合によってはWindows 8)は、XP / Vista / 7の古いレガシー痴漢の上に、クライアントエリアの性的虐待の 別のレイヤーを追加します。

Windows 10では、アプリはフレームバッファーに直接描画しませんが、代わりにAero Window Manager(DWM.exe)が合成するオフスクリーンバッファーに描画します。

DWMは、クライアント領域に独自のコンテンツを描画することで「支援」することを決定する場合があります(一種ですが、BitBltさらにひねくれた、さらには制御不能です)。

したがって、クライアントエリアの性的虐待から解放されるためには、引き続きWM_NCCALCSIZE制御下に置く必要がありますが、DWMがピクセルを混乱させないようにする必要もあります。

私はまったく同じ問題と戦っていて、このトピックに関する10年間の投稿をまとめ、いくつかの新しい洞察を提供するまとめの質問/回答を作成しました(この質問にコンテンツを貼り付けるには長すぎます)。Windows Vista以降、上記のBitBltだけが問題ではなくなりました。楽しみ:

ウィンドウのサイズを変更するとき、特に左/上境界線をドラッグするときに、醜いジッター/フリッカー/ジャンプをスムーズにするにはどうすればよいですか(Win 7-10; bg、bitblt、DWM)?

于 2018-10-26T08:12:58.887 に答える
1

一部のコントロールでは、WM_PRINTメッセージを使用して、コントロールをDCに描画させることができます。しかし、それは実際にはあなたの主な問題を解決しません。それは、サイズ変更中にWindowsに何も描画させないようにしたいということですが、すべてを実行できるようにします。

そして答えは、子ウィンドウがある限り、やりたいことができないということです。

最終的に自分のコードでこれを解決する方法は、ウィンドウレスコントロールの使用に切り替えることです。独自のウィンドウがないため、常に親ウィンドウと同時に(同じDCに)描画します。これにより、単純なダブルバッファリングを使用して、ちらつきを完全に取り除くことができます。親の描画ルーチン内で描画ルーチンを呼び出さないだけで、必要なときに子供たちの描画を簡単に抑制することもできます。

これは、サイズ変更操作中のちらつきや裂け目を完全に取り除くために私が知っている唯一の方法です。

于 2010-02-02T22:21:21.907 に答える
0

プラグを差し込む場所が見つかった場合はCWnd::LockWindowUpdates()、更新のロックを解除するまで描画が発生しないようにします。

ただし、これはハックであり、かなり醜いものであることに注意してください。サイズ変更中、ウィンドウはひどく見えます。サイズ変更中のちらつきが問題である場合は、ペイントをブロックしてちらつきを隠すのではなく、ちらつきを診断するのが最善の方法です。

探すべきことの1つは、サイズ変更中に頻繁に呼び出される再描画コマンドです。ウィンドウのコントロールが指定されRedrawWindow()RDW_UPDATENOWフラグで呼び出している場合、ウィンドウはその場で再描画されます。ただし、そのフラグを削除してRDW_INVALIDATE代わりに指定することもできます。これにより、再描画せずにウィンドウを無効にするようにコントロールに指示します。アイドル時に塗り直し、飛び散ることなくディスプレイを新鮮に保ちます。

于 2010-01-29T23:01:21.387 に答える
0

さまざまなアプローチがありますが、一般的に使用できるのはダブルバッファリングだけであることがわかりました。オフスクリーンバッファに描画してから、バッファ全体をスクリーンにブリットします。

これはVistaAero以降では無料で提供されるため、痛みは短命である可能性があります。

XPでのウィンドウとシステムコントロールの一般的なダブルバッファリングの実装については知りませんが、次のことを検討してください。

キースルールのCMemDCは、GDI
WS_EX_COMPOSITEDウィンドウスタイルで自分で描いたものをすべてダブルバッファリングします(備考のセクションと、stackoverflowに関するここを参照してください) 。

于 2010-01-29T23:17:30.407 に答える
0

再描画の問題を効果的に診断する唯一の方法は、リモートデバッグです。

2台目のPCを入手します。その上にMSVSMONをインストールします。ビルド製品をリモートPCにコピーするビルド後のステップまたはユーティリティプロジェクトを追加します。

これで、WM_PAINTハンドラー、WM_SIZEハンドラーなどにブレークポイントを配置し、サイズ変更と再描画を実行するときにダイアログコードを実際にトレースできるようになります。MSシンボルサーバーからシンボルをダウンロードすると、完全なコールスタックを確認できます。

いくつかの適切に配置されたブレークポイント-WM_PAINT、WM_ERAGEBKGNDハンドラーで、WM_SIZEサイクルの早い段階でウィンドウが同期的に再描画される理由をよく理解しておく必要があります。

システムには、階層化された子コントロールを備えた親ウィンドウで構成されるウィンドウがたくさんあります。エクスプローラーウィンドウは、リストビュー、ツリービュープレビューパネルなどで非常に複雑です。エクスプローラーは、サイズ変更時にちらつきの問題がないため、取得することは非常に可能です。親ウィンドウのちらつきのないサイズ変更:-あなたがする必要があるのは、塗り直しをキャッチし、それらの原因を突き止め、そして、まあ、原因が取り除かれていることを確認することです。

于 2010-01-30T11:34:05.113 に答える
0

動作するように見えるもの:

  1. 親ダイアログでWS_CLIPCHILDRENを使用します(WM_INITDIALOGで設定できます)
  2. WM_SIZE中に、DeferSetWindowPos()を使用して、子コントロールの移動とサイズ変更をループします。

Aeroを使用したWindows7でのテストでは、これはほぼ完璧に近いものです。

于 2010-02-01T20:07:31.177 に答える