在Windows中平滑调整窗口大小(使用Direct2D 1.1)

Smooth window resizing in Windows (using Direct2D 1.1)?

本文关键字:使用 Direct2D 窗口大小 Windows 平滑 调整      更新时间:2023-10-16

在windows中调整窗口大小并不像我希望的那样"平滑",这让我很恼火(windows程序通常都是这样,而不仅仅是我自己的程序。Visual Studio就是一个很好的例子)。它让操作系统及其程序感觉"脆弱"answers"廉价"(是的,我关心程序和用户界面的感觉,就像我关心关车门的声音和感觉一样。这反映了构建质量),在我看来,这会影响整体用户体验,并最终影响品牌的感知。

在调整大小的过程中,窗口内容的重新绘制根本跟不上鼠标的移动。每当我调整窗口大小时,都会出现"断断续续"/"闪烁"的效果,这似乎是由于在绘制新的、调整大小的内容之前,在新的、重新调整大小的窗口框架中重新绘制窗口的先前大小内容。

我正在构建一个使用Direct2D 1.1绘制UI的Win32应用程序(x64),考虑到Direct2D的速度,我认为在2014年的操作系统中应该没有必要出现这样的瑕疵。我自己使用的是Windows 8.1,但目标是使用此应用程序的Windows 7及更高版本。

当最大化一个小窗口时,"以前的大小"效果尤其明显(因为窗口大小的差异足够大,可以很容易地对比旧内容的图像,因为旧内容在大窗口的左上角短暂闪烁,新内容随后被绘制在上面)。

这似乎是正在发生的事情:

  1. (假设屏幕上有一个完全渲染的窗口,大小为500 x 500像素)
  2. 我最大化窗口:
  3. 窗框最大化
  4. 在..之前,旧的500 x 500内容绘制在新的框架中
  5. 。。使用适当大小的内容重新绘制最大化的窗口

我想知道是否有任何方法可以缓解这种情况(即取消步骤4)-例如,通过拦截Windows消息-并避免在最终重新呈现新内容之前,用旧内容将窗口重新绘制为新大小。这就像Windows在麻烦我用WM_PAINT消息或类似消息提供更新内容之前,使用现有的任何图形重新绘制窗口一样。

能做到吗?

编辑:似乎WM_WINDOWPOSCHANGING/WM_SIZING提供了对新大小数据的"早期访问",但我仍然无法抑制对旧内容的绘制

我的WndProc看起来像这样:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_ERASEBKGND:
return 1;
case WM_PAINT:
PAINTSTRUCT ps;
BeginPaint(hWnd, &ps);
D2DRender();
EndPaint(hWnd, &ps);
return 0;
case WM_SIZE:
if (DeviceContext && wParam != SIZE_MINIMIZED)
{
D2DResizeTargetBitmap();
D2DRender();
}
return 0;
case WM_DISPLAYCHANGE:
D2DRender();
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}

该窗口未设置CS_HREDRAWCS_VREDRAW。交换链是双缓冲的,Present调用是在SyncInterval=0的情况下进行的。

我知道,每次窗口大小更改时重新创建交换链缓冲区确实会产生一些开销,而不是在静态窗口表面上简单地重新绘制。然而,"卡顿"并不是由此引起的,因为即使缓冲区大小调整被禁用,并且在窗口大小调整期间,现有窗口内容也会被简单地缩放(尽管确实使其更好地跟上鼠标移动),也会发生这种情况。

有一种方法可以防止上面步骤4中提到的不必要的BitBlt。

在Windows 8之前,可以通过创建自己的WM_NCCALCSIZE自定义实现来告诉Windows不进行任何闪电战(或在其顶部闪电战一个像素),也可以拦截WM_WINDOWPOSCHANGING(首先将其传递到DefWindowProc上)并设置WINDOWPOS.flags |= SWP_NOCOPYBITS,这将禁用Windows在窗口调整期间对SetWindowPos()进行的内部调用中的BitBlt。这具有跳过BitBlt的相同最终效果。

然而,没有什么能这么简单。随着Windows 8/10 Aero的出现,应用程序现在进入屏幕外缓冲区,然后由新的、邪恶的DWM.exe窗口管理器合成。事实证明,DWM.exe有时会在传统XP/Vista/7代码已经完成的操作的基础上进行自己的BitBlt类型的操作。阻止DWM进行闪电战要困难得多;到目前为止,我还没有看到任何完整的解决方案。

所以你需要通过这两层。有关将突破XP/Vista/7层并至少提高8/10层性能的示例代码,请参阅:

