GDI+ 闪烁

Posted

技术标签:

【中文标题】GDI+ 闪烁【英文标题】:GDI+ flickering 【发布时间】:2018-12-22 01:33:50 【问题描述】:

所以我正在尝试制作 Gyazo(截图工具)的廉价副本

问题是光标坐标在闪烁,我该如何防止呢?我已经尝试过WM_ERASEBKGND,但没有任何帮助。

我的代码还有什么问题吗?有什么不好的做法/技术吗?

#include <Windows.h>
#include <string>
#include <gdiplus.h>
#pragma comment (lib, "Gdiplus.lib")

// Store the "screenshot" when first launching the program
HBITMAP hbm;

// This draws the cursor coordinates close to the cursor
void DrawCursorCoords(Gdiplus::Graphics &graphics, Gdiplus::Bitmap &bitmap, Gdiplus::Color c)

    POINT cursorPos;
    GetCursorPos(&cursorPos);

    std::wstring x = std::to_wstring(cursorPos.x);
    std::wstring y = std::to_wstring(cursorPos.y);

    graphics.DrawString(x.c_str(), x.length(), &Gdiplus::Font(L"Consolas", 16), Gdiplus::PointF(cursorPos.x, cursorPos.y), &Gdiplus::SolidBrush(c));
    graphics.DrawString(y.c_str(), y.length(), &Gdiplus::Font(L"Consolas", 16), Gdiplus::PointF(cursorPos.x, cursorPos.y + 16), &Gdiplus::SolidBrush(c));


// Paint our stuff
void Paint(HDC &hdc)

    Gdiplus::Graphics * gfx = new Gdiplus::Graphics(hdc);
    Gdiplus::Bitmap * bmap = new Gdiplus::Bitmap(hbm, (HPALETTE)0);

    gfx->DrawImage(bmap, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));

    if (GetAsyncKeyState(VK_LBUTTON))
        DrawCursorCoords(*gfx, *bmap, Gdiplus::Color::Red);
    else
        DrawCursorCoords(*gfx, *bmap, Gdiplus::Color::Green);

    delete gfx;
    delete bmap;


LRESULT APIENTRY WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

    PAINTSTRUCT ps;
    HDC hdc;

    switch (message)
    
    case WM_PAINT:
    
        hdc = BeginPaint(hwnd, &ps);

        Paint(hdc);

        EndPaint(hwnd, &ps);
        break;
    

    case WM_TIMER:
    
        InvalidateRect(hwnd, NULL, NULL);
        break;
    

    case WM_CLOSE:
    
        DestroyWindow(hwnd);
        break;
    

    case WM_RBUTTONUP:
    case WM_KEYDOWN:
    case WM_DESTROY:
    
        PostQuitMessage(0);
        break;
    

    case WM_ERASEBKGND: return TRUE;

    default: return DefWindowProc(hwnd, message, wParam, lParam);

    

    return 0L;


BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)

    char className[] = "_className";
    HWND hwnd = NULL;

    WNDCLASS wc;
    wc.style = 0;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = NULL;
    wc.hCursor = LoadCursor(NULL, IDC_CROSS);
    wc.hbrBackground = (HBRUSH)(0);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = className;

    if (RegisterClass(&wc))
    
        hwnd = CreateWindowEx(
            WS_EX_TRANSPARENT | WS_EX_TOPMOST,
            className, NULL,
            WS_POPUP | WS_VISIBLE,
            0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),
            NULL, NULL, hInstance, NULL);
    

    if (!hwnd) return FALSE;

    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);

    SetTimer(hwnd, 1, 1, NULL);

    return TRUE;


int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

    // Take a screenshot and store it to 'hbm'
    HWND hwndDesktop = GetDesktopWindow();
    HDC hdcDesktop = GetDC(hwndDesktop);
    HDC hdcCapture = CreateCompatibleDC(hdcDesktop);
    hbm = CreateCompatibleBitmap(hdcDesktop, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
    SelectObject(hdcCapture, hbm);
    BitBlt(hdcCapture, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),
        hdcDesktop, 0, 0, SRCCOPY | CAPTUREBLT);

    // Start GDI+
    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

    MSG msg;

    InitInstance(hInstance, nCmdShow);

    while (GetMessage(&msg, NULL, 0, 0) > 0)
    
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    

    Gdiplus::GdiplusShutdown(gdiplusToken);
    DeleteObject(hbm);
    ReleaseDC(hwndDesktop, hdcDesktop);
    DeleteDC(hdcDesktop);
    DeleteDC(hdcCapture);

    return msg.wParam;

【问题讨论】:

