调整窗口大小时是不是可以完全消除闪烁?

Posted

技术标签:

【中文标题】调整窗口大小时是不是可以完全消除闪烁?【英文标题】:Is it possible to eliminate flickering entirely when resizing a window?调整窗口大小时是否可以完全消除闪烁? 【发布时间】:2012-08-21 06:22:11 【问题描述】:

通常,即使使用双缓冲,在调整窗口大小时,似乎也不可避免地会发生闪烁。

第 1 步,原始窗口。

第 2 步,调整窗口大小,但未绘制额外区域。

第 3 步,调整窗口大小,绘制额外区域。

是否有可能以某种方式隐藏 setp 2?我可以在绘画动作完成之前暂停调整大小的过程吗?

这是一个例子:

#include <Windows.h>
#include <windowsx.h>
#include <Uxtheme.h>

#pragma comment(lib, "Uxtheme.lib")

LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
BOOL MainWindow_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct);
void MainWindow_OnDestroy(HWND hWnd);
void MainWindow_OnSize(HWND hWnd, UINT state, int cx, int cy);
void MainWindow_OnPaint(HWND hWnd);

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)

  WNDCLASSEX wcex =  0 ;
  HWND hWnd;
  MSG msg;
  BOOL ret;

  wcex.cbSize = sizeof(wcex);
  wcex.lpfnWndProc = WindowProc;
  wcex.hInstance = hInstance;
  wcex.hIcon = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
  wcex.hCursor = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
  wcex.lpszClassName = TEXT("MainWindow");
  wcex.hIconSm = wcex.hIcon;

  if (!RegisterClassEx(&wcex))
  
    return 1;
  

  hWnd = CreateWindow(wcex.lpszClassName, TEXT("CWin32"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_DESKTOP, NULL, hInstance, NULL);
  if (!hWnd)
  
    return 1;
  

  ShowWindow(hWnd, nCmdShow);
  UpdateWindow(hWnd);

  while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
  
    if (ret == -1)
    
      return 1;
    
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  

  return msg.wParam;


LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

  switch (uMsg)
  
    HANDLE_MSG(hWnd, WM_CREATE, MainWindow_OnCreate);
    HANDLE_MSG(hWnd, WM_DESTROY, MainWindow_OnDestroy);
    HANDLE_MSG(hWnd, WM_SIZE, MainWindow_OnSize);
    HANDLE_MSG(hWnd, WM_PAINT, MainWindow_OnPaint);
  default:
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
  


BOOL MainWindow_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

  BufferedPaintInit();
  return TRUE;


void MainWindow_OnDestroy(HWND hWnd)

  BufferedPaintUnInit();
  PostQuitMessage(0);


void MainWindow_OnSize(HWND hWnd, UINT state, int cx, int cy)

  InvalidateRect(hWnd, NULL, FALSE);


void MainWindow_OnPaint(HWND hWnd)

  PAINTSTRUCT ps;
  HPAINTBUFFER hpb;
  HDC hdc;

  BeginPaint(hWnd, &ps);
  hpb = BeginBufferedPaint(ps.hdc, &ps.rcPaint, BPBF_COMPATIBLEBITMAP, NULL, &hdc);

  FillRect(hdc, &ps.rcPaint, GetStockBrush(DKGRAY_BRUSH));
  Sleep(320); // This simulates some slow drawing actions.

  EndBufferedPaint(hpb, TRUE);
  EndPaint(hWnd, &ps);

是否可以消除闪烁?

【问题讨论】:

移除睡眠就可以了!当您花费这么长时间来绘制窗口时,很难看到系统还能做什么。 我添加它只是为了更清楚地显示闪烁的内容。我想要一个解决方案,即使绘图动作需要 320 毫秒并且仍然没有闪烁,只是不显示第二步。 I've already set the hbrBackground to NULL,这是一个错误。 @HansPassant “那是个错误”是什么意思? 它会导致您描述的那种闪烁。将其设置为与所需背景颜色匹配的画笔,以便 WM_ERASEBKGND 为您提供快速的矩形填充。 【参考方案1】:

在拖动操作期间更新窗口时,操作系统必须在扩展窗口区域中显示 something。如果您无法提供任何内容,那么它将显示背景,直到您提供为止。由于您没有指定任何背景,因此您会变黑。当然,您应该指定背景画笔?只需将以下内容添加到您的代码中,就会使行为更加可口:

wcex.hbrBackground = GetStockBrush(DKGRAY_BRUSH);

但是,如果您需要 320 毫秒的时间来响应 WM_PAINT,那么您会破坏用户调整大小的 UI。它变得生涩和反应迟钝。该系统的设计假设是您可以足够快地绘制窗口以使拖动感觉平滑。解决问题的正确方法是让WM_PAINT 在合理的时间内运行。

如果你真的无法实现足够快的绘制以实现平滑拖动,那么我建议几个替代方案:

    在拖动过程中禁用窗口更新。我确信这可以针对单个窗口完成,但我不记得如何做到这一点。 在调整大小/拖动处于活动状态时绘制假的东西,并推迟真正的绘画直到调整大小/拖动完成。收听WM_ENTERSIZEMOVEWM_EXITSIZEMOVE 是这方面的关键。此 Microsoft 示例程序说明了如何执行此操作:https://github.com/microsoft/Windows-classic-samples/blob/master/Samples/Win7Samples/winui/fulldrag/

【讨论】:

DWM 中有什么东西可以用来控制合成过程吗?我的意思是,窗口通知 DWM,“停止组合窗口,直到绘制动作完成,然后将完全绘制的窗口组合到屏幕上。” 好的,但是您想在扩展的客户区域中绘制什么?必须画一些东西。 没什么。窗口已展开,是的,但我不想立即将其显示给用户。我想在画完后展示它。 大+1。而且,对于任何 WM_PAINT 来说,320 毫秒都是不可接受的。这是一个真正的问题。如果是我,我会异步合成图像,只在 WM_PAINT 上渲染一个预先合成的位图。在图像合成过程中,系统会免费为您提供背景画笔,然后您可以将之前合成的图像显示为StretchBltd 图像,只是为了在这 320 毫秒内给用户一些东西。取决于您的窗口内容,这是一个好主意还是一个糟糕的主意。 我同意@tenfour。这确实是这里的根本问题。我的应用程序有一些 D3D MDI 子窗口。当他们调整大小时,重新初始化 D3D 上下文需要时间。拉伸绘制最近绘制的内容是我们使用的解决方案。然后在调整大小完成后绘制正确的内容。【参考方案2】:

使用WM_SIZING 而不是WM_SIZE,不要忘记WM_ERASEBKGND

【讨论】:

我已经把hbrBackground设置成了NULL,根据***.com/questions/12073721/…,WM_EREASEBACKGROUND就不用处理了。 为什么处理WM_SIZING 会有所作为? @David Heffernan WM_SIZE - 在窗口大小改变后发送到窗口。 WM_SIZING - 发送到用户正在调整大小的窗口。通过处理此消息,应用程序可以监视拖动矩形的大小和位置,并在需要时更改其大小或位置。 WM_SIZE 在拖动过程中也会发送。对不起。 WM_SIZING 旨在允许您限制拖动操作。例如,应用固定宽度、可变高度约束。【参考方案3】:

如果您进入系统属性控制面板小程序,选择高级标签,然后点击设置...在Performance 组框,您会看到一个名为拖动时显示窗口内容的复选框设置。如果您取消选中该选项并尝试调整窗口大小,您将看到只有窗口框架移动,直到您完成拖动操作,然后窗口仅以新大小重新绘制一次。当我们的计算机运行缓慢、笨拙时,这就是过去调整窗口大小的方式。

现在我们真的不想全局更改设置(您可以通过使用 SPI_SETDRAGFULLWINDOWS 调用 SystemParametersInfo 来完成,但不要真的这样做,因为您的用户不会喜欢它)。

当用户抓住调整大小边框时,线程进入了由窗口管理器控制的模态循环。您的窗口将在该循环开始时获得WM_ENTERSIZEMOVE,在操作完成时获得WM_EXITSIZEMOVE。在某些时候,您还会得到一个 WM_GETMINMAXINFO,它可能与您需要做的事情无关。当用户拖动大小调整框时,您还将快速收到WM_SIZING、WM_SIZE 消息(快速的 WM_SIZE 通常会导致WM_PAINTs)。

全局拖动时显示窗口内容设置负责快速获取 WM_SIZE 消息。如果该设置关闭,您将在一切结束时收到一条 WM_SIZE 消息。

如果您的窗口很复杂,您可能在 WM_SIZE 处理程序中有布局代码计算内容(可能还有移动子窗口),在 WM_PAINT 处理程序中有很多绘画代码。如果所有代码都太慢(正如您的示例 320 毫秒延迟所暗示的那样),那么您将获得闪烁、生涩的体验。

我们真的不想更改全局设置,但它确实激发了您的问题的解决方案:

在调整大小操作期间进行更简单的绘制,然后在操作结束时只进行一次(较慢的)复杂绘制。

解决方案:

    看到 WM_ENTERSIZEMOVE 时设置一个标志。 更改您的 WM_SIZE 处理程序以检查标志,如果已设置则不执行任何操作。 更改您的 WM_PAINT 处理程序以检查标志,并在已设置的情况下以纯色简单、快速地填充窗口。 看到 WM_EXITSIZEMOVE 时清除标志,然后触发布局代码并使窗口无效,以便根据最终大小更新所有内容。

如果您的慢速窗口是子窗口而不是应用程序的***窗口,则您必须在***窗口获得 WM_ENTERSIZEMOVE 和 WM_EXITSIZEMOVE 时向子窗口发出信号,以实现步骤 1 和 4。

【讨论】:

【参考方案4】:

是的,您可以完全删除闪烁 :)

