diff options
| author | historia <historiavg@proton.me> | 2026-09-04 15:55:37 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-09-04 15:55:37 -0400 |
| commit | 8bbdb38f930414329b06f3d4aa71ebd119403b50 (patch) | |
| tree | d357dcc2d486e8e4cba435a6ce3c9c10e3fe087f /color-picker.c | |
| download | color-picker-main.tar.gz | |
Diffstat (limited to 'color-picker.c')
| -rw-r--r-- | color-picker.c | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/color-picker.c b/color-picker.c new file mode 100644 index 0000000..a15b850 --- /dev/null +++ b/color-picker.c @@ -0,0 +1,632 @@ +// Tray Color Picker - single-file Win32 app +#define UNICODE +#define _UNICODE +#include <windows.h> +#include <shellapi.h> +#include <stdio.h> +#include <math.h> + +#define WM_TRAYICON (WM_APP + 1) +#define ID_TRAY 1 +#define IDM_PICK 100 +#define IDM_EXIT 101 +#define IDM_CLEARHIST 102 +#define IDM_HISTORY0 200 // 200..219 +#define HISTORY_CAP 20 // absolute max storage +// settings menu ids +#define IDM_ZOOM_BASE 300 +#define IDM_CELL_BASE 320 +#define IDM_INFO_HEX 340 +#define IDM_INFO_RGB 341 +#define IDM_INFO_BOTH 342 +#define IDM_GRID 350 +#define IDM_COPY_HEX 360 // "#RRGGBB" +#define IDM_COPY_RGB 361 // "rgb(r, g, b)" +#define IDM_COPY_RAW 362 // "RRGGBB" +#define IDM_COPY_NONE 363 // don't copy +#define IDM_HSIZE_BASE 380 // + index into HSIZES + +static const int ZOOMS[] = { 4, 8, 12, 16 }; +static const int CELLS[] = { 11, 15, 21, 27 }; +static const int HSIZES[] = { 0, 5, 10, 20 }; +#define NZOOMS 4 +#define NCELLS 4 +#define NHSIZES 4 + +// ---- settings (persisted) ---- +static int g_zoom = 8; +static int g_cells = 15; +static int g_infoMode = 2; // 0=hex, 1=rgb, 2=both +static BOOL g_grid = FALSE; +static int g_copyFmt = 0; // 0=#RRGGBB, 1=rgb(), 2=RRGGBB, 3=none +static int g_histSize = 10; // 0, 5, 10, 20 + +static HWND g_hwndMain, g_hwndLoupe; +static HHOOK g_mouseHook, g_kbdHook; +static BOOL g_picking = FALSE; +static COLORREF g_history[HISTORY_CAP]; +static int g_historyCount = 0; +static NOTIFYICONDATA g_nid = {0}; +static HDC g_screenDC; +static HFONT g_fontBig, g_fontSmall, g_fontMenu; + +// ---------------- settings persistence ---------------- +static void RegGetInt(HKEY k, const wchar_t *name, int *out) { + DWORD v, sz = sizeof(DWORD); + if (RegQueryValueEx(k, name, NULL, NULL, (BYTE*)&v, &sz) == ERROR_SUCCESS) *out = (int)v; +} + +static void LoadSettings(void) { + HKEY k; + if (RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\TrayColorPicker", 0, KEY_READ, &k) != ERROR_SUCCESS) return; + int g = g_grid; + RegGetInt(k, L"Zoom", &g_zoom); + RegGetInt(k, L"Cells", &g_cells); + RegGetInt(k, L"InfoMode", &g_infoMode); + RegGetInt(k, L"Grid", &g); g_grid = (BOOL)g; + RegGetInt(k, L"CopyFmt", &g_copyFmt); + RegGetInt(k, L"HistSize", &g_histSize); + if (g_histSize < 0 || g_histSize > HISTORY_CAP) g_histSize = 10; + RegCloseKey(k); +} + +static void SaveSettings(void) { + HKEY k; + if (RegCreateKeyEx(HKEY_CURRENT_USER, L"Software\\TrayColorPicker", 0, NULL, 0, + KEY_WRITE, NULL, &k, NULL) != ERROR_SUCCESS) return; + DWORD v; + v = g_zoom; RegSetValueEx(k, L"Zoom", 0, REG_DWORD, (BYTE*)&v, sizeof(v)); + v = g_cells; RegSetValueEx(k, L"Cells", 0, REG_DWORD, (BYTE*)&v, sizeof(v)); + v = g_infoMode; RegSetValueEx(k, L"InfoMode", 0, REG_DWORD, (BYTE*)&v, sizeof(v)); + v = g_grid; RegSetValueEx(k, L"Grid", 0, REG_DWORD, (BYTE*)&v, sizeof(v)); + v = g_copyFmt; RegSetValueEx(k, L"CopyFmt", 0, REG_DWORD, (BYTE*)&v, sizeof(v)); + v = g_histSize; RegSetValueEx(k, L"HistSize", 0, REG_DWORD, (BYTE*)&v, sizeof(v)); + RegCloseKey(k); +} + +// ---------------- geometry ---------------- +static int LoupePx(void) { return g_zoom * g_cells; } +static int InfoH(void) { return (g_infoMode == 2) ? 46 : 30; } +static int LoupeW(void) { int w = LoupePx() + 2; return w < 168 ? 168 : w; } +static int LoupeH(void) { return LoupePx() + InfoH() + 2; } + +// ---------------- rainbow icon (in-memory + .ico export) ---------------- +static void HsvToRgb(double h, double s, double v, BYTE *r, BYTE *g, BYTE *b) { + double c = v * s, hp = h / 60.0; + double x = c * (1.0 - fabs(fmod(hp, 2.0) - 1.0)); + double rr = 0, gg = 0, bb = 0; + if (hp < 1) { rr = c; gg = x; } + else if (hp < 2) { rr = x; gg = c; } + else if (hp < 3) { gg = c; bb = x; } + else if (hp < 4) { gg = x; bb = c; } + else if (hp < 5) { rr = x; bb = c; } + else { rr = c; bb = x; } + double m = v - c; + *r = (BYTE)((rr + m) * 255.0); *g = (BYTE)((gg + m) * 255.0); *b = (BYTE)((bb + m) * 255.0); +} + +// fill a top-down 32bpp ARGB pixel buffer with the rainbow wheel +static void FillRainbowPixels(DWORD *px, int size) { + double cx = (size - 1) / 2.0, cy = (size - 1) / 2.0; + double radius = size / 2.0 - 1.0; + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + double dx = x - cx, dy = y - cy; + double dist = sqrt(dx*dx + dy*dy); + DWORD out = 0; + if (dist <= radius + 0.5) { + double hue = fmod(atan2(dy, dx) * 180.0 / 3.14159265358979 + 360.0, 360.0); + double sat = dist / radius; if (sat > 1.0) sat = 1.0; + BYTE r, g, b; + HsvToRgb(hue, sat, 1.0, &r, &g, &b); + double a = (radius + 0.5 - dist); if (a > 1.0) a = 1.0; if (a < 0.0) a = 0.0; + BYTE alpha = (BYTE)(a * 255.0); + if (alpha < 255) { r = (BYTE)(r * a); g = (BYTE)(g * a); b = (BYTE)(b * a); } + out = ((DWORD)alpha << 24) | ((DWORD)r << 16) | ((DWORD)g << 8) | b; + } + px[y * size + x] = out; + } + } +} + +static HICON CreateRainbowIcon(int size) { + BITMAPV5HEADER bi = {0}; + bi.bV5Size = sizeof(bi); + bi.bV5Width = size; bi.bV5Height = -size; + bi.bV5Planes = 1; bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x00FF0000; bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x000000FF; bi.bV5AlphaMask = 0xFF000000; + + void *bits = NULL; + HDC hdc = GetDC(NULL); + HBITMAP color = CreateDIBSection(hdc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, &bits, NULL, 0); + ReleaseDC(NULL, hdc); + if (!color) return NULL; + FillRainbowPixels((DWORD*)bits, size); + + HBITMAP mask = CreateBitmap(size, size, 1, 1, NULL); + ICONINFO ii = {0}; + ii.fIcon = TRUE; ii.hbmColor = color; ii.hbmMask = mask; + HICON icon = CreateIconIndirect(&ii); + DeleteObject(color); DeleteObject(mask); + return icon; +} + +// write rainbow.ico with 16/32/48/256 px images (for embedding via windres) +static BOOL WriteRainbowIco(const wchar_t *path) { + static const int sizes[] = { 16, 32, 48, 256 }; + const int n = 4; + FILE *f = _wfopen(path, L"wb"); + if (!f) return FALSE; + + // ICONDIR + WORD zero = 0, one = 1, count = (WORD)n; + fwrite(&zero, 2, 1, f); fwrite(&one, 2, 1, f); fwrite(&count, 2, 1, f); + + DWORD offset = 6 + 16 * n; + // write directory entries + for (int i = 0; i < n; i++) { + int s = sizes[i]; + DWORD imgSize = 40 + s*s*4 + ((s+31)/32)*4 * s; // BITMAPINFOHEADER + XOR + AND mask + BYTE w = (s == 256) ? 0 : (BYTE)s, h = w; + BYTE colors = 0, res = 0; + WORD planes = 1, bpp = 32; + fwrite(&w, 1, 1, f); fwrite(&h, 1, 1, f); + fwrite(&colors, 1, 1, f); fwrite(&res, 1, 1, f); + fwrite(&planes, 2, 1, f); fwrite(&bpp, 2, 1, f); + fwrite(&imgSize, 4, 1, f); fwrite(&offset, 4, 1, f); + offset += imgSize; + } + // write images + for (int i = 0; i < n; i++) { + int s = sizes[i]; + BITMAPINFOHEADER bih = {0}; + bih.biSize = 40; bih.biWidth = s; bih.biHeight = s * 2; // *2: XOR+AND convention + bih.biPlanes = 1; bih.biBitCount = 32; bih.biCompression = BI_RGB; + fwrite(&bih, 40, 1, f); + + DWORD *px = (DWORD*)malloc(s * s * 4); + if (!px) { fclose(f); return FALSE; } + FillRainbowPixels(px, s); + // ICO stores rows bottom-up + for (int y = s - 1; y >= 0; y--) fwrite(&px[y * s], 4, s, f); + free(px); + // AND mask: all zero (alpha channel handles transparency) + int maskRow = ((s + 31) / 32) * 4; + BYTE *zeros = (BYTE*)calloc(maskRow, 1); + for (int y = 0; y < s; y++) fwrite(zeros, 1, maskRow, f); + free(zeros); + } + fclose(f); + return TRUE; +} + +// ---------------- clipboard / notifications ---------------- +static void FormatColor(COLORREF c, wchar_t *buf, int n) { + if (g_copyFmt == 1) swprintf(buf, n, L"rgb(%d, %d, %d)", GetRValue(c), GetGValue(c), GetBValue(c)); + else if (g_copyFmt == 2) swprintf(buf, n, L"%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c)); + else swprintf(buf, n, L"#%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c)); +} + +static void CopyColorToClipboard(COLORREF c, HWND owner) { + if (g_copyFmt == 3) return; // None + wchar_t buf[32]; + FormatColor(c, buf, 32); + if (OpenClipboard(owner)) { + EmptyClipboard(); + size_t bytes = (wcslen(buf) + 1) * sizeof(wchar_t); + HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, bytes); + if (h) { + memcpy(GlobalLock(h), buf, bytes); + GlobalUnlock(h); + SetClipboardData(CF_UNICODETEXT, h); + } + CloseClipboard(); + } +} + +static void ShowBalloon(COLORREF c) { + wchar_t fmt[32]; + swprintf(fmt, 32, L"#%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c)); + if (g_copyFmt == 3) + swprintf(g_nid.szInfo, 256, L"%s (RGB %d, %d, %d)", + fmt, GetRValue(c), GetGValue(c), GetBValue(c)); + else { + wchar_t copied[32]; FormatColor(c, copied, 32); + swprintf(g_nid.szInfo, 256, L"%s copied to clipboard (RGB %d, %d, %d)", + copied, GetRValue(c), GetGValue(c), GetBValue(c)); + } + wcscpy(g_nid.szInfoTitle, L"Color picked"); + g_nid.uFlags = NIF_INFO; + g_nid.dwInfoFlags = NIIF_INFO | NIIF_NOSOUND; + Shell_NotifyIcon(NIM_MODIFY, &g_nid); +} + +static void AddHistory(COLORREF c) { + if (g_histSize == 0) return; + for (int i = g_histSize - 1; i > 0; i--) g_history[i] = g_history[i-1]; + g_history[0] = c; // newest at top + if (g_historyCount < g_histSize) g_historyCount++; +} + +// ---------------- picking ---------------- +static void PositionLoupe(POINT pt) { + int w = LoupeW(), h = LoupeH(); + int x = pt.x + 24, y = pt.y + 24; + RECT wa; SystemParametersInfo(SPI_GETWORKAREA, 0, &wa, 0); + if (x + w > wa.right) x = pt.x - w - 24; + if (y + h > wa.bottom) y = pt.y - h - 24; + SetWindowPos(g_hwndLoupe, HWND_TOPMOST, x, y, w, h, SWP_NOACTIVATE | SWP_SHOWWINDOW); + InvalidateRect(g_hwndLoupe, NULL, FALSE); +} + +static void StopPicking(void) { + if (!g_picking) return; + g_picking = FALSE; + if (g_mouseHook) { UnhookWindowsHookEx(g_mouseHook); g_mouseHook = NULL; } + if (g_kbdHook) { UnhookWindowsHookEx(g_kbdHook); g_kbdHook = NULL; } + ShowWindow(g_hwndLoupe, SW_HIDE); +} + +static void PickColorAt(POINT pt) { + COLORREF c = GetPixel(g_screenDC, pt.x, pt.y); + StopPicking(); + AddHistory(c); + CopyColorToClipboard(c, g_hwndMain); + ShowBalloon(c); +} + +static LRESULT CALLBACK MouseHook(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode == HC_ACTION && g_picking) { + MSLLHOOKSTRUCT *m = (MSLLHOOKSTRUCT*)lParam; + if (wParam == WM_MOUSEMOVE) { + PositionLoupe(m->pt); + } else if (wParam == WM_LBUTTONDOWN) { + PickColorAt(m->pt); + return 1; + } else if (wParam == WM_LBUTTONUP) { + return 1; + } else if (wParam == WM_RBUTTONDOWN || wParam == WM_RBUTTONUP) { + StopPicking(); + return 1; + } + } + return CallNextHookEx(NULL, nCode, wParam, lParam); +} + +static LRESULT CALLBACK KbdHook(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode == HC_ACTION && g_picking && wParam == WM_KEYDOWN) { + KBDLLHOOKSTRUCT *k = (KBDLLHOOKSTRUCT*)lParam; + if (k->vkCode == VK_ESCAPE) { StopPicking(); return 1; } + } + return CallNextHookEx(NULL, nCode, wParam, lParam); +} + +static void StartPicking(void) { + if (g_picking) return; + g_picking = TRUE; + g_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHook, GetModuleHandle(NULL), 0); + g_kbdHook = SetWindowsHookEx(WH_KEYBOARD_LL, KbdHook, GetModuleHandle(NULL), 0); + POINT pt; GetCursorPos(&pt); + PositionLoupe(pt); +} + +// ---------------- loupe painting ---------------- +static void PaintLoupe(HWND hwnd) { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + RECT rc; GetClientRect(hwnd, &rc); + + HDC mem = CreateCompatibleDC(hdc); + HBITMAP bmp = CreateCompatibleBitmap(hdc, rc.right, rc.bottom); + HBITMAP old = SelectObject(mem, bmp); + + HBRUSH dark = CreateSolidBrush(RGB(32,32,32)); + FillRect(mem, &rc, dark); + DeleteObject(dark); + + POINT pt; GetCursorPos(&pt); + int half = g_cells / 2; + int lp = LoupePx(); + int ox = (rc.right - lp) / 2; + + SetStretchBltMode(mem, COLORONCOLOR); + StretchBlt(mem, ox, 1, lp, lp, + g_screenDC, pt.x - half, pt.y - half, g_cells, g_cells, SRCCOPY); + + if (g_grid && g_zoom >= 6) { + HPEN gpen = CreatePen(PS_SOLID, 1, RGB(70,70,70)); + HPEN op = SelectObject(mem, gpen); + for (int i = 1; i < g_cells; i++) { + MoveToEx(mem, ox + i*g_zoom, 1, NULL); LineTo(mem, ox + i*g_zoom, 1 + lp); + MoveToEx(mem, ox, 1 + i*g_zoom, NULL); LineTo(mem, ox + lp, 1 + i*g_zoom); + } + SelectObject(mem, op); DeleteObject(gpen); + } + + COLORREF c = GetPixel(g_screenDC, pt.x, pt.y); + HPEN pen = CreatePen(PS_SOLID, 2, RGB(255,60,60)); + HPEN oldPen = SelectObject(mem, pen); + SelectObject(mem, GetStockObject(NULL_BRUSH)); + Rectangle(mem, ox + half*g_zoom - 1, 1 + half*g_zoom - 1, + ox + half*g_zoom + g_zoom + 1, 1 + half*g_zoom + g_zoom + 1); + SelectObject(mem, oldPen); DeleteObject(pen); + + int iy = lp + 2; + int swSize = InfoH() - 14; + HBRUSH sw = CreateSolidBrush(c); + RECT swr = { 8, iy + 7, 8 + swSize, iy + 7 + swSize }; + FillRect(mem, &swr, sw); DeleteObject(sw); + FrameRect(mem, &swr, GetStockObject(WHITE_BRUSH)); + + wchar_t hex[16], rgb[32]; + swprintf(hex, 16, L"#%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c)); + swprintf(rgb, 32, L"RGB %d, %d, %d", GetRValue(c), GetGValue(c), GetBValue(c)); + SetBkMode(mem, TRANSPARENT); + int tx = swr.right + 8; + + if (g_infoMode == 2) { + SelectObject(mem, g_fontBig); + SetTextColor(mem, RGB(255,255,255)); + TextOut(mem, tx, iy + 5, hex, (int)wcslen(hex)); + SelectObject(mem, g_fontSmall); + SetTextColor(mem, RGB(190,190,190)); + TextOut(mem, tx, iy + 25, rgb, (int)wcslen(rgb)); + } else { + const wchar_t *s = (g_infoMode == 1) ? rgb : hex; + SelectObject(mem, g_fontBig); + SetTextColor(mem, RGB(255,255,255)); + TextOut(mem, tx, iy + (InfoH() - 20) / 2, s, (int)wcslen(s)); + } + + FrameRect(mem, &rc, GetStockObject(BLACK_BRUSH)); + BitBlt(hdc, 0, 0, rc.right, rc.bottom, mem, 0, 0, SRCCOPY); + SelectObject(mem, old); DeleteObject(bmp); DeleteDC(mem); + EndPaint(hwnd, &ps); +} + +// ---------------- menus ---------------- +static void ShowTrayMenu(HWND hwnd) { + HMENU settings = CreatePopupMenu(); + + HMENU zoomMenu = CreatePopupMenu(); + for (int i = 0; i < NZOOMS; i++) { + wchar_t b[16]; swprintf(b, 16, L"%d\u00D7", ZOOMS[i]); + AppendMenu(zoomMenu, MF_STRING | (g_zoom == ZOOMS[i] ? MF_CHECKED : 0), IDM_ZOOM_BASE + i, b); + } + AppendMenu(settings, MF_POPUP, (UINT_PTR)zoomMenu, L"Magnification"); + + HMENU cellMenu = CreatePopupMenu(); + for (int i = 0; i < NCELLS; i++) { + wchar_t b[24]; swprintf(b, 24, L"%d \u00D7 %d pixels", CELLS[i], CELLS[i]); + AppendMenu(cellMenu, MF_STRING | (g_cells == CELLS[i] ? MF_CHECKED : 0), IDM_CELL_BASE + i, b); + } + AppendMenu(settings, MF_POPUP, (UINT_PTR)cellMenu, L"Capture area"); + + HMENU infoMenu = CreatePopupMenu(); + AppendMenu(infoMenu, MF_STRING | (g_infoMode == 0 ? MF_CHECKED : 0), IDM_INFO_HEX, L"Hex only"); + AppendMenu(infoMenu, MF_STRING | (g_infoMode == 1 ? MF_CHECKED : 0), IDM_INFO_RGB, L"RGB only"); + AppendMenu(infoMenu, MF_STRING | (g_infoMode == 2 ? MF_CHECKED : 0), IDM_INFO_BOTH, L"Hex + RGB"); + AppendMenu(settings, MF_POPUP, (UINT_PTR)infoMenu, L"Readout"); + + AppendMenu(settings, MF_STRING | (g_grid ? MF_CHECKED : 0), IDM_GRID, L"Pixel grid"); + + HMENU copyMenu = CreatePopupMenu(); + AppendMenu(copyMenu, MF_STRING | (g_copyFmt == 0 ? MF_CHECKED : 0), IDM_COPY_HEX, L"#RRGGBB"); + AppendMenu(copyMenu, MF_STRING | (g_copyFmt == 2 ? MF_CHECKED : 0), IDM_COPY_RAW, L"RRGGBB"); + AppendMenu(copyMenu, MF_STRING | (g_copyFmt == 1 ? MF_CHECKED : 0), IDM_COPY_RGB, L"rgb(r, g, b)"); + AppendMenu(copyMenu, MF_STRING | (g_copyFmt == 3 ? MF_CHECKED : 0), IDM_COPY_NONE, L"None (don't copy)"); + AppendMenu(settings, MF_POPUP, (UINT_PTR)copyMenu, L"Copy format"); + + HMENU histMenu = CreatePopupMenu(); + for (int i = 0; i < NHSIZES; i++) { + wchar_t b[24]; + if (HSIZES[i] == 0) wcscpy(b, L"Off"); + else swprintf(b, 24, L"%d colors", HSIZES[i]); + AppendMenu(histMenu, MF_STRING | (g_histSize == HSIZES[i] ? MF_CHECKED : 0), IDM_HSIZE_BASE + i, b); + } + AppendMenu(settings, MF_POPUP, (UINT_PTR)histMenu, L"History size"); + + HMENU menu = CreatePopupMenu(); + AppendMenu(menu, MF_STRING, IDM_PICK, L"Pick Color"); + AppendMenu(menu, MF_POPUP, (UINT_PTR)settings, L"Settings"); + if (g_historyCount > 0) { + AppendMenu(menu, MF_SEPARATOR, 0, NULL); + // non-clickable hint header + AppendMenu(menu, MF_STRING | MF_GRAYED | MF_DISABLED, 0, L"Recent colors (newest first)"); + for (int i = 0; i < g_historyCount; i++) { + // owner-drawn: swatch is painted in WM_DRAWITEM; itemData = history index + AppendMenu(menu, MF_OWNERDRAW, IDM_HISTORY0 + i, (LPCWSTR)(UINT_PTR)i); + } + AppendMenu(menu, MF_STRING, IDM_CLEARHIST, L"Clear history"); + } + AppendMenu(menu, MF_SEPARATOR, 0, NULL); + AppendMenu(menu, MF_STRING, IDM_EXIT, L"Exit"); + + POINT pt; GetCursorPos(&pt); + SetForegroundWindow(hwnd); + TrackPopupMenu(menu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, NULL); + DestroyMenu(menu); +} + +// owner-drawn history rows: [swatch] #RRGGBB (r, g, b) +#define HITEM_H 24 +#define HITEM_W 190 + +static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + switch (msg) { + case WM_TRAYICON: + if (lParam == WM_LBUTTONUP) StartPicking(); + else if (lParam == WM_RBUTTONUP) ShowTrayMenu(hwnd); + return 0; + + case WM_MEASUREITEM: { + MEASUREITEMSTRUCT *mi = (MEASUREITEMSTRUCT*)lParam; + if (mi->CtlType == ODT_MENU) { + mi->itemWidth = HITEM_W; + mi->itemHeight = HITEM_H; + return TRUE; + } + break; + } + + case WM_DRAWITEM: { + DRAWITEMSTRUCT *di = (DRAWITEMSTRUCT*)lParam; + if (di->CtlType == ODT_MENU) { + int idx = (int)(UINT_PTR)di->itemData; + if (idx < 0 || idx >= g_historyCount) return TRUE; + COLORREF c = g_history[idx]; + BOOL sel = (di->itemState & ODS_SELECTED) != 0; + + // background + HBRUSH bg = CreateSolidBrush(sel ? GetSysColor(COLOR_HIGHLIGHT) : GetSysColor(COLOR_MENU)); + FillRect(di->hDC, &di->rcItem, bg); + DeleteObject(bg); + + // swatch with border + RECT swr = { di->rcItem.left + 6, di->rcItem.top + 4, + di->rcItem.left + 6 + (HITEM_H - 8), di->rcItem.bottom - 4 }; + HBRUSH sw = CreateSolidBrush(c); + FillRect(di->hDC, &swr, sw); + DeleteObject(sw); + FrameRect(di->hDC, &swr, (HBRUSH)GetStockObject(GRAY_BRUSH)); + + // text + wchar_t buf[48]; + swprintf(buf, 48, L"#%02X%02X%02X (%d, %d, %d)", + GetRValue(c), GetGValue(c), GetBValue(c), + GetRValue(c), GetGValue(c), GetBValue(c)); + SetBkMode(di->hDC, TRANSPARENT); + SetTextColor(di->hDC, sel ? GetSysColor(COLOR_HIGHLIGHTTEXT) : GetSysColor(COLOR_MENUTEXT)); + HFONT of = SelectObject(di->hDC, g_fontMenu); + RECT tr = di->rcItem; + tr.left = swr.right + 8; + DrawText(di->hDC, buf, -1, &tr, DT_SINGLELINE | DT_VCENTER | DT_LEFT); + SelectObject(di->hDC, of); + return TRUE; + } + break; + } + + case WM_COMMAND: { + WORD id = LOWORD(wParam); + if (id == IDM_PICK) StartPicking(); + else if (id == IDM_EXIT) DestroyWindow(hwnd); + else if (id == IDM_CLEARHIST) g_historyCount = 0; + else if (id >= IDM_ZOOM_BASE && id < IDM_ZOOM_BASE + NZOOMS) { g_zoom = ZOOMS[id - IDM_ZOOM_BASE]; SaveSettings(); } + else if (id >= IDM_CELL_BASE && id < IDM_CELL_BASE + NCELLS) { g_cells = CELLS[id - IDM_CELL_BASE]; SaveSettings(); } + else if (id == IDM_INFO_HEX) { g_infoMode = 0; SaveSettings(); } + else if (id == IDM_INFO_RGB) { g_infoMode = 1; SaveSettings(); } + else if (id == IDM_INFO_BOTH) { g_infoMode = 2; SaveSettings(); } + else if (id == IDM_GRID) { g_grid = !g_grid; SaveSettings(); } + else if (id == IDM_COPY_HEX) { g_copyFmt = 0; SaveSettings(); } + else if (id == IDM_COPY_RGB) { g_copyFmt = 1; SaveSettings(); } + else if (id == IDM_COPY_RAW) { g_copyFmt = 2; SaveSettings(); } + else if (id == IDM_COPY_NONE) { g_copyFmt = 3; SaveSettings(); } + else if (id >= IDM_HSIZE_BASE && id < IDM_HSIZE_BASE + NHSIZES) { + g_histSize = HSIZES[id - IDM_HSIZE_BASE]; + if (g_historyCount > g_histSize) g_historyCount = g_histSize; // trim + SaveSettings(); + } + else if (id >= IDM_HISTORY0 && id < IDM_HISTORY0 + HISTORY_CAP) { + COLORREF c = g_history[id - IDM_HISTORY0]; + if (g_copyFmt == 3) { + // Copy format is None; still copy hex on explicit history click + wchar_t buf[16]; + swprintf(buf, 16, L"#%02X%02X%02X", GetRValue(c), GetGValue(c), GetBValue(c)); + if (OpenClipboard(hwnd)) { + EmptyClipboard(); + size_t bytes = (wcslen(buf) + 1) * sizeof(wchar_t); + HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, bytes); + if (h) { memcpy(GlobalLock(h), buf, bytes); GlobalUnlock(h); SetClipboardData(CF_UNICODETEXT, h); } + CloseClipboard(); + } + } else { + CopyColorToClipboard(c, hwnd); + } + ShowBalloon(c); + } + return 0; + } + + case WM_DESTROY: + StopPicking(); + Shell_NotifyIcon(NIM_DELETE, &g_nid); + PostQuitMessage(0); + return 0; + } + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +static LRESULT CALLBACK LoupeProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (msg == WM_PAINT) { PaintLoupe(hwnd); return 0; } + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrev, LPWSTR cmd, int show) { + // "--make-ico" mode: write rainbow.ico next to the exe and exit + if (cmd && wcsstr(cmd, L"--make-ico")) { + if (WriteRainbowIco(L"rainbow.ico")) + MessageBox(NULL, L"rainbow.ico written to the current folder.", L"Color Picker", MB_OK | MB_ICONINFORMATION); + else + MessageBox(NULL, L"Failed to write rainbow.ico.", L"Color Picker", MB_OK | MB_ICONERROR); + return 0; + } + + SetProcessDPIAware(); + LoadSettings(); + g_screenDC = GetDC(NULL); + + g_fontBig = CreateFont(-15, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0, DEFAULT_CHARSET, + OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, + DEFAULT_PITCH, L"Segoe UI"); + g_fontSmall = CreateFont(-12, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, + OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, + DEFAULT_PITCH, L"Segoe UI"); + g_fontMenu = CreateFont(-13, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, + OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, + DEFAULT_PITCH, L"Segoe UI"); + + WNDCLASS wc = {0}; + wc.lpfnWndProc = WndProc; + wc.hInstance = hInst; + wc.lpszClassName = L"TrayColorPickerMain"; + RegisterClass(&wc); + g_hwndMain = CreateWindow(wc.lpszClassName, L"", 0, 0,0,0,0, NULL, NULL, hInst, NULL); + + WNDCLASS lc = {0}; + lc.lpfnWndProc = LoupeProc; + lc.hInstance = hInst; + lc.lpszClassName = L"TrayColorPickerLoupe"; + lc.hCursor = LoadCursor(NULL, IDC_CROSS); + RegisterClass(&lc); + g_hwndLoupe = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, + lc.lpszClassName, L"", WS_POPUP, + 0, 0, LoupeW(), LoupeH(), + NULL, NULL, hInst, NULL); + + // Prefer the embedded icon resource (id 1, added via windres); fall back to generated + HICON trayIcon = (HICON)LoadImage(hInst, MAKEINTRESOURCE(1), IMAGE_ICON, 16, 16, 0); + HICON generated = NULL; + if (!trayIcon) { generated = CreateRainbowIcon(32); trayIcon = generated; } + + g_nid.cbSize = sizeof(g_nid); + g_nid.hWnd = g_hwndMain; + g_nid.uID = ID_TRAY; + g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; + g_nid.uCallbackMessage = WM_TRAYICON; + g_nid.hIcon = trayIcon ? trayIcon : LoadIcon(NULL, IDI_APPLICATION); + wcscpy(g_nid.szTip, L"Color Picker \u2014 click to pick a color"); + Shell_NotifyIcon(NIM_ADD, &g_nid); + + MSG m; + while (GetMessage(&m, NULL, 0, 0)) { + TranslateMessage(&m); + DispatchMessage(&m); + } + if (generated) DestroyIcon(generated); + DeleteObject(g_fontBig); DeleteObject(g_fontSmall); DeleteObject(g_fontMenu); + ReleaseDC(NULL, g_screenDC); + return 0; +} |
