FancyZones_DrawLayoutTest.cpp
1 #include "framework.h" 2 #include "FancyZones_DrawLayoutTest.h" 3 4 #include <Uxtheme.h> 5 #include <objidl.h> 6 #include <gdiplus.h> 7 #include <Dwmapi.h> 8 #include <shellscalingapi.h> 9 10 #include <vector> 11 #include <thread> 12 #include <string> 13 14 using namespace Gdiplus; 15 16 #pragma comment (lib,"Gdiplus.lib") 17 #pragma comment (lib,"UXTheme.lib") 18 #pragma comment (lib,"Dwmapi.lib") 19 #pragma comment (lib,"Shcore.lib") 20 21 constexpr int ZONE_COUNT = 4; 22 constexpr int ANIMATION_TIME = 200; // milliseconds 23 constexpr int DISPLAY_REFRESH_TIME = 10; // milliseconds 24 constexpr DWORD Q_KEY_CODE = 0x51; 25 constexpr DWORD W_KEY_CODE = 0x57; 26 27 HWND mainWindow; 28 HHOOK keyboardHook; 29 bool showZoneLayout = false; 30 31 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); 32 33 std::vector<RECT> zones{}; 34 std::vector<bool> highlighted{}; 35 36 inline const int RectWidth(const RECT& rect) 37 { 38 return rect.right - rect.left; 39 } 40 41 inline const int RectHeight(const RECT& rect) 42 { 43 return rect.bottom - rect.top; 44 } 45 46 std::vector<RECT> BuildColumnZoneLayout(int zoneCount, const RECT& workArea) 47 { 48 // Builds column layout with specified number of zones (columns). 49 int zoneWidth = RectWidth(workArea) / zoneCount; 50 int zoneHeight = RectHeight(workArea); 51 std::vector<RECT> zones(zoneCount); 52 for (int i = 0; i < zoneCount; ++i) 53 { 54 int left = workArea.left + i * zoneWidth; 55 int top = workArea.top; 56 int right = left + zoneWidth; 57 int bottom = top + zoneHeight; 58 59 zones[i] = { left, top, right, bottom }; 60 } 61 return zones; 62 } 63 64 int GetHighlightedZoneIdx(const std::vector<RECT>& zones, const POINT& cursorPosition) 65 { 66 // Determine which zone should be highlighted based on cursor position. 67 for (size_t i = 0; i < zones.size(); ++i) 68 { 69 if (cursorPosition.x >= zones[i].left && cursorPosition.x < zones[i].right) 70 { 71 return static_cast<int>(i); 72 } 73 } 74 return -1; 75 } 76 77 void ShowZonesOverlay() 78 { 79 // InvalidateRect will essentially send WM_PAINT to main window. 80 UINT flags = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE; 81 SetWindowPos(mainWindow, nullptr, 0, 0, 0, 0, flags); 82 83 std::thread{ [=]() { 84 AnimateWindow(mainWindow, ANIMATION_TIME, AW_BLEND); 85 InvalidateRect(mainWindow, nullptr, true); 86 } }.detach(); 87 } 88 89 void HideZonesOverlay() 90 { 91 highlighted = std::vector<bool>(ZONE_COUNT, false); 92 ShowWindow(mainWindow, SW_HIDE); 93 } 94 95 void RefreshMainWindow() 96 { 97 while (1) 98 { 99 std::this_thread::sleep_for(std::chrono::milliseconds(DISPLAY_REFRESH_TIME)); 100 101 POINT cursorPosition{}; 102 if (GetCursorPos(&cursorPosition)) 103 { 104 if (showZoneLayout) 105 { 106 int idx = GetHighlightedZoneIdx(zones, cursorPosition); 107 if (idx != -1) 108 { 109 if (highlighted[idx]) { 110 // Same zone is active as in previous check, skip invalidating rect. 111 } 112 else 113 { 114 highlighted = std::vector<bool>(ZONE_COUNT, false); 115 highlighted[idx] = true; 116 117 ShowZonesOverlay(); 118 } 119 } 120 } 121 } 122 } 123 } 124 125 LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) 126 { 127 if (nCode == HC_ACTION && wParam == WM_KEYDOWN) 128 { 129 PKBDLLHOOKSTRUCT info = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam); 130 if (info->vkCode == Q_KEY_CODE) 131 { 132 PostQuitMessage(0); 133 return 1; 134 } 135 else if (info->vkCode == W_KEY_CODE) 136 { 137 // Toggle zone layout display. 138 showZoneLayout = !showZoneLayout; 139 if (showZoneLayout) 140 { 141 ShowZonesOverlay(); 142 } 143 else 144 { 145 HideZonesOverlay(); 146 } 147 return 1; 148 } 149 } 150 return CallNextHookEx(nullptr, nCode, wParam, lParam); 151 } 152 153 void StartLowLevelKeyboardHook() 154 { 155 keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(nullptr), 0); 156 } 157 158 void StopLowLevelKeyboardHook() 159 { 160 if (keyboardHook) 161 { 162 UnhookWindowsHookEx(keyboardHook); 163 keyboardHook = nullptr; 164 } 165 } 166 167 168 inline void MakeWindowTransparent(HWND window) 169 { 170 int const pos = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8; 171 if (HRGN hrgn{ CreateRectRgn(pos, 0, (pos + 1), 1) }) 172 { 173 DWM_BLURBEHIND bh = { DWM_BB_ENABLE | DWM_BB_BLURREGION, TRUE, hrgn, FALSE }; 174 DwmEnableBlurBehindWindow(window, &bh); 175 } 176 } 177 178 void RegisterClass(HINSTANCE hInstance) 179 { 180 WNDCLASSEXW wcex{}; 181 182 wcex.cbSize = sizeof(WNDCLASSEX); 183 wcex.lpfnWndProc = WndProc; 184 wcex.hInstance = hInstance; 185 wcex.lpszClassName = L"DrawRectangle_Test"; 186 wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); 187 188 RegisterClassExW(&wcex); 189 } 190 191 bool InitInstance(HINSTANCE hInstance, int nCmdShow) 192 { 193 MONITORINFO mi{}; 194 mi.cbSize = sizeof(mi); 195 if (!GetMonitorInfo(MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY), &mi)) { 196 return false; 197 } 198 199 mainWindow = CreateWindowExW(WS_EX_TOOLWINDOW, 200 L"DrawRectangle_Test", 201 L"", 202 WS_POPUP, 203 mi.rcWork.left, 204 mi.rcWork.top, 205 RectWidth(mi.rcWork), 206 RectHeight(mi.rcWork), 207 nullptr, 208 nullptr, 209 hInstance, 210 nullptr); 211 212 if (mainWindow) 213 { 214 MakeWindowTransparent(mainWindow); 215 return true; 216 } 217 218 return false; 219 } 220 221 int APIENTRY wWinMain(_In_ HINSTANCE hInstance, 222 _In_opt_ HINSTANCE hPrevInstance, 223 _In_ LPWSTR lpCmdLine, 224 _In_ int nCmdShow) 225 { 226 UNREFERENCED_PARAMETER(hPrevInstance); 227 UNREFERENCED_PARAMETER(lpCmdLine); 228 229 GdiplusStartupInput gdiplusStartupInput; 230 ULONG_PTR gdiplusToken; 231 GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr); 232 233 SetProcessDpiAwareness(PROCESS_DPI_UNAWARE); 234 StartLowLevelKeyboardHook(); 235 236 RegisterClass(hInstance); 237 238 if (!InitInstance(hInstance, nCmdShow)) 239 { 240 return 0; 241 } 242 243 RECT clientRect{}; 244 GetClientRect(mainWindow, &clientRect); 245 zones = BuildColumnZoneLayout(ZONE_COUNT, clientRect); 246 highlighted = std::vector<bool>(ZONE_COUNT, false); 247 248 // Invoke main window re-drawing from separate thread (based on changes in cursor position). 249 std::thread refreshThread = std::thread(RefreshMainWindow); 250 refreshThread.detach(); 251 252 253 HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_FANCYZONESDRAWLAYOUTTEST)); 254 MSG msg{}; 255 while (GetMessage(&msg, nullptr, 0, 0)) 256 { 257 if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 258 { 259 TranslateMessage(&msg); 260 DispatchMessage(&msg); 261 } 262 } 263 264 StopLowLevelKeyboardHook(); 265 GdiplusShutdown(gdiplusToken); 266 267 return (int)msg.wParam; 268 } 269 270 struct ColorSetting 271 { 272 BYTE fillAlpha{}; 273 COLORREF fill{}; 274 BYTE borderAlpha{}; 275 COLORREF border{}; 276 int thickness{}; 277 }; 278 279 inline void InitRGB(_Out_ RGBQUAD* quad, BYTE alpha, COLORREF color) 280 { 281 ZeroMemory(quad, sizeof(*quad)); 282 quad->rgbReserved = alpha; 283 quad->rgbRed = GetRValue(color) * alpha / 255; 284 quad->rgbGreen = GetGValue(color) * alpha / 255; 285 quad->rgbBlue = GetBValue(color) * alpha / 255; 286 } 287 288 inline void FillRectARGB(HDC hdc, const RECT& prcFill, BYTE alpha, COLORREF color, bool blendAlpha) 289 { 290 BITMAPINFO bi; 291 ZeroMemory(&bi, sizeof(bi)); 292 bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); 293 bi.bmiHeader.biWidth = 1; 294 bi.bmiHeader.biHeight = 1; 295 bi.bmiHeader.biPlanes = 1; 296 bi.bmiHeader.biBitCount = 32; 297 bi.bmiHeader.biCompression = BI_RGB; 298 299 RECT fillRect; 300 CopyRect(&fillRect, &prcFill); 301 302 RGBQUAD bitmapBits; 303 InitRGB(&bitmapBits, alpha, color); 304 StretchDIBits( 305 hdc, 306 fillRect.left, 307 fillRect.top, 308 fillRect.right - fillRect.left, 309 fillRect.bottom - fillRect.top, 310 0, 311 0, 312 1, 313 1, 314 &bitmapBits, 315 &bi, 316 DIB_RGB_COLORS, 317 SRCCOPY); 318 } 319 320 void DrawBackdrop(HDC& hdc, const RECT& clientRect) 321 { 322 FillRectARGB(hdc, clientRect, 0, RGB(0, 0, 0), false); 323 } 324 325 void DrawIndex(HDC hdc, const RECT& rect, size_t index) 326 { 327 Gdiplus::Graphics g(hdc); 328 329 Gdiplus::FontFamily fontFamily(L"Segoe ui"); 330 Gdiplus::Font font(&fontFamily, 80, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel); 331 Gdiplus::SolidBrush solidBrush(Gdiplus::Color(255, 0, 0, 0)); 332 333 std::wstring text = std::to_wstring(index); 334 335 g.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias); 336 Gdiplus::StringFormat stringFormat = new Gdiplus::StringFormat(); 337 stringFormat.SetAlignment(Gdiplus::StringAlignmentCenter); 338 stringFormat.SetLineAlignment(Gdiplus::StringAlignmentCenter); 339 340 Gdiplus::RectF gdiRect( 341 static_cast<Gdiplus::REAL>(rect.left), 342 static_cast<Gdiplus::REAL>(rect.top), 343 static_cast<Gdiplus::REAL>(RectWidth(rect)), 344 static_cast<Gdiplus::REAL>(RectHeight(rect))); 345 346 g.DrawString(text.c_str(), -1, &font, gdiRect, &stringFormat, &solidBrush); 347 } 348 349 void DrawZone(HDC hdc, const ColorSetting& colorSetting, const RECT& rect, size_t index) 350 { 351 Gdiplus::Graphics g(hdc); 352 Gdiplus::Color fillColor(colorSetting.fillAlpha, GetRValue(colorSetting.fill), GetGValue(colorSetting.fill), GetBValue(colorSetting.fill)); 353 Gdiplus::Color borderColor(colorSetting.borderAlpha, GetRValue(colorSetting.border), GetGValue(colorSetting.border), GetBValue(colorSetting.border)); 354 355 Gdiplus::Rect rectangle(rect.left, rect.top, RectWidth(rect), RectHeight(rect)); 356 357 Gdiplus::Pen pen(borderColor, static_cast<Gdiplus::REAL>(colorSetting.thickness)); 358 g.FillRectangle(new Gdiplus::SolidBrush(fillColor), rectangle); 359 g.DrawRectangle(&pen, rectangle); 360 361 DrawIndex(hdc, rect, index); 362 } 363 364 constexpr inline BYTE OpacitySettingToAlpha(int opacity) 365 { 366 return static_cast<BYTE>(opacity * 2.55); 367 } 368 369 COLORREF ParseColor(const std::wstring& zoneColor) 370 { 371 // Skip the leading # and convert to long 372 const auto color = zoneColor; 373 const auto tmp = std::stol(color.substr(1), nullptr, 16); 374 const auto nR = (tmp & 0xFF0000) >> 16; 375 const auto nG = (tmp & 0xFF00) >> 8; 376 const auto nB = (tmp & 0xFF); 377 return RGB(nR, nG, nB); 378 } 379 380 static int highlightedIdx = -1; 381 382 void OnPaint(HDC hdc) 383 { 384 int zoneOpacity = 50; 385 std::wstring zoneColor = L"#0078D7"; 386 std::wstring zoneBorderColor = L"#FFFFFF"; 387 std::wstring zoneHighlightColor = L"#F5FCFF"; 388 389 ColorSetting color{ OpacitySettingToAlpha(zoneOpacity), 390 ParseColor(zoneColor), 391 255, 392 ParseColor(zoneBorderColor), 393 -2 }; 394 395 ColorSetting highlight{ OpacitySettingToAlpha(zoneOpacity), 396 ParseColor(zoneHighlightColor), 397 255, 398 ParseColor(zoneBorderColor), 399 -2 }; 400 401 HMONITOR monitor = MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY); 402 MONITORINFOEX mi; 403 mi.cbSize = sizeof(mi); 404 GetMonitorInfo(monitor, &mi); 405 406 HDC hdcMem{ nullptr }; 407 HPAINTBUFFER bufferedPaint = BeginBufferedPaint(hdc, &mi.rcWork, BPBF_TOPDOWNDIB, nullptr, &hdcMem); 408 if (bufferedPaint) 409 { 410 DrawBackdrop(hdcMem, mi.rcWork); 411 for (size_t i = 0; i < zones.size(); ++i) 412 { 413 if (highlighted[i]) 414 { 415 DrawZone(hdcMem, color, zones[i], i); 416 } 417 else 418 { 419 DrawZone(hdcMem, highlight, zones[i], i); 420 } 421 } 422 EndBufferedPaint(bufferedPaint, TRUE); 423 } 424 } 425 426 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 427 { 428 switch (message) 429 { 430 case WM_NCDESTROY: 431 { 432 DefWindowProc(mainWindow, message, wParam, lParam); 433 SetWindowLongPtr(mainWindow, GWLP_USERDATA, 0); 434 } 435 break; 436 437 case WM_ERASEBKGND: 438 return 1; 439 440 case WM_PRINTCLIENT: 441 case WM_PAINT: 442 { 443 PAINTSTRUCT ps; 444 HDC hdc = BeginPaint(hWnd, &ps); 445 OnPaint(hdc); 446 EndPaint(hWnd, &ps); 447 } 448 break; 449 case WM_DESTROY: 450 PostQuitMessage(0); 451 break; 452 default: 453 return DefWindowProc(hWnd, message, wParam, lParam); 454 } 455 return 0; 456 }