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 | |
| download | color-picker-8bbdb38f930414329b06f3d4aa71ebd119403b50.tar.gz | |
| -rw-r--r-- | Makefile | 25 | ||||
| -rw-r--r-- | README.md | 40 | ||||
| -rw-r--r-- | color-picker.c | 632 | ||||
| -rw-r--r-- | demo.png | bin | 0 -> 76786 bytes | |||
| -rw-r--r-- | icon.rc | 1 |
5 files changed, 698 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..70f8812 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +CC = gcc +WINDRES = windres +RUNNER = + +CFLAGS = -mwindows -municode +LIBS = -lgdi32 -luser32 -lshell32 -ladvapi32 -lm + +all: ColorPicker.exe + +ColorPicker-noico.exe: color-picker.c + $(CC) $< -o $@ $(CFLAGS) $(LIBS) + +rainbow.ico: ColorPicker-noico.exe + $(RUNNER) ./ColorPicker-noico.exe --make-ico + +icon.res: icon.rc rainbow.ico + $(WINDRES) $< -O coff -o $@ + +ColorPicker.exe: color-picker.c icon.res + $(CC) color-picker.c icon.res -o $@ $(CFLAGS) $(LIBS) + +clean: + rm -f ColorPicker-noico.exe rainbow.ico icon.res ColorPicker.exe + +.PHONY: all clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..a48b089 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Tray Color Picker + + + +A tiny, single-file Windows color picker that lives in the system tray. Click the tray icon to zoom in on any pixel, then pick it. The color hex code is copied to the clipboard. + +## Building + +Requires a MinGW-w64 toolchain (gcc, windres, make) — e.g. [MSYS2](https://www.msys2.org/). + +### With make + +``` +make +``` + +This produces `ColorPicker.exe` with the icon embedded. + +### Manual build + +Without the icon: + +``` +gcc color-picker.c -o ColorPicker.exe -mwindows -municode -lgdi32 -luser32 -lshell32 -ladvapi32 -lm +``` + +With the icon: + +``` +gcc color-picker.c -o ColorPicker-noico.exe -mwindows -municode -lgdi32 -luser32 -lshell32 -ladvapi32 -lm +./ColorPicker-noico.exe --make-ico +windres icon.rc -O coff -o icon.res +gcc color-picker.c icon.res -o ColorPicker.exe -mwindows -municode -lgdi32 -luser32 -lshell32 -ladvapi32 -lm +``` + +## Usage + +Run `ColorPicker.exe`. A tray icon appears. Click it to pick a color. + +Right-click the tray icon for settings and recent colors. 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; +} diff --git a/demo.png b/demo.png Binary files differnew file mode 100644 index 0000000..2a442ee --- /dev/null +++ b/demo.png @@ -0,0 +1 @@ +1 ICON "rainbow.ico" |
