Ralf
Ralf

Reputation: 161

How to initialize the background color of Win32 App to something other than white to avoid flash on ShowWindow?

I was looking into why when running my Windows App, it has a short flash of white background before rendering the actual app (i.e., before WM_ERASEBKGND and WM_PAINT is received).

Now, I just noticed that this problem is also present in the default sample app created by Visual Studio. At least this is the case for me when running under Windows 10,21H1 (in VS2008 & VS2013).

The only thing you have to do, after creating a "new Win32 Project", is change the background color of the window class, e.g., to the color red:

    //wcex.hbrBackground    = (HBRUSH)(COLOR_WINDOW+1);
    wcex.hbrBackground = (HBRUSH) CreateSolidBrush(RGB(255, 0, 0));

And then add a WM_ERASEBKGND with a Sleep to the WndProc:

    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        // TODO: Add any drawing code here...
        EndPaint(hWnd, &ps);
        break;
    case WM_ERASEBKGND:
        Sleep(1000);
        return DefWindowProc(hWnd, message, wParam, lParam);

The Sleep exaggerates the problem, causing the white background to show for at least a second. After that the red background is drawn as expected.

I'm including a short video when running the App with those changes.

For any app, it looks quite unprofessional that the window flashes white before rendering, especially if the interface is dark. So my question is: What is causing this behavior? The background color is set through RegisterClassEx and passed to CreateWindow, before calling ShowWindow(..) So Windows should know that the background color is red. So why does it render it white? Am I missing something?

Ideally, I would like to change this initial background color to something other than white, such as black. But how? I've tried drawing to the window before calling ShowWindow, without luck.

Upvotes: 7

Views: 5255

Answers (6)

radhino
radhino

Reputation: 11

I came across exact same problem and solved it by firstly showing window in minimized state and then restoring it.

So instead of:

ShowWindow(hwnd, SW_SHOW);

I used:

ShowWindow(hwnd, SW_SHOWMINIMIZED);
ShowWindow(hwnd, SW_RESTORE);

No white background.

Upvotes: 1

PaulOfTheCoders
PaulOfTheCoders

Reputation: 21

I've hit this issue recently. I tried mnistic's solution using layered windows and transparency but it caused problems with rendering pane captions in the MFC app I'm working on. However, I've found a simple solution which appears to work nicely without any need for animation, changing window styles, etc.:

The Desktop Window Manager API enables a window to be "cloaked", so that it isn't shown on the screen but is still internally composited, i.e. still accumulates the results of drawing operations. You can turn "cloaking" on by:

BOOL cloak = TRUE;
DwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &cloak, sizeof(cloak));

To avoid the white flash on first showing the window, do the above before calling ShowWindow(). Then do the initial UpdateWindow() to get correct content drawn. Finally, turn "cloaking" off using:

BOOL cloak = FALSE;
DwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &cloak, sizeof(cloak));

to get the final window content displayed.

This should work on all Windows versions which have a Desktop Windows Manager, so Windows Vista and up.

Upvotes: 2

mnistic
mnistic

Reputation: 11020

This indeed seems to be a Windows bug as demonstrated by the excellent research by the OP.

The bug is even affecting applications developed by Microsoft.

The question is what is best workaround, especially for products that need to support backwards compatibility even after a fix is released in a specific version of Windows 11 (or Windows 10).

The main problem is that it is the act of making the window visible that makes Windows paint it with the white brush prior to correctly applying the background brush, regardless of what was painted into its DC beforehand. Therefore tricks such as painting into the DC prior to showing the window are unsatisfying, as the white background will still be shown, even if only for a few frames.

One method that seems to work well is to make the window visible, but fully transparent, paint the background, and then make the window opaque. We also need to animate the activation of the window, so it doesn't just pop in. For example, we can hijack WM_SHOWWINDOW for this:

