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

时间:2014-02-16 20:43:28

标签: c++ windows winapi paint direct2d

让我感到很恼火的是,在Windows中调整Windows的大小并不像我想的那样“平滑”(一般情况下这是Windows程序的情况,而不仅仅是我自己的情况.Visual Studio就是一个很好的例子)。它使操作系统及其程序感觉“脆弱”和“便宜”(是的,我关心程序和用户界面感觉,就像我关心关闭汽车的声音和感觉一样“这是建筑质量的反映”,在我看来,这影响了整体用户体验,最终影响了品牌的感知。

在调整大小期间,重新绘制窗口内容无法跟上鼠标移动的步伐。每当我调整窗口大小时,都会出现“口吃”/“闪烁”效果,这似乎是由于在绘制新的,已调整大小的内容之前,窗口的先前大小内容被重新绘制在新的已调整大小的窗口框架中。

我正在构建一个使用Direct2D 1.1来绘制UI的Win32应用程序(x64),并且考虑到Direct2D的速度,我认为在2014年操作系统中不应该有这样的工件。我自己在Windows 8.1上,但针对Windows 7以及此应用程序。

在最大化小窗口时,“先前尺寸”效果尤为明显(因为窗口大小的差异非常大,可以轻松对比旧内容的图像,因为它在较大窗口的左上角短暂闪烁随后将新内容涂在上面。)

这似乎正在发生:

  1. (假设屏幕上有一个完全渲染的窗口,尺寸为500 x 500像素)。
  2. 我最大化窗口:
  3. 窗框最大化
  4. 旧的500 x 500内容是在新框架中绘制的,之前是..
  5. ..最大化的窗口重新绘制了适当大小的内容。
  6. 我想知道是否有任何方法可以缓解这种情况(即摆脱第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。交换链是双缓冲的,并且使用SyncInterval = 0进行Present调用。

    我知道每次窗口大小更改时重新创建交换链缓冲区会产生一些开销,而不是在静态窗口表面上进行简单重绘。但是,“口吃”不是由此引起的,因为即使在窗口大小调整期间禁用缓冲区大小调整并且现有窗口内容只是缩放时也会发生这种情况(尽管 make随着鼠标移动,它会保持更好的状态。)

6 个答案:

答案 0 :(得分:1)

如果你有一个无边框的childwindow(只在父级内部渲染的类型)以固定大小(与全屏分辨率相同)怎么办,你应该得到更平滑的结果,因为没有内存重新分配(我认为是什么导致抖动)。

如果它仍然不完美,请查看WM_SIZE和WM_SIZING,并检查是否可以对它们进行一些魔术。例如,在WM_SIZING上,您可以返回true告诉Windows您处理消息(保持窗口不变)并将UI重新呈现为WM_SIZING提供的大小的缓冲区,完成后,您发送自己的WM_SIZING但是在WPARAM中使用一个被操作的未使用位(以及它之前的内容),告诉你你有一个预先渲染的缓冲区,你可以搞清楚。从msdn上的WM_SIZING文档来看,WPARAM看起来应该有几个位。

希望这有帮助。

答案 1 :(得分:1)

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

答案 2 :(得分:0)

将WM_SETREDRAW设置为FALSE,进行大小调整,然后重新启用绘图,使窗口无效,操作系统将blit它。

我从列表中选择不同的项目时从按钮启用和禁用按钮完成此操作,从不用于整个窗口。

答案 3 :(得分:0)

这是我提出的最好的功能,虽然backbuffer blitting导致一些边缘闪烁,但尚未通过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;
}

编辑:使用&#34;位掩码&#34; /分层窗口的另一个例子。

// 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;
}

答案 4 :(得分:0)

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

在Windows 8之前,可以通过创建自己的WM_NCCALCSIZE的自定义实现来告诉Windows不进行盲处理(或对自身顶部进行消隐处理),或者也可以拦截{{1} }(首先将其传递到WM_WINDOWPOSCHANGING上)并设置DefWindowProc,这将禁用Windows在调整窗口大小期间对WINDOWPOS.flags |= SWP_NOCOPYBITS进行的内部调用中的BitBlt。最终具有跳过SetWindowPos()的相同效果。

但是,没有什么可以如此简单。随着Windows 8/10 Aero的出现,应用程序现在可以绘制到屏幕外缓冲区中,然后由新的邪恶DWM.exe窗口管理器进行合成。事实证明,DWM.exe有时会在旧版XP / Vista / 7代码已经完成的操作之上执行自己的BitBlt类型的操作。阻止DWM变得更加困难。到目前为止,我还没有看到完整的解决方案。

因此,您需要遍历这两层。有关将突破XP / Vista / 7层并至少改善8/10层性能的示例代码,请参阅:

How to smooth ugly jitter/flicker/jumping when resizing windows, especially dragging left/top border (Win 7-10; bg, bitblt and DWM)?

答案 5 :(得分:-1)

虽然你的目标值得称赞,但我怀疑任何尝试做到这一点的最终结果都只是你和Windows之间的斗争 - 你不会赢得胜利(尽管你可能会努力争取一个光荣的平局)。很抱歉是否定的。