如何将Win32 App的背景颜色初始化为白色以外的颜色以避免ShowWindow上的闪烁?

我正在研究为什么在运行我的 Windows 应用程序时,它在渲染实际应用程序之前(即在收到 WM_ERASEBKGND 和 WM_PAINT 之前)有短暂的白色背景闪烁。

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

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

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

睡眠夸大了问题,导致白色背景显示至少一秒钟。之后,红色背景按预期绘制。

在运行带有这些更改的应用程序时,我将包含一个简短的 video

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

理想情况下,我想将此初始背景颜色更改为白色以外的颜色,例如黑色。但是怎么做?在调用 ShowWindow 之前,我尝试过绘制到窗口,但没有运气。

stack overflow How to initialize the background color of Win32 App to something other than white to avoid flash on ShowWindow?
原文答案
author avatar

接受的答案

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

这个错误甚至是 affecting applications developed by Microsoft

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

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

一种似乎效果很好的方法是使窗口可见,但完全透明,绘制背景,然后使窗口不透明。我们还需要对窗口的激活进行动画处理,因此它不只是弹出。例如,我们可以为此劫持 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;
}

答案:

作者头像

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

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

Windows XP:

Windows XP:没有 WM_ERASEBKGND/WM_PAINT:OK(无白色背景) enter image description here

Windows XP:使用 WM_ERASEBKGND:OK(无白色背景) enter image description here

Windows 7的:

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

Windows 7:使用 WM_ERASEBKGND:不行(白色背景) enter image description here

Windows 7:使用 WM_ERASEBKGND + 睡眠:不行(白色背景) enter image description here

禁用 Aero 的 Windows 7:

禁用 Aero 的 Windows 7:没有 WM_ERASEBKGND/WM_PAINT:OK(无白色背景) enter image description here

禁用 Aero 的 Windows 7:使用 WM_ERASEBKGND:OK(无白色背景) enter image description here

禁用 Aero 的 Windows 7:使用 WM_ERASEBKGND + 睡眠:OK(无白色背景) enter image description here

Windows 11(禁用动画)

Windows 11:没有 WM_ERASEBKGND/WM_PAINT:不行(白色背景) enter image description here

Windows 11:使用 WM_ERASEBKGND:OK(无白色背景) enter image description here

Windows 11:使用 WM_ERASEBKGND + 睡眠:不行(白色背景) enter image description here

我在很难发现问题的测试中添加了睡眠。

总结一下:

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

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

如果有人可以揭穿这种说法,请在下面发表评论。

作者头像

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

现在,理想情况下,如果 Windows 能够简单地使用正确的初始背景颜色渲染窗口,那就太好了,但无论我多么努力,我只能通过使用 WM_ERASEBKGND 或 WM_PAINT 来更新背景颜色。

因此,显示窗口(即使用 ShowWindow)与实际清除背景(WM_ERASEBKGND)之间的时间延迟似乎是问题的症结所在。因此,对其进行概要分析是有意义的。我通过记录调用 ShowWindow 和使用 QueryPerformanceCounter 到达 WM_ERASEBKGND 之间的时间差来做到这一点。

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

现在潜在的解决方法是在显示窗口之前使用 SetWindowPos 和 RedrawWindow 的组合。

对于我的测试,我只是在调用 ShowWindow(..) 之前添加了这两行:

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

尽管 RedrawWindow 似乎没有任何区别。再次分析,ShowWindow 和 WM_ERASEBKGND 之间的时间现在是 10 - 23ms。 10倍加速!

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

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

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

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

请随时发布替代答案、问题或建议。如果我发现其他任何内容,我当然会更新此线程。

作者头像

做了更多的戳,所以这里有一个不同的潜在答案。

我意识到即使我完全放弃 WM_PAINT 和 WM_ERASEBKGND(即,在 WM_PAINT 中返回 0,在 WM_ERASEBKGND 中返回 TRUE),我仍然可以通过手动 resizing 窗口让应用程序绘制红色背景!这是一个剪辑来说明:

resize

这意味着 Windows 确实知道并尊重 hbrBackground,这很棒!出于某种奇怪的原因,它只是没有清除它,而是清除为白色。

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

无论如何,以上内容使我尝试在 ShowWindow 之后以编程方式调整窗口大小。但是因为我不希望它在打开时闪烁,所以在屏幕外执行 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,因此应该不会有时间问题。此外,窗口的显示与常规 ShowWindow(...) 完全一样,只是使用了正确的 hbrBackground,这正是我想要的。

这是@ 25Hz的样子:

offscreen

请注意,没有白色背景的闪光。

请注意,我已尝试编写代码来满足虚拟桌面/多显示器的需求,但尚未实际测试过。

但不幸的是,一切都不是很好和花花公子。在我写这个答案时,我用 OBSStudio 录制 @ 60Hz 进行了多次试运行,并浏览了这些镜头。在那里,我发现了一个在打开的窗口框架内简单地显示垃圾(显然来自 Chrome),仅显示一帧。这是一个慢下来的回放:

replay

我难住了。也许这才是真正的问题?

作者头像

我最近遇到了这个问题。我尝试了使用分层窗口和透明度的 mnistic 解决方案,但它导致在我正在处理的 MFC 应用程序中渲染窗格标题出现问题。但是,我找到了一个简单的解决方案,它看起来运行良好,无需动画、更改窗口样式等:

桌面窗口管理器 API 使窗口可以“隐藏”,因此它不会显示在屏幕上,但仍会在内部合成,即仍会累积绘图操作的结果。您可以通过以下方式打开“隐藏”:

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

为避免第一次显示窗口时出现白色闪烁,请在调用 ShowWindow() 之前执行上述操作。然后执行初始 UpdateWindow() 以获取正确的内容绘制。最后,使用以下命令关闭“隐藏”:

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

以显示最终的窗口内容。

这应该适用于具有桌面 Windows 管理器的所有 Windows 版本,例如 Windows Vista 及更高版本。