case WM_SHOWWINDOW:
    {
        if (!GetLayeredWindowAttributes(hWnd, NULL, NULL, NULL))
        {
            SetLayeredWindowAttributes(hWnd, 0, 0, LWA_ALPHA);
            DefWindowProc(hWnd, WM_ERASEBKGND, (WPARAM)GetDC(hWnd), lParam);
            SetLayeredWindowAttributes(hWnd, 0, 255, LWA_ALPHA);
            AnimateWindow(hWnd, 200, AW_ACTIVATE|AW_BLEND);
            return 0;
        }
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;

Full sample code:

#include "framework.h"
#include "WindowsProject1.h"

#define MAX_LOADSTRING 100

HINSTANCE hInst; 
WCHAR szTitle[MAX_LOADSTRING]; 
WCHAR szWindowClass[MAX_LOADSTRING]; 

ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);
HINSTANCE mInstance;

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    mInstance = hInstance;

    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_WINDOWSPROJECT1, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWSPROJECT1));

    MSG msg;

    while (GetMessage(&msg, nullptr, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int) msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW | CS_CLASSDC;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSPROJECT1));
    wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground  = CreateSolidBrush(RGB(255, 0, 0));
    wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT1);
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance; 

   HWND hWnd = CreateWindowExW(WS_EX_LAYERED, szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);
   
   return TRUE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_COMMAND:
        {
            int wmId = LOWORD(wParam);
            switch (wmId)
            {
            case IDM_ABOUT:
                DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
                break;
            case IDM_EXIT:
                DestroyWindow(hWnd);
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
            }
        }
        break;
    case WM_SHOWWINDOW:
        {
            if (!GetLayeredWindowAttributes(hWnd, NULL, NULL, NULL))
            {
                SetLayeredWindowAttributes(hWnd, 0, 0, LWA_ALPHA);
                DefWindowProc(hWnd, WM_ERASEBKGND, (WPARAM)GetDC(hWnd), lParam);
                SetLayeredWindowAttributes(hWnd, 0, 255, LWA_ALPHA);
                AnimateWindow(hWnd, 200, AW_ACTIVATE|AW_BLEND);
                return 0;
            }
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &ps);
            ReleaseDC(hWnd, hdc);
            EndPaint(hWnd, &ps);
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
            EndDialog(hDlg, LOWORD(wParam));
            return (INT_PTR)TRUE;
        }
        break;
    }
    return (INT_PTR)FALSE;
}

Upvotes: 6

Ralf
Ralf

Reputation: 161

A more controversial answer could be that this is simply a bug in Windows.

For reference, (aside from the existing GIFs from Windows 10 that I already posted) here are recordings of the sample app running with and without background erase in Windows XP, Windows 7 and Windows 11.

Windows XP:

Windows XP: Without WM_ERASEBKGND/WM_PAINT: OK (no white background) enter image description here

Windows XP: With WM_ERASEBKGND: OK (no white background) enter image description here

Windows 7:

Windows 7: Without WM_ERASEBKGND/WM_PAINT: NOT OK (white background) enter image description here

Windows 7: With WM_ERASEBKGND: NOT OK (white background) enter image description here

Windows 7: With WM_ERASEBKGND + Sleep: NOT OK (white background) enter image description here

Windows 7 with Aero disabled:

Windows 7 with Aero disabled: Without WM_ERASEBKGND/WM_PAINT: OK (no white background) enter image description here

Windows 7 with Aero disabled: With WM_ERASEBKGND: OK (no white background) enter image description here

Windows 7 with Aero disabled: With WM_ERASEBKGND + Sleep: OK (no white background) enter image description here

Windows 11 (with Animation disabled):

Windows 11: Without WM_ERASEBKGND/WM_PAINT: NOT OK (white background) enter image description here

Windows 11: With WM_ERASEBKGND: OK (no white background) enter image description here

Windows 11: With WM_ERASEBKGND + Sleep: NOT OK (white background) enter image description here

I've added Sleep to tests where it was hard to see the issue.

To sum up:

  • Windows XP: No issue. Everything seems to work as expected.
  • Windows 7: Issue occurs when Aero is enabled (Windows 7 theme), but not when it is disabled (Classic theme).
  • Windows 10: Issue occurs for all tests.
  • Windows 11: Issue occurs, but works without Sleep added. Most likely since this was running on a faster machine.

So although I cannot conclude anything solid from these tests, it does look like this behavior was introduced in Windows 7 with Aero.

If someone can debunk this claim, please comment below.

Upvotes: 4

Ralf
Ralf

Reputation: 161

I did some more testing, and want to post a potential answer to this question. Now, this is mainly based on the suggestion by @JonathanPotter, so full credit to him. And while it doesn't really fix the problem, it does alleviate it quite a bit.

Now, ideally, it would be great if Windows would simply render the window with the correct initial background color, but no matter how hard I've tried, I can only get it to update the background color by utilizing WM_ERASEBKGND or WM_PAINT.

So it seems that the time delay between showing the window (i.e. using ShowWindow), and the actual clearing of the background (WM_ERASEBKGND) is the crux of the problem. Hence, it makes sense to profile it. I've done so by recording the time difference between calling ShowWindow and reaching WM_ERASEBKGND using QueryPerformanceCounter.

