调整窗口大小时是不是可以完全消除闪烁?
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_ENTERSIZEMOVE
和WM_EXITSIZEMOVE
是这方面的关键。此 Microsoft 示例程序说明了如何执行此操作:https://github.com/microsoft/Windows-classic-samples/blob/master/Samples/Win7Samples/winui/fulldrag/
【讨论】:
DWM 中有什么东西可以用来控制合成过程吗?我的意思是,窗口通知 DWM,“停止组合窗口,直到绘制动作完成,然后将完全绘制的窗口组合到屏幕上。” 好的,但是您想在扩展的客户区域中绘制什么?必须画一些东西。 没什么。窗口已展开,是的,但我不想立即将其显示给用户。我想在画完后展示它。 大+1。而且,对于任何 WM_PAINT 来说,320 毫秒都是不可接受的。这是一个真正的问题。如果是我,我会异步合成图像,只在 WM_PAINT 上渲染一个预先合成的位图。在图像合成过程中,系统会免费为您提供背景画笔,然后您可以将之前合成的图像显示为StretchBlt
d 图像,只是为了在这 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 处于活动状态
如何在不闪烁的情况下调整 Swing JWindow 的大小?