如何将 Win32 应用程序的背景颜色初始化为白色以外的其他颜色以避免在 ShowWindow 上闪烁?

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

我正在研究为什么 运行 我的 Windows 应用程序在呈现实际应用程序之前(即 WM_ERASEBKGND 和 WM_PAINT 已收到)。

现在,我刚刚注意到 Visual Studio 创建的默认示例应用程序中也存在此问题。至少当 运行 在 Windows 10,21H1(在 VS2008 和 VS2013 中)时,我就是这种情况。

创建“新 Win32 项目”后,您唯一需要做的就是更改 window class 的背景颜色,例如,更改为红色:

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

然后在 WndProc 中添加一个带有 Sleep 的 WM_ERASEBKGND:

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

Sleep 夸大了这个问题,导致白色背景至少显示一秒钟。 之后红色背景如预期绘制。

当 运行 具有这些更改的应用程序时,我会添加一个简短的 video。

对于任何应用程序来说,window 在渲染之前闪烁白色看起来很不专业, 特别是如果界面很暗。 所以我的问题是:是什么导致了这种行为? 背景色是通过RegisterClassEx设置的,传递给CreateWindow,然后再调用ShowWindow(..) 所以Windows应该知道背景色是红色的。那么为什么它会呈现白色呢?我错过了什么吗?

理想情况下,我想将初始背景颜色更改为白色以外的颜色,例如黑色。但是怎么办?我试过在调用 ShowWindow 之前绘制到 window,但没有成功。

我做了一些更多的测试,并希望 post 这个问题的潜在答案。 现在,这主要基于@JonathanPotter 的建议,完全归功于他。虽然它并没有真正解决问题,但确实减轻了很多。

现在,理想情况下,如果 Windows 简单地用正确的初始背景颜色渲染 window 就好了,但无论我多么努力地尝试,我只能得到它使用 WM_ERASEBKGND 或 WM_PAINT.

更新背景颜色

所以显示 window(即使用 ShowWindow)和实际清除背景(WM_ERASEBKGND)之间的时间延迟似乎是关键问题。因此,对它进行剖析是有意义的。我这样做是通过使用 QueryPerformanceCounter.

记录调用 ShowWindow 和达到 WM_ERASEBKGND 之间的时间差

所以在 i7-4960HQ CPU @ 2.60GHz 运行 Window 10 上,ShowWindow 和 WM_ERASEBKGND 之间的时间在 100 - 317 毫秒。它波动很大。这是一个普通的 Win32 示例应用程序,内置在 Release 中,没有任何 Sleeps 或类似的东西,但使用红色 hbrBackground 来显示问题。这意味着在绘制红色背景之前,白色背景在几帧内清晰可见。这是以 25Hz 捕获的动画 gif: 在该动画中,白色背景可见 3 帧。

现在可能的解决方法是在显示 window.

之前结合使用 SetWindowPos 和 RedrawWindow

对于我的测试,我只是在调用 ShowWindow(..):

之前添加了这两行
   SetWindowPos(hWnd, NULL, 0,0,0,0,   SWP_NOMOVE | SWP_NOSIZE | SWP_NOREDRAW);
   RedrawWindow(hWnd, NULL, 0, RDW_INVALIDATE |  RDW_ERASE);

虽然重绘Window好像没什么区别。 再次分析,ShowWindow 和 WM_ERASEBKGND 之间的时间现在是 10 - 23ms。 10 倍加速!

同样,在 25Hz 下捕获的动画 gif(使用 SetWindowPos): 这清楚地表明白色背景的闪光已经消失,因此问题已解决。就像白天和黑夜。

现在,我认为这不是修复,而是解决方法。由于 Windows 使用白色背景颜色的潜在问题仍然存在。 由于这是一个时间问题,我可以很容易地想象白色背景会再次出现,比如系统运行缓慢或忙于做其他事情。 同样,拥有更快的系统意味着您不太可能首先看到这一点,从而有效地隐藏了问题。 但是简单地在 WM_ERASEBKGND 中设置一个断点仍然会显示一个白色的 window.

此外,我对加速没有任何解释。我跟踪了消息泵中的消息数量,两种情况下它们是相同的。

现在,我仍然希望有更好的解决办法。我很难相信微软工程师发现用硬编码的 0xFFFFFF 填充所有新创建的 Windows 很酷,所以我希望这种颜色实际上是 read 来自某处,因此可以更改,因此初始背景与 hbrBackground 匹配。

请随时 post 其他答案、问题或建议。 如果我有其他发现,我当然会更新这个帖子。

又查了一下,所以这是一个不同的潜在答案。