如何在调整窗口大小时平滑丑陋的抖动/闪烁/跳跃,尤其是拖动左/上边界(Win7-10;bg、bitblt和DWM)?

如果你有一个固定大小(与全屏分辨率相同)的无边界子窗口(只在父窗口内部渲染的类型),你应该会得到更平滑的结果,因为没有内存重新分配(我认为这就是导致抖动的原因)。

如果它仍然不完美,请查看WM_SIZE和WM_SIZING,并检查您是否可以用它们施展魔法。例如,在WM_SIZING上,您可以返回true,告诉Windows您处理了消息(保持窗口原样),然后将UI重新呈现到WM_SIZING提供的大小的缓冲区中,完成后,您发送自己的WM_SIZING,但在WPARAM中有一个未使用的操作位(以及它以前的内容),它告诉您有一个预先呈现的缓冲区,您可以直接清除。从msdn上的WM_SIZING文档来看,WPARAM应该有几个位可供您使用。

希望这能有所帮助。

调用CreateSwapChainForHwnd时,请确保已将交换链描述Scaling属性设置为DXGI_SCALING_NONE。这仅在带有平台更新的Windows 7上受支持,因此您可能需要回退到默认的DXGI_SCALING_STRETCH(后者是导致闪烁的原因)。

将WM_SETREDRAW设置为FALSE,调整大小,然后重新启用绘图,使窗口无效,操作系统将对其进行闪电扫描。

我这样做是为了在从列表中选择不同项目时启用和禁用按钮,而不是整个窗口。

这是我想到的最好的,尽管后缓冲区闪电战会导致一些边缘闪烁,但它的大小非常好,还没有用DX或OGL进行测试,但它应该在硬件加速方面工作得更好。它有点笨重,但可以作为概念的证明。

如果可以在不使用MDI的情况下剪裁画布,那就更好了,比如使用位掩码缓冲区。

有一件事我不满意,那就是子窗口的位置坐标,因为它们可能不适用于所有系统,但GetSystemMetrics调用来获取边框和标题大小应该可以解决这个问题。

/* Smooth resizing of GDI+ MDI window
* 
* Click window to resize, hit Escape or Alt+F4 to quit
* 
* Character type is set to multibyte
* Project->Properties->Config Properties->General->Character Set = Multibyte
* 
* Pritam 2014 */

// Includes
#include <Windows.h>
#include <gdiplus.h>
#pragma comment (lib,"Gdiplus.lib")
using namespace Gdiplus;

// Max resolution
#define XRES 1600
#define YRES 900

// Globals
bool resizing = false;
HWND parent, child;        // child is the canvas window, parent provides clipping of child
Bitmap * buffer;

// Render
void Render() {
// Get parent client size
RECT rc;
GetClientRect(parent, &rc);
// Draw backbuffer
Graphics * g = Graphics::FromImage(buffer);
// Clear buffer
g->Clear(Color(100, 100, 100));
// Gray border
Pen pen(Color(255, 180, 180, 180));
g->DrawRectangle(&pen, 10, 10, rc.right - 20, rc.bottom - 20);
pen.SetColor(Color(255, 0, 0, 0));
g->DrawRectangle(&pen, 0, 0, rc.right - 1, rc.bottom - 1);
// Draw buffer to screen
PAINTSTRUCT ps;
HDC hdc = BeginPaint(child, &ps);
Graphics graphics(hdc);
graphics.DrawImage(buffer, Point(0, 0));
// Free
EndPaint(child, &ps);
}

// MDI Callback
LRESULT CALLBACK MDICallback(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
switch(message) {
case WM_LBUTTONDOWN:
resizing = true; // Start resizing
return 0;
break;
case WM_KEYDOWN:
if(wparam == VK_ESCAPE) { // Exit on escape
PostQuitMessage(0);
}
TranslateMessage((const MSG *)&message);
return 0;
break;
case WM_PAINT:
Render();
return 0;
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
break;
}
return DefMDIChildProc(hwnd, message, wparam, lparam);
}

// Parent window callback
LRESULT CALLBACK WndCallback(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
return DefFrameProc(hwnd, child, message, wparam, lparam);
}

