4
\$\begingroup\$

(See the follow-up question.)

I have this program that shows a small window and that window shows a rectangle whose color is the same as the color of the pixel under the mouse cursor. Note that it will show the color of the mouse cursor pixel even outside of its native window. As additional fun, I have coded the clipboard support: when the user presses Ctrl+C, the RGB value of the current color will be copied to the clipboard. All works fine, but when I try to resize the window, it lags.

What do you think?

#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>
#include <stdio.h>

const int WIDTH = 300;
const int HEIGHT = 200;
const int TEXT_LINE_HEIGHT = 30;

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam);
VOID WINAPI CopyToClipboard(HWND hwnd);

HWND hwnd;
BOOL ctrlKeyDown = FALSE;
POINT prevPoint;

int WINAPI wWinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    PWSTR pCmdLine,
    int nCmdShow) {
    prevPoint.x = -1;
    prevPoint.y = -1;
    const wchar_t CLASS_NAME[] = L"GrabPixlMainWndw";

    WNDCLASSEX wc = {};

    wc.cbSize = sizeof(wc);
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClassEx(&wc);

    int screenWidth = GetSystemMetrics(SM_CXFULLSCREEN);
    int screenHeight = GetSystemMetrics(SM_CYFULLSCREEN);

    hwnd = CreateWindowEx(
        0,
        CLASS_NAME,
        L"GrabPixel 1.6",
        WS_OVERLAPPEDWINDOW,
        (screenWidth - WIDTH) / 2,
        (screenHeight - HEIGHT) / 2,
        WIDTH,
        HEIGHT,
        NULL,
        NULL,
        hInstance,
        NULL);

    if (hwnd == NULL) {
        return 0;
    }

    HHOOK mouseHook =
        SetWindowsHookExA(
            WH_MOUSE_LL,
            LowLevelMouseProc,
            hInstance,
            0);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg = {};

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

    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;

        case WM_KEYDOWN:
            if (wParam == VK_CONTROL) {
                ctrlKeyDown = TRUE;
            } else if (wParam == 0x43) {
                if (ctrlKeyDown) {
                    CopyToClipboard(hwnd);
                }
            }

            break;

        case WM_KEYUP:
            if (wParam == VK_CONTROL) {
                ctrlKeyDown = FALSE;
            }

            break;

        case WM_PAINT: {
            PAINTSTRUCT ps;
            RECT rect;
            HFONT hFont;
            wchar_t buffer[10];

            HDC hdc = BeginPaint(hwnd, &ps);
            COLORREF color = (COLORREF)wParam;
            HBRUSH brush = CreateSolidBrush(color);

            GetClientRect(hwnd, &rect);
            rect.bottom -= TEXT_LINE_HEIGHT;

            // Draw the color rectangle:
            hdc = GetDC(hwnd);
            FillRect(hdc, &rect, brush);
            DeleteObject(brush);

            // Draw the RGB rectangle:
            brush = CreateSolidBrush(RGB(255, 255, 255));
            rect.top = rect.bottom;
            rect.bottom += TEXT_LINE_HEIGHT;
            FillRect(hdc, &rect, brush);
            DeleteObject(brush);

            // Print the RGB value of the current pixel:
            wsprintf(buffer,
                L"#%02x%02x%02x",
                GetRValue(color),
                GetGValue(color),
                GetBValue(color));

            hFont = CreateFont(30,
                16,
                0,
                0,
                FW_BOLD,
                FALSE,
                FALSE,
                FALSE,
                ANSI_CHARSET,
                OUT_DEFAULT_PRECIS,
                CLIP_DEFAULT_PRECIS,
                ANTIALIASED_QUALITY,
                FF_MODERN,
                L"Monospaced");

            SetTextColor(hdc, RGB(0, 0, 0));
            SelectObject(hdc, hFont);
            TextOut(hdc, 
                    5,
                    rect.bottom - TEXT_LINE_HEIGHT,
                    buffer, 
                    7);

            ReleaseDC(hwnd, hdc);
            return 0;
        }
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (wParam == WM_MOUSEMOVE) {
        POINT p;

        if (GetCursorPos(&p)) {
            if (p.x == prevPoint.x && p.y == prevPoint.y) {
                // No new pixel:
                return 0;
            }

            prevPoint.x = p.x;
            prevPoint.y = p.y;

            HDC hdc = GetDC(NULL);

            if (hdc != NULL) {
                COLORREF color = GetPixel(hdc, p.x, p.y);
                PostMessage(hwnd, WM_PAINT, color, color);
            }
        }
    }

    return 0;
}

