Smooth window resizing in Windows (using Direct2D 1.1)?
Asked Answered
S

6

15

It annoys me that the resizing of windows in Windows is not as "smooth" as it I'd like it to be (this is the case with Windows programs in general, not just my own. Visual Studio is a good example). It makes the OS and its programs feel "flimsy" and "cheap" (yes, I care about how programs and user interfaces feel, in the same way I care about the sound and feel of closing a car door. It's a reflection of build quality), which in my view affects the overall UX and ultimately the perception of the brand.

The redrawing of window contents simply does not keep up with mouse movement during resize. Whenever I resize a window, there is a "stuttering" / "flickering" effect, seemingly due to the previous-size-contents of the window being redrawn in the new, resized window frame before the new, resized contents are drawn.

I am building a Win32 application (x64) that uses Direct2D 1.1 to draw its UI, and given the speed of Direct2D, i think it should be unnecessary to suffer such artifacts in an OS in 2014. I am on Windows 8.1 myself, but targeting Windows 7 and up with this application.

The "previous size" effect is especially discernible when maximizing a small window (since the difference in window size is sufficiently great to easily contrast the image of the old content as it flashes briefly in the upper left corner of the larger window with the new content subsequently being painted over it).

This is what appears to be going on:

  1. (Let's say there's a fully rendered window on screen, size 500 x 500 pixels).
  2. I maximize the window:
  3. The window frame is maximized
  4. The old 500 x 500 content is drawn in the new frame, before..
  5. ..the maximized window is repainted with properly sized content.

I'm wondering if there's any way to mitigate this (i.e. get rid of step 4) - via intercepting a Windows Message, for example - and avoid the window being repainted at the new size with the old content before the final re-rendering of the new content happens. It's like Windows does the window redrawing itself, using whatever graphics it already has available, BEFORE it bothers to ask me to provide updated content with a WM_PAINT message or similar.

Can it be done?

Edit: It seems that WM_WINDOWPOSCHANGING / WM_SIZING provides "early access" to the new size data, but I still haven't managed to suppress the painting of the old content.

My WndProc looks like this:

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

The window does not have CS_HREDRAW or CS_VREDRAW set. The swapchain is double buffered and the Present call is made with SyncInterval = 0.

I am aware that recreating the swapchain buffers every time the window size changes does create some overhead compared to plain redrawing on a static window surface. However, the "stuttering" is not caused by that, as it happens even when buffer resizing is disabled and existing window contents are simply scaled during window resizing (although that does make it keep up better with mouse movement).

Skiplane answered 16/2, 2014 at 20:43 Comment(10)
The problem is that in Windows painting messages are asynchronous - by the time you start handling a repaint the window could have already changed size again. You can minimize the effects with a lot of effort but you can never completely overcome it.Ibnrushd
Are we talking removing window borders and manually implementing drag/resize detection on the client area, determining new window size myself, resizing swapchain, repainting and THEN manually resizing the window before processing the next (current) mouse position?Skiplane
Those are the sort of lengths you'd need to go to I suspect.Ibnrushd
What you see is - in a way - deliberate behavior. For example, you speak of the window contents being "redrawn" too small right after maximising, but it technically is not being repainted at this exact moment, just like if you were to move it. Such optimizations (or all of Desktop Composition generally) are done to make the GUI feel more responsible (give faster visual feedback when the user takes an action - actual repainting would take noticably too long). We are so used to this responsiveness of today's OSes that even slight delays (<50ms) may feel irritating to established users.Abomb
@JonathanPotter: Hmm. That might not be so bad in my case. I am building a complete WPF-ish layout system anyway, with built-in drag'n'drop and resizing functionality for UI elements, so I could just designate the whole window as such a [root] control and get it for free.. For now, I'm looking into WM_WINDOWPOSCHANGING after seeing this article by Raymond Chen: blogs.msdn.com/b/oldnewthing/archive/2008/01/16/7123299.aspxSkiplane
@dialer: I understand the responsiveness concern, but I'm tossing graphics around at 60 FPS (which is the screen refresh rate anyway) in full screen windows, so I can't understand why the same redraw speed couldn't be used to update the window contents at 60 FPS during resizing, too..Skiplane
@Skiplane That is a good point and I didn't want to keep you from trying, just commenting why things are this way. I'd be very interested if you manage to come up with a solution (maybe you can keep this thread updated if you keep working on it?). I've tried messing with this myself, but I came to the conclusion that this might just not fit in the concept of how Windows handles windows (<- ?)Abomb
@dialer: Sure. I suspect this could be some kind of architectural relic from earlier OS versions, originally designed for slower processors and before Direct2D / GPU acceleration.. But this is 2014 and I want my applications to feel like a Mercedes, not a 70s Skoda :)Skiplane
@JonathanPotter Solved. I did completely overcome it (but I did need to go to the lengths outlined in my first comment).Skiplane
@Abomb Done. Hopefully I will have time to post the solution along with some musings as an Answer at a later time.Skiplane
G
2