// Create windows
bool CreateWindows(void) {
// Parent class
WNDCLASSEX wndclass;
ZeroMemory(&wndclass, sizeof(wndclass)); wndclass.cbSize = sizeof(wndclass);
wndclass.style = CS_NOCLOSE;
wndclass.lpfnWndProc = WndCallback;
wndclass.hInstance = GetModuleHandle(NULL);
wndclass.lpszClassName = "WNDCALLBACKPARENT";
wndclass.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
if(!RegisterClassEx(&wndclass)) return false;
// MDI class
wndclass.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
wndclass.lpfnWndProc = MDICallback;
wndclass.lpszClassName = "MDICALLBACKCANVAS";
if(!RegisterClassEx(&wndclass)) return false;

// Parent window styles
DWORD style = WS_POPUP | WS_CLIPCHILDREN;
DWORD exstyle = 0;
// Set initial window size and position
RECT rc;
rc.right = 640;
rc.bottom = 480;
AdjustWindowRectEx(&rc, style, false, exstyle);
rc.left = 20;
rc.top = 20;
// Create window
if(!(parent = CreateWindowEx(exstyle, "MDICLIENT", "MDI Resize", style, rc.left, rc.top, rc.right, rc.bottom, NULL, NULL, wndclass.hInstance, NULL))) return false;

// MDI window styles
style = MDIS_ALLCHILDSTYLES;
exstyle = WS_EX_MDICHILD;
// Set MDI size
rc.left = - 8; // The sizes occupied by borders and caption, if position is not correctly set an ugly caption will appear
rc.top = - 30;
rc.right = XRES;
rc.bottom = YRES;
AdjustWindowRectEx(&rc, style, false, exstyle);
// Create MDI child window
if(!(child = CreateWindowEx(exstyle, "MDICALLBACKCANVAS", "", style, rc.left, rc.top, rc.right, rc.bottom, parent, NULL, wndclass.hInstance, NULL))) return 8;
// Finalize
ShowWindow(child, SW_SHOW);
ShowWindow(parent, SW_SHOWNORMAL);
// Success
return true;
}

// Resize
void Resize(void) {
// Init
RECT rc, rcmdi;
GetClientRect(child, &rcmdi); // Use mdi window size to set max resize for parent
GetWindowRect(parent, &rc);
// Get mouse position
POINT mp;
GetCursorPos(&mp);
// Set new size
rc.right = mp.x - rc.left + 10;
rc.bottom = mp.y - rc.top + 10;
// Apply min & max size
if(rc.right < 240) rc.right = 240; if(rc.bottom < 180) rc.bottom = 180;
if(rc.right > rcmdi.right) rc.right = rcmdi.right; if(rc.bottom > rcmdi.bottom) rc.bottom = rcmdi.bottom;
// Update window size
SetWindowPos(parent, NULL, rc.left, rc.top, rc.right, rc.bottom, SWP_NOZORDER | SWP_NOMOVE);
// Make sure client is entirely repainted
GetClientRect(child, &rc);
InvalidateRect(child, &rc, false);
UpdateWindow(child);
// Stop resizing if mousebutton is up
if(!(GetKeyState(VK_LBUTTON) & 1 << (sizeof(short) * 8 - 1)))
resizing = false;
}

