Is it possible to eliminate flickering entirely when resizing a window?
Asked Answered
A

4

11

Normally, even when using double buffering, when resizing a window, it seems that it's inevitable that the flickering will happen.

Step 1, the original window.

Step 1

Step 2, the window is resized, but the extra area hasn't been painted.

Step 2

Step 3, the window is resized, and the extra area has been painted.

Step 3

Is it possible somehow to hide setp 2? Can I suspend the resizing process until the painting action is done?

Here's an example:

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

Is it possible to eliminate the flickering?

Ange answered 26/8, 2012 at 13:30 Comment(8)
Removing the Sleep does the trick! Difficult to see what else the system can do while you take so long to paint the window.Expiratory
I added it just to show what's flickering like more clearly. I want a solution even when the drawing actions takes 320ms and still no flickering, just not show the second step.Ange
I've already set the hbrBackground to NULL, that was a mistake.Hube
@HansPassant What do you mean by "that was a mistake"?Ange
It causes the kind of flicker you describe. Set it to a brush that matches the desired background color so that WM_ERASEBKGND gives you a fast rectangle fill.Hube
What if my background is dynamically changing by window size?Ange
Then paint it in WM_ERASEBACKGROUND.Expiratory
Then I can't use double buffering. OK, that's not important. I'll focus on reducing painting time, Thanks.Ange
E
9

When the window is updated during a drag operation, then the OS has to show something in the extended window region. If you can't provide anything then it will show the background until you do. Since you didn't specify any background you get blackness. Surely you ought to be specifying a background brush? Simply adding the following to your code makes the behaviour more palatable:

wcex.hbrBackground = GetStockBrush(DKGRAY_BRUSH);

However, if you take as long as 320ms to respond to a WM_PAINT then you ruin the resize UI for the user. It becomes jerky and unresponsive. The system is designed around the assumption that you can paint the window quickly enough for dragging to feel smooth. The right way to fix your problem is to make WM_PAINT run in a reasonable time.

If you really can't achieve quick enough painting for smooth dragging then I suggest a couple of alternatives:

  1. Disable window updates during dragging. I'm sure this can be done for individual windows, but I can't remember how to do it off the top of my head.
  2. Paint something fake whilst a resize/drag is active, and postpone the real painting until when the resize/drag has completed. Listening for WM_ENTERSIZEMOVE and WM_EXITSIZEMOVE are the keys to this. This Microsoft sample program illustrates how to do that: https://github.com/microsoft/Windows-classic-samples/blob/master/Samples/Win7Samples/winui/fulldrag/
Expiratory answered 26/8, 2012 at 13:57 Comment(8)
Is there something in DWM I can use to control the composing process? I mean something like, the window informs DWM, "Stop composing the window, until the painting action is done, then compose the fully painted window to screen."Ange
OK, but what do you want to be drawn in the expanded client region? Something has to be drawn.Expiratory
Nothing. The window is expanded, yes, but I don't want to display it to user immediately. I want to display it when I finished painting.Ange
Something has to be shown. The screen has pixels there. Screens cannot display nothing.Expiratory
The desktop, another window, what's under the window.Ange
Right then, you are looking to do option 1 in my list then.Expiratory
A big +1. And, 320ms is really unacceptable for any WM_PAINT. This is a real problem. If it was me, I would asynchronously compose the image and only render a precomposed bitmap on WM_PAINT. During image composition, the system will give you the background brush for free, then maybe you could show a StretchBltd image of the previously composed image just to give the user something during those 320ms. Depends on the content of your window whether that's a good idea or a terrible one.Neologism
I agree with @tenfour. That's really the fundamental problem here. My app has some D3D MDI child windows. And when they resize it takes time to reinitialize the D3D context. Stretch drawing the most recently painted content is the solution that we use. We then draw the proper content when the resize is complete.Expiratory
D
3

Use WM_SIZING instead of WM_SIZE and don't forget about WM_ERASEBKGND.

Decastro answered 26/8, 2012 at 13:35 Comment(4)
I've already set the hbrBackground to NULL, according to #12074221, the WM_EREASEBACKGROUND doesn't need to handle anymore.Ange
Why would handling WM_SIZING make a difference?Expiratory
@David Heffernan WM_SIZE - Sent to a window after its size has changed. WM_SIZING - Sent to a window that the user is resizing. By processing this message, an application can monitor the size and position of the drag rectangle and, if needed, change its size or position.Decastro
WM_SIZE is sent during the drag as well. Sorry. WM_SIZING is meant to allow you to constrain the drag operation. For example to apply a fixed width, variably height constraint.Expiratory
A
3

If you go into the System Properties control panel applet, choose the Advanced tab, and then click Settings... in the Performance group box, you'll see a checkbox setting called Show window contents while dragging. If you uncheck that and try resizing a window, you'll see that only the window frame moves until you complete the drag operation, and then the window repaints just once at the new size. This is how window sizing used to work when we had slow, crufty computers.

Now we don't really want to change the setting globally (which you would do by calling SystemParametersInfo with SPI_SETDRAGFULLWINDOWS, but don't really do that because your users won't like it).

What happens when the user grabs the resize border is that the thread enters a modal loop controlled by the window manager. Your window will get WM_ENTERSIZEMOVE as that loop begins and WM_EXITSIZEMOVE when the operation is complete. At some point you'll also get a WM_GETMINMAXINFO, which probably isn't relevant to what you need to do. You'll also get WM_SIZING, WM_SIZE messages rapidly as the user drags the sizing frame (and the rapid WM_SIZEs often lead to WM_PAINTs).

The global Show window contents while dragging setting is responsible for getting the rapid WM_SIZE messages. If that setting is off, you'll just get one WM_SIZE message when it's all over.