So on an i7-4960HQ CPU @ 2.60GHz running Window 10, the time between ShowWindow and WM_ERASEBKGND is between 100 - 317ms. It fluctuates quite a bit. This is with a vanilla Win32 Sample App, built in Release without any Sleeps or anything like that, but using a red hbrBackground to show the issue. This means that the white background is clearly visible for a few frames before the red background is drawn. Here is an animated gif captured @ 25Hz: without_SetWindowPos The white background is visible for 3 frames in that animation.

Now the potential fix is to use a combination of SetWindowPos and RedrawWindow before showing the window.

For my tests I simply added these two lines before calling ShowWindow(..):

   SetWindowPos(hWnd, NULL, 0,0,0,0,   SWP_NOMOVE | SWP_NOSIZE | SWP_NOREDRAW);
   RedrawWindow(hWnd, NULL, 0, RDW_INVALIDATE |  RDW_ERASE);

Although RedrawWindow does not seem to make any difference. Profiling again, the time between ShowWindow and WM_ERASEBKGND is now 10 - 23ms. A 10x speed-up!

Again, an animated gif captured (with SetWindowPos) @ 25Hz: with_SetWindowPos This clearly shows that the flash of white background is gone, and thus the problem is fixed. It's like night and day.

Now, I would argue that this is not a fix, but rather a workaround. Since the underlying problem of Windows using a white background color is still there. And since this is a timing issue, I can easily imagine that the white background could show up again, say if the system was sluggish or busy doing other stuff. Similarly, having a faster system means you that you are less likely to see this in the first place, effectively hiding the issue. But simply setting a breakpoint in WM_ERASEBKGND will still show you a white window.

Also, I have no explanation for the speed-up. I tracked the number of messages in the message pump, and they are the same in both scenarios.

Now, I'm still hoping for a better fix. I find it hard to believe that the Microsoft engineers found it cool to fill all freshly created Windows with a hardcoded 0xFFFFFF, so I'm hoping that this color is actually read from somewhere, and thus possible to change, so the initial background matches the hbrBackground.

Please feel free to post alternative answers, questions, or suggestions. I will of course update this thread if I figure out anything else.

Upvotes: 3

Ralf
Ralf

Reputation: 161

Did some more poking, so here is a different potential answer.

I realized that even if I completely discard WM_PAINT and WM_ERASEBKGND (i.e., return 0 in WM_PAINT and return TRUE in WM_ERASEBKGND), I can still get the app to draw the red background by manually resizing the window! Here is a clip to illustrate:

resize

This means that Windows does indeed know and respect hbrBackground, which is great! For some odd reason it just doesn't clear it to that, but to white instead.

(Incidentally, I went through all the system colors in the registry (HKEY_CURRENT_USER\Control Panel\Colors HKEY_CURRENT_USER\Control Panel\Desktop\Colors) with a "255 255 255" setting and forcibly changed them to see if that would change the initial white background. But no luck. This makes me conclude that the white background is not a system color.)

Anyway, the above lead me to try to programmatically resize the window after ShowWindow. But since I don't want it to flicker on open, do the ShowWindow off-screen.

So here is the code that would replace regular ShowWindow(..):

    int x0 = GetSystemMetrics(SM_XVIRTUALSCREEN); 
    int x1 = GetSystemMetrics(SM_CXVIRTUALSCREEN); 
    
    RECT rect;
    GetWindowRect(hWnd, &rect); 

    // resize and move off-screen
    SetWindowPos(hWnd, NULL, x1-x0, 0, 0, 0, SWP_NOREDRAW );    
      
    // show window
    ShowWindow(hWnd,nCmdShow);  
    
    // restore and redraw
    SetWindowPos(hWnd, NULL, rect.left, rect.top, rect.right-rect.left, rect.bottom-rect.top, 0 ); 

Now, I would call this a hack. Yet, it does not rely on WM_ERASEBKGND nor WM_PAINT, so there should be less of a timing issue. Also, the window shows up exactly like a regular ShowWindow(...) would, just with the correct hbrBackground, which is what I wanted.

Here is what it looks like @ 25Hz:

offscreen

Notice that there is no flash of white background.

Please note that I've tried to write the code to cater for virtual desktop/multi-monitor, but haven't actually tested that.

But unfortunately everything is not fine and dandy. As I was writing this answer, I did quite a few trial runs with OBSStudio recording @ 60Hz, and went through the footage. There I found one that simply shows trash inside the window frame on open (apparently from Chrome), for just one frame. Here is a slowed-down replay:

replay

I'm stumped. Perhaps that is the real issue ?

Upvotes: 1

Related Questions