您可以在一个线程中处理所有窗口消息,并在另一个线程中绘制其上下文。您的窗口始终保持响应。效果很好,不明白为什么这不是最佳实践。

例如,如果您绑定 Direct3D 上下文,它可以在调整大小时立即缩放,完全无需更新上下文!

我的代码如下所示:

int WINAPI wWinMain( HINSTANCE a_hInstance, HINSTANCE a_hPrevInstance, LPWSTR a_lpCmdLine, int a_nCmdShow )

    Win32WindowRunnable* runnableWindow=new Win32WindowRunnable(a_hInstance, a_nCmdShow);
    IThread* threadWindow=new Win32Thread(runnableWindow);
    threadWindow->start();

    Scene1* scene=new Scene1(runnableWindow->waitForWindowHandle());
    IThread* threadRender=new Win32Thread(scene);
    threadRender->start();

    threadWindow->join();
    threadRender->pause();
    threadRender->kill();

    delete runnableWindow;
    return 0;

此处的完整源代码示例: https://github.com/TheWhiteAmbit/TheWhiteAmbit/blob/master/Win32App/Win32Main.cpp

【讨论】:

以上是关于调整窗口大小时是不是可以完全消除闪烁?的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL 闪烁/损坏,窗口调整大小和 DWM 处于活动状态

如何消除TPaintBox右边缘的闪烁(例如调整大小时)

如何在不闪烁的情况下调整 Swing JWindow 的大小?

为啥添加约束会消除调整 NSWindow 大小的能力?

在winform画了一个自定义控件,现在我要在运行后进行拉伸动态改变大小,但快速拉伸就闪烁,怎么消除

React-virtualized - 是不是可以在窗口调整大小时更新 rowHeights?