VOID WINAPI CopyToClipboard(HWND hwnd) {
    HGLOBAL hglbCopy;
    LPSTR lpstrCopy;

    if (!OpenClipboard(GetDesktopWindow())) {
        return;
    }

    EmptyClipboard();

    POINT point;

    if (GetCursorPos(&point)) {
        int x = point.x;
        int y = point.y;
        COLORREF color = GetPixel(GetDC(NULL), x, y);
        int r = GetRValue(color);
        int g = GetGValue(color);
        int b = GetBValue(color);

        char buffer[10];
        sprintf_s(buffer, "#%02x%02x%02x", r, g, b);
        HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, 10);

        if (!hGlobal) {
            CloseClipboard();
            return;
        }

        memcpy(GlobalLock(hGlobal), buffer, 10);
        GlobalUnlock(hGlobal);
        SetClipboardData(CF_TEXT, hGlobal);
        CloseClipboard();
        GlobalFree(hGlobal);
    }
}
```
\$\endgroup\$
1
  • \$\begingroup\$ Above, I made a minor optimization mistake: I had to cache the previous color, not the previous pixel coordinates. :( \$\endgroup\$
    – coderodde
    Feb 9, 2022 at 11:41

2 Answers 2

5
\$\begingroup\$
COLORREF color = GetPixel(GetDC(NULL), x, y); <- resource leak

Each call to GetDC should be cleaned up by ReleaseDC, otherwise we get GDI resource leak. You can monitor this GDI resource leak in Windows Task Manager (under "Details" tab, right-click on the list column header, and select to show "GDI objects") See also documentation for GetDC

A call to BeginPaint should be cleaned up with EndPaint (we don't want to call GetDC/ReleaseDC in between)

HDC hdc = BeginPaint(hwnd, &ps);
//use hdc...
EndPaint(hwnd, &ps);

Font created by CreateFont also needs cleanup with DeleteObject. You must also save the old font and restore it (see suggested code).

Use InvalidateRect, instead of PostMessage(main_wnd, WM_PAINT, 0, 0); to update paint. WM_PAINT message is not for sending messages, it is used for processing the message. This, and the resource leaks, are likely the cause of lag in resizing.

Use GetKeyState(VK_CONTROL) to see if control key is pushed.

LowLevelMouseProc should return CallNextHookEx if it does not process the message.

We can also handle WM_SIZE to update the window on resizing.

Error handling for functions such as GetCursorPos is not necessary. This function fails only if the parameter is incorrect, or there is something seriously wrong, in which case the program will crash anyway.

Make sure process is DPI aware, otherwise mouse position will be off, and GetPixel returns the wrong color.

Suggested changes:

#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>
#include <stdio.h>

HWND main_wnd;
HHOOK mouseHook;
COLORREF color = 0xffffffff;

void CopyToClipboard(HWND hwnd)
{
    if (!OpenClipboard(hwnd))
        return;
    EmptyClipboard();

    int len = 10;
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len);
    if (!hMem) return;
    char* buffer = GlobalLock(hMem);
    if (!buffer) return;
    sprintf_s(buffer, len, "#%02x%02x%02x",
        GetRValue(color), GetGValue(color), GetBValue(color));
    GlobalUnlock(buffer);

    SetClipboardData(CF_TEXT, hMem);
    CloseClipboard();
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 
{
    switch (uMsg) 
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        const int TEXT_LINE_HEIGHT = 30;

        if ((GetKeyState(VK_LBUTTON) & 0x8000) == 0)//mouse is up
        {
            POINT pt;
            GetCursorPos(&pt);
            HDC hdc_desktop = GetDC(0);
            COLORREF clr = GetPixel(hdc_desktop, pt.x, pt.y);
            ReleaseDC(0, hdc_desktop);
            color = clr;
        }

        RECT rect;
        GetClientRect(hwnd, &rect);
        rect.bottom -= TEXT_LINE_HEIGHT;

        HBRUSH brush = CreateSolidBrush(color);
        FillRect(hdc, &rect, brush);
        DeleteObject(brush);

        // Draw the RGB rectangle:
        brush = CreateSolidBrush(RGB(255, 255, 255));
        rect.top = rect.bottom;
        rect.bottom += TEXT_LINE_HEIGHT;
        FillRect(hdc, &rect, brush);
        DeleteObject(brush);

        wchar_t buffer[20];
        wsprintf(buffer, L"#%02x%02x%02x",
            GetRValue(color), GetGValue(color), GetBValue(color));

        SetTextColor(hdc, RGB(0, 0, 0));
        HFONT hFont = CreateFont(30, 0, 0, 0, FW_BOLD, 0, 0, 0,
            0, 0, 0, ANTIALIASED_QUALITY, 0, L"Monospaced");
        HFONT oldfont = (HFONT)SelectObject(hdc, hFont);
        TextOut(hdc, 5, rect.bottom - TEXT_LINE_HEIGHT, buffer, 7);
        SelectObject(hdc, oldfont);
        DeleteObject(hFont);

        EndPaint(hwnd, &ps);
        return 0;
    }

    case WM_KEYDOWN:
        //Ctrl+C:
        if ((GetKeyState(VK_CONTROL) & 0x8000) == 0x8000 && wParam == 'C') 
            CopyToClipboard(hwnd);
        return 0;

    case WM_SIZE:
        InvalidateRect(hwnd, NULL, FALSE);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) 
{
    if (wParam == WM_MOUSEMOVE)
    {
        int is_mouse_down = (GetKeyState(VK_LBUTTON) & 0x8000) != 0;
        if(!is_mouse_down)
            InvalidateRect(main_wnd, 0, 0);
    }
    return CallNextHookEx(mouseHook, nCode, wParam, lParam);
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE prev, PWSTR cmd, int show)
{
    const wchar_t CLASS_NAME[] = L"GrabPixlMainWndw";

    WNDCLASSEX wc = { 0 };
    wc.cbSize = sizeof(wc);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;
    RegisterClassEx(&wc);

    int screenWidth = GetSystemMetrics(SM_CXFULLSCREEN);
    int screenHeight = GetSystemMetrics(SM_CYFULLSCREEN);

    const int w = 300;
    const int h = 200;
    main_wnd = CreateWindowEx(0, CLASS_NAME, L"GrabPixel 1.6",
        WS_OVERLAPPEDWINDOW, (screenWidth - w) / 2, (screenHeight - h) / 2,
        w, h, NULL, NULL, hInstance, NULL);
    ShowWindow(main_wnd, show);
    UpdateWindow(main_wnd);

    mouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, hInstance, 0);

    MSG msg = { 0 };
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}
\$\endgroup\$
2
  • \$\begingroup\$ What is the w32.h? \$\endgroup\$
    – coderodde
    Feb 9, 2022 at 14:45
  • \$\begingroup\$ That was my personal file, I forgot to remove it. I updated the code to use InvalidateRect (instead of PostMessage) \$\endgroup\$ Feb 9, 2022 at 14:56
2
\$\begingroup\$

Some quick observations:

  1. It does not work correctly with scaling enabled in Windows.

  2. There are two unused variables, hglbCopy and lpstrCopy

  3. Every call to GetDC must be followed by ReleaseDC.

I'll do a more detailed review a bit later when I get some time.

\$\endgroup\$
2
  • \$\begingroup\$ +1 for mentioning scaling. \$\endgroup\$
    – coderodde
    Feb 10, 2022 at 16:24
  • 1
    \$\begingroup\$ Using the MSLLHOOKSTRUCT inside LowLevelMouseProc should sort out the scaling. Here is an example: TIO \$\endgroup\$
    – jdt
    Feb 10, 2022 at 20:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.