If your window is complicated, you probably have layout code computing stuff (and maybe moving child windows) in the WM_SIZE handler and a lot of painting code in the WM_PAINT handler. If all that code is too slow (as your sample 320 ms delay suggests), then you'll have a flickery, jerky experience.

We really don't want to change the global setting, but it does inspire a solution to your problem:

Do simpler drawing during the resize operation and then do your (slower) complex drawing just once when the operation is over.

Solution:

  1. Set a flag when you see the WM_ENTERSIZEMOVE.
  2. Change your WM_SIZE handler to check the flag and do nothing if it's set.
  3. Change your WM_PAINT handler to check the flag and do a simple, fast fill of the window in a solid color if it's set.
  4. Clear the flag when you see WM_EXITSIZEMOVE, and then trigger your layout code and invalidate your window so that everything gets updated based on the final size.

If your slow window is a child rather than your application's top-level window, you'll have to signal the child window when the top-level window gets the WM_ENTERSIZEMOVE and WM_EXITSIZEMOVE in order to implement steps 1 and 4.

Alderman answered 30/1, 2014 at 22:54 Comment(0)
M
0

Here is my POC of a non-flickering resizable Direct3D window. It is based on a solution posted here: https://mcmap.net/q/259822/-how-to-smooth-ugly-jitter-flicker-jumping-when-resizing-windows-especially-dragging-left-top-border-win-7-10-bg-bitblt-and-dwm, but with some improvements.

Window with triangle

https://github.com/bigfatbrowncat/noflicker_directx_window

This is a Direct3D-specific solution, it contains dependency on DirectComposition (so, Windows 8, at least).

The repo contains a simple app supporting Windows 10 & 11 and DirectX 11 and 12. It paints a standard "colorful triangle" in a window. Please read the README.md for details.

The issue is rather complicated if you start digging into it and contains some non-obvious hacks (including Intel GPU-specific ones).

The overall ideas in the basement of the app:

  1. Use WS_EX_NOREDIRECTIONBITMAP flag with CreateWindowEx. That turns off all the standard old-school tricks like background painting and old, pre composition, window painting.

With this flag window becomes completely transparent, unless...

  1. Use DirectComposition API. Create a context, a Visual, prepare everything. In my app all related stuff is situated in the DCompContetxt class.

  2. Initialize Direct3D device (D3DContext class) and make all the painting in it. Do the resize of the 3D Context properly.

Never use WM_PAINT event. It occurs too late, after the window is already resized. So you will have a frame of the size bigger than the painted image. It will look like a flicker.

Instead, initiate all the size changes inside WM_NCCALCSIZE. This is the best message to recalculate everything if you want to change size before any painting and in sync with it.

Here is the central part of the code - the window initialization and the event loop:

void updateLayout(int width, int height) {
    // Uncomment this fake resizing load here to see how the app handles it
    // 100ms is a huge time pretty enough to recalculate even a very complicated layout
    //
    // Sleep(100);
}



// Win32 message handler.
LRESULT window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
    std::mutex draw_mutex;
    switch (message)
    {
        case WM_DESTROY: {
            // Destroy the DirectComposition context properly,
            // so that the window fades away beautifully.
            dcompContext->unbind();
        }

        case WM_CLOSE: {
            exitPending = true;
            return 0;
        }

        case WM_SIZING: {
            RECT& wr = *(RECT*)lparam;
            updateLayout(wr.right - wr.left, wr.bottom - wr.top);
            return 0;
        }

        case WM_NCCALCSIZE: {
            // Use the result of DefWindowProc's WM_NCCALCSIZE handler to get the upcoming client rect.
            // Technically, when wparam is TRUE, lparam points to NCCALCSIZE_PARAMS, but its first
            // member is a RECT with the same meaning as the one lparam points to when wparam is FALSE.
            DefWindowProc(hwnd, message, wparam, lparam);
            if (RECT *rect = (RECT *) lparam; rect->right > rect->left && rect->bottom > rect->top) {
                context->reposition(*rect);
            }
            // We're never preserving the client area, so we always return 0.
            return 0;
        }

        default:
            return DefWindowProc(hwnd, message, wparam, lparam);
    }
}

// The app entry point.
int WinMain(HINSTANCE hinstance, HINSTANCE, LPSTR, int)
{
    context = std::make_shared<D3DContext>();

    // Register the window class.
    WNDCLASS wc = {};
    wc.lpfnWndProc = window_proc;
    wc.hInstance = hinstance;
    wc.hCursor = win32_check(LoadCursor(nullptr, IDC_ARROW));
    wc.lpszClassName = TEXT("D3DWindow");
    win32_check(RegisterClass(&wc));

    std::wstring windowTitle = L"A Never Flickering DirectX Window";

    // Create the window. We can use WS_EX_NOREDIRECTIONBITMAP
    // since all our presentation is happening through DirectComposition.
    HWND hwnd = win32_check(CreateWindowEx(
            WS_EX_NOREDIRECTIONBITMAP, wc.lpszClassName, windowTitle.c_str(),
            WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hinstance, nullptr));

    // The DCompContext creation/destruction is fundamentally asymmetric.
    // We are cleaning up the resources in WM_DESTROY, but should NOT create the object in WM_CREATE.
    // Instead, we create it here between construction of the window and showing it
    dcompContext = std::make_shared<DCompContext>(hwnd, context);

    // Show the window and enter the message loop.
    ShowWindow(hwnd, SW_SHOWNORMAL);

    exitPending = false;
    while (!exitPending)
    {
        MSG msg;
        win32_check(GetMessage(&msg, nullptr, 0, 0) > 0);
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}
Maize answered 6/2 at 20:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.