是 DrawImage() 调用导致闪烁。它覆盖先前绘制的文本,然后 DrawCursorCoords() 将其放回原处。需要双缓冲来消除它,谷歌“c++ gdi+双缓冲”以获得良好的命中率。 @JesperJuhl:那么...您建议使用哪种 C++ 工具来实现与平台无关的 GUI?如果你不能说出一个,是什么让选择正确的工具成为你标记“不好的做法” @IInspectable 我的首选是 Qt、SFML 和 SDL。很可能是Qt。希望这算作“命名”。 @JesperJuhl:因此,您建议不要为仅针对 Windows 的工具编写特定于 Windows 的代码,而是建议添加另一个依赖项,现在使其特定于 Windows 的库特定的?最近是否将过度工程传播到最佳实践?此外,Qt 是一个玩具,它永远无法生成与本机 Windows 桌面 GUI 相媲美的 GUI。它的实现在核心上被破坏了,无法修复。您可以通过使用其键盘界面轻松识别 Qt 应用程序,然后等待它中断。它会,不可避免地。总是。无一例外。 我的 $.02 - Qt 对我来说太重了,除非你真的非常需要它。 【参考方案1】:

您需要使用缓冲区来绘制。你可以创建一个内存dc,或者使用BeginBufferedPaint

#include <uxtheme.h>
#pragma comment (lib, "uxtheme.lib")
...

case WM_PAINT:

    hdc = BeginPaint(hwnd, &ps);
    RECT rc;
    GetClientRect(hwnd, &rc);
    HDC memdc;
    auto hbuff = BeginBufferedPaint(hdc, &rc, BPBF_COMPATIBLEBITMAP, NULL, &memdc);
    Paint(memdc);
    EndBufferedPaint(hbuff, TRUE);
    EndPaint(hwnd, &ps);
    break;

这应该可以解决闪烁问题。我建议删除计时器并改为在鼠标移动中更新绘画:

case WM_MOUSEMOVE:
    InvalidateRect(hwnd, NULL, NULL);
    break;

您可能还想用SetLayeredWindowAttributes 探索WS_EX_LAYERED 标志,这将创建一个透明窗口,显示其下方的桌面。简单的文本绘制实际上并不需要 GDI+。

此外,Gdiplus 的大多数类都有不同的构造函数,这可以让您避免使用new/delete。示例:

void DrawCursorCoords(Gdiplus::Graphics &graphics, Gdiplus::Bitmap&, Gdiplus::Color c)

    POINT cursorPos;
    GetCursorPos(&cursorPos);

    std::wstring x = std::to_wstring(cursorPos.x);
    std::wstring y = std::to_wstring(cursorPos.y);

    Gdiplus::Font font(L"Consolas", (Gdiplus::REAL)16);
    Gdiplus::SolidBrush brush(c);

    graphics.DrawString(x.c_str(), (int)x.length(), &font,
        Gdiplus::PointF((Gdiplus::REAL)cursorPos.x, (Gdiplus::REAL)cursorPos.y),
        &brush);

    graphics.DrawString(y.c_str(), (int)y.length(), &font,
        Gdiplus::PointF((Gdiplus::REAL)cursorPos.x, (Gdiplus::REAL)(cursorPos.y + 16)),
        &brush);


void Paint(HDC &hdc)

    Gdiplus::Graphics gfx(hdc);
    Gdiplus::Bitmap bmap(hbm, (HPALETTE)0);

    gfx.DrawImage(&bmap, 0, 0,
        GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));

    if(GetAsyncKeyState(VK_LBUTTON))
        DrawCursorCoords(gfx, bmap, (Gdiplus::Color)Gdiplus::Color::Red);
    else
        DrawCursorCoords(gfx, bmap, (Gdiplus::Color)Gdiplus::Color::Green);

或者您可以声明Gdiplus::Bitmap *bmap = new Gdiplus::Bitmap(hbm, NULL);,这会复制hbm,因此您可以通过将bmap 声明为全局来提高效率,并且只创建/销毁一次。

ReleaseDC(hwndDesktop, hdcDesktop);
DeleteDC(hdcDesktop); //<- not required

DeleteDC(hdcDesktop) 不是必需的。 hdcDesktop 来自GetDC,已被ReleaseDC 清理

hbm = CreateCompatibleBitmap(...)
SelectObject(hdcCapture, hbm);
...
DeleteObject(hbm);

您还应该按如下方式恢复旧位图:

 hbm = CreateCompatibleBitmap(...)
 auto oldbitmap = SelectObject(hdcCapture, hbm);
 ...
 //cleanup
 SelectObject(hdcCapture, oldbitmap);
 DeleteObject(hbm);

虽然如果不恢复旧位图,windows无论如何都会尝试修复错误,所以大部分时间不会有任何问题。

【讨论】:

以上是关于GDI+ 闪烁的主要内容,如果未能解决你的问题,请参考以下文章

win32-gdi系统驱动的WM_PAINT无闪烁吗?

GDI双缓冲绘图

C# WinForm 当窗体控件图片过多时,切换界面的显示会发生闪烁,该怎么取消闪烁。(注:双缓冲开启了)

离屏绘图 GDI+

在 Qt 中通过 Windows GDI 绘图

GDI双缓冲的一些学习