我意识到,即使我完全丢弃 WM_PAINT 和 WM_ERASEBKGND(即 WM_PAINT 中的 return 0 和 [=52 中的 return TRUE =]),我仍然可以通过手动调整大小 window 让应用程序绘制红色背景! 这是一个片段来说明:

这意味着Windows确实了解并尊重hbrBackground,这太棒了!出于某种奇怪的原因,它只是不清除它,而是清除它。

(顺便说一句,我在注册表中检查了所有系统颜色(HKEY_CURRENT_USER\Control Panel\Colors HKEY_CURRENT_USER\Control Panel\Desktop\Colors),设置为“255 255 255”并强行更改它们以查看是否会更改初始的白色背景。但没有运气。这让我得出结论,白色背景不是系统颜色。)

无论如何,上面的内容让我尝试在 ShowWindow 之后以编程方式调整 window 的大小。但是因为我不希望它在打开时闪烁,所以在屏幕外执行 ShowWindow。

下面是替换常规 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 ); 

现在,我会称之为 hack。然而,它不依赖于 WM_ERASEBKGND 或 WM_PAINT,因此时间问题应该较少。 此外,window 的显示与常规 ShowWindow(...) 完全一样,只是具有正确的 hbrBackground,这正是我想要的。

这是它在 25Hz 时的样子:

注意没有白色背景的闪烁。

请注意,我已尝试编写代码以满足虚拟 desktop/multi-monitor,但尚未实际测试。

但不幸的是,一切都不是很好。在我写这个答案时,我用 OBSStudio 录制 @ 60Hz 做了很多试运行,并浏览了镜头。在那里,我发现一个只在一帧中显示 window 帧内的垃圾(显然来自 Chrome)。这是一个放慢速度的回放:

我很难过。也许这才是真正的问题?

一个更有争议的答案可能是,这只是 Windows 中的一个错误。

作为参考,(除了我已经发布的 Windows 10 中的现有 GIF)这里是示例应用程序 运行 的录音,在 Windows XP 中没有背景擦除, Windows 7 和 Windows 11.

Windows 经验值:

Windows XP: 没有 WM_ERASEBKGND/WM_PAINT: OK (没有白色背景)

Windows XP: With WM_ERASEBKGND: OK (无白底)

Windows 7:

Windows 7:没有WM_ERASEBKGND/WM_PAINT:不行(白色背景)

Windows 7:与WM_ERASEBKGND:不正常(白色背景)

Windows 7:使用 WM_ERASEBKGND + 睡眠:不正常(白色背景)

Windows 7 禁用 Aero:

Windows 7 禁用 Aero:不使用 WM_ERASEBKGND/WM_PAINT:OK(无白色背景)

Windows 7 禁用 Aero: WM_ERASEBKGND:OK(无白色背景)

Windows 7 禁用 Aero:WM_ERASEBKGND + 睡眠:OK(无白色背景)

Windows 11(禁用动画):

Windows 11:没有 WM_ERASEBKGND/WM_PAINT:不正常(白色背景)

Windows 11: With WM_ERASEBKGND: OK (无白底)

Windows 11:使用 WM_ERASEBKGND + 睡眠:不正常(白色背景)

我已将睡眠添加到难以发现问题的测试中。

总结一下:

  • Windows XP:没问题。一切似乎都按预期工作。
  • Windows 7:启用 Aero 时出现问题 (Windows 7 主题),但禁用时不会(经典主题)。
  • Windows 10:所有测试都出现问题。
  • Windows 11:出现问题,但在不添加睡眠的情况下工作。 很可能是因为这是 运行 在更快的机器上。

因此,尽管我无法从这些测试中得出任何可靠的结论,但看起来 这种行为确实是在 Windows 7 中引入的 Aero

如果有人可以揭穿此说法,请在下方发表评论。

正如 OP 的出色研究所证明的那样,这确实似乎是一个 Windows 错误。

bug甚至是affecting applications developed by Microsoft.

问题是最好的解决方法是什么,特别是对于需要支持向后兼容性的产品,即使在 Windows 11(或 Windows 10)的特定版本中发布修复后也是如此。

主要问题是,正是使 window 可见的行为使得 Windows 在正确应用背景画笔之前使用白色画笔对其进行绘制,而不管绘制的是什么事先它的DC。因此,在显示 window 之前在 DC 中绘制等技巧并不令人满意,因为白色背景仍会显示,即使只有几帧。

一种似乎很有效的方法是使 window 可见,但完全透明,绘制背景,然后使 window 不透明。我们还需要为 window 的激活设置动画,这样它就不会突然弹出。例如,我们可以劫持 WM_SHOWWINDOW 为此:

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;

完整示例代码:

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