// Main
int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE pinstance, LPSTR cmdline, int cmdshow) {
// Initiate GDI+
ULONG_PTR gdiplusToken;
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
buffer = new Bitmap(XRES, YRES, PixelFormat24bppRGB);
// Create windows
if(!CreateWindows()) return 1;

// Main loop
bool running = true;
MSG message;
while(running) {
// Check message or pass them on to window callback
if(PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {
if(message.message == WM_QUIT) {
running = false;
} else {
if(!TranslateMDISysAccel(child, &message)) {
TranslateMessage(&message);
DispatchMessage(&message);
}
}
}
// Resize
if(resizing)
Resize();
// Sleep a millisecond to spare the CPU
Sleep(1);
}

// Free memmory and exit
delete buffer;
GdiplusShutdown(gdiplusToken);
return 0;
}

编辑:另一个使用"bitmask"/分层窗口的例子。

// Escape to quit, left mousebutton to move window, right mousebutton to resize.
// And again char set must be multibyte
// Include
#include <Windows.h>
#include <gdiplus.h>
#pragma comment (lib,"Gdiplus.lib")
using namespace Gdiplus;

// Globals
Bitmap * backbuffer;
int xres, yres;
bool move, size;
POINT framePos, frameSize, mouseOffset;
// Renders the backbuffer
void Render(void) {
if(!backbuffer) return;
// Clear window with mask color
Graphics * gfx = Graphics::FromImage(backbuffer);
gfx->Clear(Color(255, 0, 255));
// Draw stuff
SolidBrush brush(Color(120, 120, 120));
gfx->FillRectangle(&brush, framePos.x, framePos.y, frameSize.x, frameSize.y);
}
// Paints the backbuffer to window
void Paint(HWND hwnd) {
if(!hwnd) return;
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
Graphics gfx(hdc);
gfx.DrawImage(backbuffer, Point(0, 0));
EndPaint(hwnd, &ps);
}

void HandleMove(HWND hwnd) {
// Get mouse position
POINT mouse;
GetCursorPos(&mouse);
// Update frame position
framePos.x = mouse.x - mouseOffset.x;
framePos.y = mouse.y - mouseOffset.y;
// Redraw buffer and invalidate & update window
Render();
InvalidateRect(hwnd, NULL, false);
UpdateWindow(hwnd);
// Stop move
if(!(GetKeyState(VK_LBUTTON) & 1 << (sizeof(short) * 8 - 1)))
move = false;
}
void HandleSize(HWND hwnd) {
// Get mouse position
POINT mouse;
GetCursorPos(&mouse);
// Update frame size
frameSize.x = mouse.x + mouseOffset.x - framePos.x;
frameSize.y = mouse.y + mouseOffset.y - framePos.y;
//frameSize.x = mouse.x + mouseOffset.x;
//frameSize.y = mouse.y + mouseOffset.y;
// Redraw buffer and invalidate & update window
Render();
InvalidateRect(hwnd, NULL, false);
UpdateWindow(hwnd);
// Stop size
if(!(GetKeyState(VK_RBUTTON) & 1 << (sizeof(short) * 8 - 1)))
size = false;
}

LRESULT CALLBACK WindowCallback(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
POINTS p;
switch(msg) {
case WM_KEYDOWN:
if(wparam == VK_ESCAPE) PostQuitMessage(0);
return 0;
break;
case WM_LBUTTONDOWN:
p = MAKEPOINTS(lparam); // Get mouse coords
mouseOffset.x = p.x - framePos.x;
mouseOffset.y = p.y - framePos.y;
move = true;
break;
case WM_RBUTTONDOWN:
p = MAKEPOINTS(lparam);
mouseOffset.x = framePos.x + frameSize.x - p.x;
mouseOffset.y = framePos.y + frameSize.y - p.y;
size = true;
break;
case WM_PAINT:
Paint(hwnd);
return 0;
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
break;
}
return DefWindowProc(hwnd, msg, wparam, lparam);
}

// Main
int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE pinstance, LPSTR cmdline, int cmdshow) {
// Init resolution, frame
xres = GetSystemMetrics(SM_CXSCREEN);
yres = GetSystemMetrics(SM_CYSCREEN);
move = false; size = false;
framePos.x = 100; framePos.y = 80;
frameSize.x = 320; frameSize.y = 240;
mouseOffset.x = 0; mouseOffset.y = 0;
// Initiate GDI+
ULONG_PTR gdiplusToken;
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
// Init backbuffer
backbuffer = ::new Bitmap(xres, yres, PixelFormat24bppRGB);
Render();

// Window class
WNDCLASSEX wc; ZeroMemory(&wc, sizeof(wc)); wc.cbSize = sizeof(wc);
wc.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
wc.lpfnWndProc = WindowCallback;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = "SingleResizeCLASS";
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
if(!RegisterClassEx(&wc)) return 1;

// Create window
HWND hwnd;
DWORD style = WS_POPUP;
DWORD exstyle = WS_EX_LAYERED;
if(!(hwnd = CreateWindowEx(exstyle, wc.lpszClassName, "Resize", style, 0, 0, xres, yres, NULL, NULL, wc.hInstance, NULL)))
return 2;
// Make window fully transparent to avoid the display of unpainted window
SetLayeredWindowAttributes(hwnd, 0, 0, LWA_ALPHA);
// Finalize
ShowWindow(hwnd, SW_SHOWNORMAL);
UpdateWindow(hwnd);
// Make window fully opaque, and set color mask key
SetLayeredWindowAttributes(hwnd, RGB(255, 0, 255), 0, LWA_COLORKEY);

// Main loop
MSG msg;
bool running = true;
while(running) {
// Check message
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if(msg.message == WM_QUIT) {
running = false;
} else {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// Move or size frame
if(move) { HandleMove(hwnd); }
if(size) { HandleSize(hwnd); }
Sleep(1);
}
// Free memory
::delete backbuffer;
backbuffer = NULL;
GdiplusShutdown(gdiplusToken);
// Exit
return 0;
}

虽然你的目标值得称赞,但我怀疑任何这样做的尝试都会以你和Windows之间的战斗告终——你不会赢(尽管你可能会设法争取到一场体面的平局)。抱歉,我是否定的。