GDI+ мигает

Итак, я пытаюсь сделать дешевую копию 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;
}

person Kordnak    schedule 13.07.2018    source источник
comment
Именно вызов DrawImage() вызывает мерцание. Он закрашивает ранее нарисованный текст, а затем DrawCursorCoords() возвращает его обратно. Для устранения этого требуется двойная буферизация, двойная буферизация google c++ gdi+ для хороших попаданий.   -  person Hans Passant    schedule 13.07.2018
comment
@JesperJuhl: Итак ... какое средство C ++ вы бы предложили использовать для реализации независимого от платформы графического интерфейса? Если вы не можете назвать ни одного, что делает выбор правильного инструмента чем-то, что вы бы назвали плохой практикой?   -  person IInspectable    schedule 13.07.2018
comment
@IInspectable Моим первым выбором были бы Qt, SFML и SDL. Скорее всего Qt. Надеюсь, это считается именем одного.   -  person Jesper Juhl    schedule 13.07.2018
comment
@JesperJuhl: Итак, вместо того, чтобы писать специфичный для Windows код для инструмента, предназначенного только для Windows, вы предлагаете добавить еще одну зависимость, а теперь сделать его специфичным для Windows и для конкретной библиотеки? Распространялся ли в последнее время избыточный инжиниринг на лучшие практики? Кроме того, Qt — это игрушка, которая никогда не сможет создать графический интерфейс, сравнимый с родным графическим интерфейсом рабочего стола Windows. Его реализация сломана в ядре и не может быть исправлена. Вы можете легко идентифицировать приложение Qt, используя его клавиатурный интерфейс, и ждать, пока оно не сломается. Это неизбежно. Всегда. Без исключения.   -  person IInspectable    schedule 13.07.2018
comment
@IInspectable Я вижу, что мы не согласны с некоторыми вещами. Тут спорить не буду. Я упомянул, что я сделал бы, вы сказали, что вы сделали бы. Оставим это на этом.   -  person Jesper Juhl    schedule 13.07.2018
comment
@JesperJuhl: неверно. Вы прямо заявили, что написание специфичного для Windows кода для приложения, ориентированного на Windows, каким-то образом подпадает под плохие практики. Я рад, что вы удалили этот комментарий.   -  person IInspectable    schedule 13.07.2018
comment
Мой $ 0,02 - Qt слишком тяжеловесен для меня, если только он вам действительно не нужен.   -  person Paul Sanders    schedule 13.07.2018


Ответы (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;

Вы также можете изучить флаг WS_EX_LAYERED с помощью SetLayeredWindowAttributes, это создаст прозрачное окно, в котором будет отображаться рабочий стол под ним. Для простого рисования текста 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 все равно попытается исправить ошибку, поэтому в большинстве случаев проблем не возникнет.

person Barmak Shemirani    schedule 13.07.2018