There is a way to prevent the needless BitBlt mentioned in step 4 above.

Until Windows 8 it could be done either by creating your own custom implementation of WM_NCCALCSIZE to tell Windows to blit nothing (or to blit one pixel on top of itself), or alternately you could intercept WM_WINDOWPOSCHANGING (first passing it onto DefWindowProc) and set WINDOWPOS.flags |= SWP_NOCOPYBITS, which disables the BitBlt inside the internal call to SetWindowPos() that Windows makes during window resizing. This has the same eventual effect of skipping the BitBlt.

However, nothing can be so simple. With the advent of Windows 8/10 Aero, apps now draw into an offscreen buffer which is then composited by the new, evil DWM.exe window manager. And it turns out DWM.exe will sometimes do its own BitBlt type operation on top of the one already done by the legacy XP/Vista/7 code. And stopping DWM from doing its blit is much harder; so far I have not seen any complete solutions.

So you need to get through both layers. For sample code that will break through the XP/Vista/7 layer and at least improve the performance of the 8/10 layer, see:

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

Graduation answered 16/2, 2014 at 20:43 Comment(0)
A
1

When calling CreateSwapChainForHwnd, ensure that you have set the swap chain description Scaling property to DXGI_SCALING_NONE. This is only supported on Windows 7 with the Platform Update, so you may need to fall back to the default DXGI_SCALING_STRETCH (the latter is what is causing the flickering).

Algae answered 16/2, 2014 at 20:43 Comment(0)
H
1

What if you have a borderless childwindow (the type that only renders inside the parent) at a fixed size (same as fullscreen resolution), you should get a lot smoother results because there is no memory reallocation (which i think is what causes the jitterieness).

If it's still not perfect, look into both WM_SIZE and WM_SIZING and check if you can do some magic with them. For instance, on WM_SIZING you could return true telling Windows you handled the message (leaving the window as is) and you re-render your UI to a buffer with the size provided by WM_SIZING and when that is done, you send your own WM_SIZING but with a manipulated unused bit in WPARAM (along with its previous content) that tells you you have a pre-rendered buffer for this that you can just blit out. From the WM_SIZING documentation on msdn it looks like WPARAM should have a couple of bits at your disposal.

Hope this helps.

Humus answered 16/2, 2014 at 20:43 Comment(3)
The application is already running a fully featured custom (Direct2D/3D based) layout system internally - similar to an OS desktop with windows that can be moved, resized, faded etc - and that is smooth as silk. And it is meant to be run in full-screen mode. But when the application is running in windowed mode, there are reasons to keep it as a regular, windowed Windows window - which is where the challenge lies.Skiplane
I have yet to try bypassing all WM_ based sizing paths and instead rely on my own layout system to handle it - by having a borderless main window, but with "custom" borders that capture the mouse and track dragging. It would then first resize the window buffer (using the swapchain buffer resizing already in place), re-render the contents and then manually setting the window size / position to reflect the changes. Rather than Windows first resizing the window border and then letting me know the new size afterwards, via WM_SIZE etc. That disconnect could be the cause of the flicker.Skiplane
Bypassing the WM_size messages sounds like a good idea, i'm also looking for a solution for smooth Direct3D/OpenGL resizing when my project gets a bit further, also using a borderless window. Please post your solution when you get around to it, i'll do the same.Humus
H
0

This is the best I've come up with and resizes great although the backbuffer blitting causes some edge flickering, haven't tested with DX or OGL yet but it should work even better with hardware acceleration. It's a bit bulky but will do as a proof of concept.

If the canvas could be clipped without using MDI that would be even better, like using a bitmask buffer.

One thing i'm not happy about is the position coords of the child window because they might not work on all systems, but a combination of GetSystemMetrics calls to get border and caption sizes should fix that.

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

Edit: Another example using "bitmask"/layered window.

// 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;
}
Humus answered 16/2, 2014 at 20:43 Comment(1)
I think your best bet is something like thisHumus
A
0

Set WM_SETREDRAW to FALSE, do your resizing, then reenable drawing, invalidate the window and the OS will blit it.

I've done this for button enabling and disabling buttons when selecting different items from a list, never for an entire window.

Ascarid answered 16/2, 2014 at 20:43 Comment(1)
That disables painting altogether, so the window contents are not dynamic at all during resizing, but instead the old content is simply cropped or extended with black (depending on whether you're sizing down or up, respectively). And to top it off, the "paint last content" artifact is still there when reenabling and repainting at the end of the operation.Skiplane
K
-4

While your aim is laudable, I suspect that any attempt to do this will just end up as a fight between you and Windows - which you will not win (though you might manage to fight your way to an honourable draw). Sorry to be negative.

Knowing answered 16/2, 2014 at 20:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.