Utility.cpp
1 //============================================================================== 2 // 3 // Zoomit 4 // Sysinternals - www.sysinternals.com 5 // 6 // Utility functions 7 // 8 //============================================================================== 9 #include "pch.h" 10 #include "Utility.h" 11 #include <string> 12 13 #pragma comment(lib, "uxtheme.lib") 14 15 //---------------------------------------------------------------------------- 16 // Dark Mode - Static/Global State 17 //---------------------------------------------------------------------------- 18 static bool g_darkModeInitialized = false; 19 static bool g_darkModeEnabled = false; 20 static HBRUSH g_darkBackgroundBrush = nullptr; 21 static HBRUSH g_darkControlBrush = nullptr; 22 static HBRUSH g_darkSurfaceBrush = nullptr; 23 24 // Theme override from registry (defined in ZoomItSettings.h) 25 extern DWORD g_ThemeOverride; 26 27 // Preferred App Mode values for Windows 10/11 dark mode 28 enum class PreferredAppMode 29 { 30 Default, 31 AllowDark, 32 ForceDark, 33 ForceLight, 34 Max 35 }; 36 37 // Undocumented ordinals from uxtheme.dll for dark mode support 38 using fnSetPreferredAppMode = PreferredAppMode(WINAPI*)(PreferredAppMode appMode); 39 using fnAllowDarkModeForWindow = bool(WINAPI*)(HWND hWnd, bool allow); 40 using fnShouldAppsUseDarkMode = bool(WINAPI*)(); 41 using fnRefreshImmersiveColorPolicyState = void(WINAPI*)(); 42 using fnFlushMenuThemes = void(WINAPI*)(); 43 44 static fnSetPreferredAppMode pSetPreferredAppMode = nullptr; 45 static fnAllowDarkModeForWindow pAllowDarkModeForWindow = nullptr; 46 static fnShouldAppsUseDarkMode pShouldAppsUseDarkMode = nullptr; 47 static fnRefreshImmersiveColorPolicyState pRefreshImmersiveColorPolicyState = nullptr; 48 static fnFlushMenuThemes pFlushMenuThemes = nullptr; 49 50 //---------------------------------------------------------------------------- 51 // 52 // InitializeDarkModeSupport 53 // 54 // Initialize dark mode function pointers from uxtheme.dll 55 // 56 //---------------------------------------------------------------------------- 57 static void InitializeDarkModeSupport() 58 { 59 if (g_darkModeInitialized) 60 return; 61 62 g_darkModeInitialized = true; 63 64 HMODULE hUxTheme = GetModuleHandleW(L"uxtheme.dll"); 65 if (hUxTheme) 66 { 67 // These are undocumented ordinal exports 68 // Ordinal 135: SetPreferredAppMode (Windows 10 1903+) 69 pSetPreferredAppMode = reinterpret_cast<fnSetPreferredAppMode>( 70 GetProcAddress(hUxTheme, MAKEINTRESOURCEA(135))); 71 // Ordinal 133: AllowDarkModeForWindow 72 pAllowDarkModeForWindow = reinterpret_cast<fnAllowDarkModeForWindow>( 73 GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133))); 74 // Ordinal 132: ShouldAppsUseDarkMode 75 pShouldAppsUseDarkMode = reinterpret_cast<fnShouldAppsUseDarkMode>( 76 GetProcAddress(hUxTheme, MAKEINTRESOURCEA(132))); 77 // Ordinal 104: RefreshImmersiveColorPolicyState 78 pRefreshImmersiveColorPolicyState = reinterpret_cast<fnRefreshImmersiveColorPolicyState>( 79 GetProcAddress(hUxTheme, MAKEINTRESOURCEA(104))); 80 // Ordinal 136: FlushMenuThemes 81 pFlushMenuThemes = reinterpret_cast<fnFlushMenuThemes>( 82 GetProcAddress(hUxTheme, MAKEINTRESOURCEA(136))); 83 84 // Set preferred app mode based on our theme override or system setting 85 // Note: We check g_ThemeOverride directly here because IsDarkModeEnabled 86 // calls InitializeDarkModeSupport, which would cause recursion 87 if (pSetPreferredAppMode) 88 { 89 bool useDarkMode = false; 90 if (g_ThemeOverride == 0) 91 { 92 useDarkMode = false; // Force light 93 } 94 else if (g_ThemeOverride == 1) 95 { 96 useDarkMode = true; // Force dark 97 } 98 else if (pShouldAppsUseDarkMode) 99 { 100 useDarkMode = pShouldAppsUseDarkMode(); // Use system setting 101 } 102 103 if (useDarkMode) 104 { 105 pSetPreferredAppMode(PreferredAppMode::ForceDark); 106 } 107 else 108 { 109 pSetPreferredAppMode(PreferredAppMode::ForceLight); 110 } 111 } 112 113 // Flush menu themes to apply dark mode to context menus 114 if (pFlushMenuThemes) 115 { 116 pFlushMenuThemes(); 117 } 118 } 119 120 // Update cached dark mode state 121 g_darkModeEnabled = false; 122 if (g_ThemeOverride == 0) 123 { 124 g_darkModeEnabled = false; 125 } 126 else if (g_ThemeOverride == 1) 127 { 128 g_darkModeEnabled = true; 129 } 130 else if (pShouldAppsUseDarkMode) 131 { 132 g_darkModeEnabled = pShouldAppsUseDarkMode(); 133 } 134 } 135 136 //---------------------------------------------------------------------------- 137 // 138 // IsDarkModeEnabled 139 // 140 //---------------------------------------------------------------------------- 141 bool IsDarkModeEnabled() 142 { 143 // Check for theme override from registry (0=light, 1=dark, 2+=system) 144 if (g_ThemeOverride == 0) 145 { 146 return false; // Force light mode 147 } 148 else if (g_ThemeOverride == 1) 149 { 150 return true; // Force dark mode 151 } 152 153 InitializeDarkModeSupport(); 154 155 // Check the undocumented API first 156 if (pShouldAppsUseDarkMode) 157 { 158 return pShouldAppsUseDarkMode(); 159 } 160 161 // Fallback: Check registry for system theme preference 162 HKEY hKey; 163 if (RegOpenKeyExW(HKEY_CURRENT_USER, 164 L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", 165 0, KEY_READ, &hKey) == ERROR_SUCCESS) 166 { 167 DWORD value = 1; 168 DWORD size = sizeof(value); 169 RegQueryValueExW(hKey, L"AppsUseLightTheme", nullptr, nullptr, 170 reinterpret_cast<LPBYTE>(&value), &size); 171 RegCloseKey(hKey); 172 return value == 0; // 0 = dark mode, 1 = light mode 173 } 174 175 return false; 176 } 177 178 //---------------------------------------------------------------------------- 179 // 180 // RefreshDarkModeState 181 // 182 //---------------------------------------------------------------------------- 183 void RefreshDarkModeState() 184 { 185 InitializeDarkModeSupport(); 186 187 if (pRefreshImmersiveColorPolicyState) 188 { 189 pRefreshImmersiveColorPolicyState(); 190 } 191 192 // Update preferred app mode based on our IsDarkModeEnabled (respects override) 193 bool useDark = IsDarkModeEnabled(); 194 if (pSetPreferredAppMode) 195 { 196 if (useDark) 197 { 198 pSetPreferredAppMode(PreferredAppMode::ForceDark); 199 } 200 else 201 { 202 pSetPreferredAppMode(PreferredAppMode::ForceLight); 203 } 204 } 205 206 // Flush menu themes to apply dark mode to context menus 207 if (pFlushMenuThemes) 208 { 209 pFlushMenuThemes(); 210 } 211 212 g_darkModeEnabled = useDark; 213 } 214 215 //---------------------------------------------------------------------------- 216 // 217 // SetDarkModeForWindow 218 // 219 //---------------------------------------------------------------------------- 220 void SetDarkModeForWindow(HWND hWnd, bool enable) 221 { 222 InitializeDarkModeSupport(); 223 224 if (pAllowDarkModeForWindow) 225 { 226 pAllowDarkModeForWindow(hWnd, enable); 227 } 228 229 // Use DWMWA_USE_IMMERSIVE_DARK_MODE attribute (Windows 10 build 17763+) 230 // Attribute 20 is DWMWA_USE_IMMERSIVE_DARK_MODE 231 BOOL useDarkMode = enable ? TRUE : FALSE; 232 HMODULE hDwmapi = GetModuleHandleW(L"dwmapi.dll"); 233 if (hDwmapi) 234 { 235 using fnDwmSetWindowAttribute = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD); 236 auto pDwmSetWindowAttribute = reinterpret_cast<fnDwmSetWindowAttribute>( 237 GetProcAddress(hDwmapi, "DwmSetWindowAttribute")); 238 if (pDwmSetWindowAttribute) 239 { 240 // Try attribute 20 first (Windows 11 / newer Windows 10) 241 HRESULT hr = pDwmSetWindowAttribute(hWnd, 20, &useDarkMode, sizeof(useDarkMode)); 242 if (FAILED(hr)) 243 { 244 // Fall back to attribute 19 (older Windows 10) 245 pDwmSetWindowAttribute(hWnd, 19, &useDarkMode, sizeof(useDarkMode)); 246 } 247 } 248 } 249 } 250 251 //---------------------------------------------------------------------------- 252 // 253 // GetDarkModeBrush / GetDarkModeControlBrush / GetDarkModeSurfaceBrush 254 // 255 //---------------------------------------------------------------------------- 256 HBRUSH GetDarkModeBrush() 257 { 258 if (!g_darkBackgroundBrush) 259 { 260 g_darkBackgroundBrush = CreateSolidBrush(DarkMode::BackgroundColor); 261 } 262 return g_darkBackgroundBrush; 263 } 264 265 HBRUSH GetDarkModeControlBrush() 266 { 267 if (!g_darkControlBrush) 268 { 269 g_darkControlBrush = CreateSolidBrush(DarkMode::ControlColor); 270 } 271 return g_darkControlBrush; 272 } 273 274 HBRUSH GetDarkModeSurfaceBrush() 275 { 276 if (!g_darkSurfaceBrush) 277 { 278 g_darkSurfaceBrush = CreateSolidBrush(DarkMode::SurfaceColor); 279 } 280 return g_darkSurfaceBrush; 281 } 282 283 //---------------------------------------------------------------------------- 284 // 285 // ApplyDarkModeToDialog 286 // 287 //---------------------------------------------------------------------------- 288 void ApplyDarkModeToDialog(HWND hDlg) 289 { 290 if (IsDarkModeEnabled()) 291 { 292 SetDarkModeForWindow(hDlg, true); 293 294 // Set dark theme for the dialog 295 SetWindowTheme(hDlg, L"DarkMode_Explorer", nullptr); 296 297 // Apply dark theme to common controls (buttons, edit boxes, etc.) 298 EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL { 299 wchar_t className[64] = { 0 }; 300 GetClassNameW(hChild, className, _countof(className)); 301 302 // Apply appropriate theme based on control type 303 if (_wcsicmp(className, L"Button") == 0) 304 { 305 // Check if this is a checkbox or radio button 306 LONG style = GetWindowLong(hChild, GWL_STYLE); 307 LONG buttonType = style & BS_TYPEMASK; 308 if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX || 309 buttonType == BS_3STATE || buttonType == BS_AUTO3STATE || 310 buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON) 311 { 312 // Subclass checkbox/radio for dark mode painting - but keep DarkMode_Explorer theme 313 // for proper hit testing (empty theme can break mouse interaction) 314 SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr); 315 SetWindowSubclass(hChild, CheckboxSubclassProc, 2, 0); 316 } 317 else if (buttonType == BS_GROUPBOX) 318 { 319 // Subclass group box for dark mode painting 320 SetWindowTheme(hChild, L"", L""); 321 SetWindowSubclass(hChild, GroupBoxSubclassProc, 4, 0); 322 } 323 else 324 { 325 SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr); 326 } 327 } 328 else if (_wcsicmp(className, L"Edit") == 0) 329 { 330 // Use empty theme and subclass for dark mode border drawing 331 SetWindowTheme(hChild, L"", L""); 332 SetWindowSubclass(hChild, EditControlSubclassProc, 3, 0); 333 } 334 else if (_wcsicmp(className, L"ComboBox") == 0) 335 { 336 SetWindowTheme(hChild, L"DarkMode_CFD", nullptr); 337 } 338 else if (_wcsicmp(className, L"SysListView32") == 0 || 339 _wcsicmp(className, L"SysTreeView32") == 0) 340 { 341 SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr); 342 } 343 else if (_wcsicmp(className, L"msctls_trackbar32") == 0) 344 { 345 // Subclass trackbar controls for dark mode painting 346 SetWindowTheme(hChild, L"", L""); 347 SetWindowSubclass(hChild, SliderSubclassProc, 1, 0); 348 } 349 else if (_wcsicmp(className, L"SysTabControl32") == 0) 350 { 351 // Use empty theme for tab control to allow dark background 352 SetWindowTheme(hChild, L"", L""); 353 } 354 else if (_wcsicmp(className, L"msctls_updown32") == 0) 355 { 356 SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr); 357 } 358 else if (_wcsicmp(className, L"msctls_hotkey32") == 0) 359 { 360 // Subclass hotkey controls for dark mode painting 361 SetWindowTheme(hChild, L"", L""); 362 SetWindowSubclass(hChild, HotkeyControlSubclassProc, 1, 0); 363 } 364 else if (_wcsicmp(className, L"Static") == 0) 365 { 366 // Check if this is a text label (not an owner-draw or image control) 367 LONG style = GetWindowLong(hChild, GWL_STYLE); 368 LONG staticType = style & SS_TYPEMASK; 369 370 // Options header uses a dedicated static subclass (to support large title font). 371 // Avoid applying the generic static subclass on top of it. 372 const int controlId = GetDlgCtrlID( hChild ); 373 if( controlId == IDC_VERSION || controlId == IDC_COPYRIGHT ) 374 { 375 SetWindowTheme( hChild, L"", L"" ); 376 return TRUE; 377 } 378 379 if (staticType == SS_LEFT || staticType == SS_CENTER || staticType == SS_RIGHT || 380 staticType == SS_LEFTNOWORDWRAP || staticType == SS_SIMPLE) 381 { 382 // Subclass text labels for proper dark mode painting 383 SetWindowTheme(hChild, L"", L""); 384 SetWindowSubclass(hChild, StaticTextSubclassProc, 5, 0); 385 } 386 else 387 { 388 // Other static controls (icons, bitmaps, frames) - just remove theme 389 SetWindowTheme(hChild, L"", L""); 390 } 391 } 392 else 393 { 394 SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr); 395 } 396 return TRUE; 397 }, 0); 398 } 399 else 400 { 401 // Light mode - remove dark mode 402 SetDarkModeForWindow(hDlg, false); 403 SetWindowTheme(hDlg, nullptr, nullptr); 404 405 EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL { 406 // Remove subclass from controls 407 wchar_t className[64] = { 0 }; 408 GetClassNameW(hChild, className, _countof(className)); 409 if (_wcsicmp(className, L"msctls_hotkey32") == 0) 410 { 411 RemoveWindowSubclass(hChild, HotkeyControlSubclassProc, 1); 412 } 413 else if (_wcsicmp(className, L"msctls_trackbar32") == 0) 414 { 415 RemoveWindowSubclass(hChild, SliderSubclassProc, 1); 416 } 417 else if (_wcsicmp(className, L"Button") == 0) 418 { 419 LONG style = GetWindowLong(hChild, GWL_STYLE); 420 LONG buttonType = style & BS_TYPEMASK; 421 if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX || 422 buttonType == BS_3STATE || buttonType == BS_AUTO3STATE || 423 buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON) 424 { 425 RemoveWindowSubclass(hChild, CheckboxSubclassProc, 2); 426 } 427 else if (buttonType == BS_GROUPBOX) 428 { 429 RemoveWindowSubclass(hChild, GroupBoxSubclassProc, 4); 430 } 431 } 432 else if (_wcsicmp(className, L"Edit") == 0) 433 { 434 RemoveWindowSubclass(hChild, EditControlSubclassProc, 3); 435 } 436 else if (_wcsicmp(className, L"Static") == 0) 437 { 438 RemoveWindowSubclass(hChild, StaticTextSubclassProc, 5); 439 } 440 SetWindowTheme(hChild, nullptr, nullptr); 441 return TRUE; 442 }, 0); 443 } 444 } 445 446 //---------------------------------------------------------------------------- 447 // 448 // HandleDarkModeCtlColor 449 // 450 //---------------------------------------------------------------------------- 451 HBRUSH HandleDarkModeCtlColor(HDC hdc, HWND hCtrl, UINT message) 452 { 453 if (!IsDarkModeEnabled()) 454 { 455 return nullptr; 456 } 457 458 switch (message) 459 { 460 case WM_CTLCOLORDLG: 461 SetBkColor(hdc, DarkMode::BackgroundColor); 462 SetTextColor(hdc, DarkMode::TextColor); 463 return GetDarkModeBrush(); 464 465 case WM_CTLCOLORSTATIC: 466 SetBkMode(hdc, TRANSPARENT); 467 // Use dimmed color for disabled static controls 468 if (!IsWindowEnabled(hCtrl)) 469 { 470 SetTextColor(hdc, RGB(100, 100, 100)); 471 } 472 else 473 { 474 SetTextColor(hdc, DarkMode::TextColor); 475 } 476 return GetDarkModeBrush(); 477 478 case WM_CTLCOLORBTN: 479 SetBkColor(hdc, DarkMode::ControlColor); 480 SetTextColor(hdc, DarkMode::TextColor); 481 return GetDarkModeControlBrush(); 482 483 case WM_CTLCOLOREDIT: 484 SetBkColor(hdc, DarkMode::SurfaceColor); 485 SetTextColor(hdc, DarkMode::TextColor); 486 return GetDarkModeSurfaceBrush(); 487 488 case WM_CTLCOLORLISTBOX: 489 SetBkColor(hdc, DarkMode::SurfaceColor); 490 SetTextColor(hdc, DarkMode::TextColor); 491 return GetDarkModeSurfaceBrush(); 492 } 493 494 return nullptr; 495 } 496 497 //---------------------------------------------------------------------------- 498 // 499 // ApplyDarkModeToMenu 500 // 501 // Uses undocumented uxtheme functions to enable dark mode for menus 502 // 503 //---------------------------------------------------------------------------- 504 void ApplyDarkModeToMenu(HMENU hMenu) 505 { 506 if (!hMenu) 507 { 508 return; 509 } 510 511 if (!IsDarkModeEnabled()) 512 { 513 // Light mode - clear any dark background 514 MENUINFO mi = { sizeof(mi) }; 515 mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS; 516 mi.hbrBack = nullptr; 517 SetMenuInfo(hMenu, &mi); 518 return; 519 } 520 521 // For popup menus, we need to use MENUINFO to set the background 522 MENUINFO mi = { sizeof(mi) }; 523 mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS; 524 mi.hbrBack = GetDarkModeSurfaceBrush(); 525 SetMenuInfo(hMenu, &mi); 526 } 527 528 //---------------------------------------------------------------------------- 529 // 530 // RefreshWindowTheme 531 // 532 // Forces a window and all its children to redraw with current theme 533 // 534 //---------------------------------------------------------------------------- 535 void RefreshWindowTheme(HWND hWnd) 536 { 537 if (!hWnd) 538 { 539 return; 540 } 541 542 // Reapply theme to this window 543 ApplyDarkModeToDialog(hWnd); 544 545 // Force redraw 546 RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_FRAME); 547 } 548 549 //---------------------------------------------------------------------------- 550 // 551 // CleanupDarkModeResources 552 // 553 //---------------------------------------------------------------------------- 554 void CleanupDarkModeResources() 555 { 556 if (g_darkBackgroundBrush) 557 { 558 DeleteObject(g_darkBackgroundBrush); 559 g_darkBackgroundBrush = nullptr; 560 } 561 if (g_darkControlBrush) 562 { 563 DeleteObject(g_darkControlBrush); 564 g_darkControlBrush = nullptr; 565 } 566 if (g_darkSurfaceBrush) 567 { 568 DeleteObject(g_darkSurfaceBrush); 569 g_darkSurfaceBrush = nullptr; 570 } 571 } 572 573 //---------------------------------------------------------------------------- 574 // 575 // InitializeDarkMode 576 // 577 // Public wrapper to initialize dark mode support early in app startup 578 // 579 //---------------------------------------------------------------------------- 580 void InitializeDarkMode() 581 { 582 InitializeDarkModeSupport(); 583 } 584 585 //---------------------------------------------------------------------------- 586 // 587 // ForceRectInBounds 588 // 589 //---------------------------------------------------------------------------- 590 RECT ForceRectInBounds( RECT rect, const RECT& bounds ) 591 { 592 if( rect.left < bounds.left ) 593 { 594 rect.right += bounds.left - rect.left; 595 rect.left = bounds.left; 596 } 597 if( rect.top < bounds.top ) 598 { 599 rect.bottom += bounds.top - rect.top; 600 rect.top = bounds.top; 601 } 602 if( rect.right > bounds.right ) 603 { 604 rect.left -= rect.right - bounds.right; 605 rect.right = bounds.right; 606 } 607 if( rect.bottom > bounds.bottom ) 608 { 609 rect.top -= rect.bottom - bounds.bottom; 610 rect.bottom = bounds.bottom; 611 } 612 return rect; 613 } 614 615 //---------------------------------------------------------------------------- 616 // 617 // GetDpiForWindow 618 // 619 //---------------------------------------------------------------------------- 620 UINT GetDpiForWindowHelper( HWND window ) 621 { 622 auto function = reinterpret_cast<UINT (WINAPI *)(HWND)>(GetProcAddress( GetModuleHandleW( L"user32.dll" ), "GetDpiForWindow" )); 623 if( function ) 624 { 625 return function( window ); 626 } 627 628 wil::unique_hdc hdc{GetDC( nullptr )}; 629 return static_cast<UINT>(GetDeviceCaps( hdc.get(), LOGPIXELSX )); 630 } 631 632 //---------------------------------------------------------------------------- 633 // 634 // GetMonitorRectFromCursor 635 // 636 //---------------------------------------------------------------------------- 637 RECT GetMonitorRectFromCursor() 638 { 639 POINT point; 640 GetCursorPos( &point ); 641 MONITORINFO monitorInfo{}; 642 monitorInfo.cbSize = sizeof( monitorInfo ); 643 GetMonitorInfoW( MonitorFromPoint( point, MONITOR_DEFAULTTONEAREST ), &monitorInfo ); 644 return monitorInfo.rcMonitor; 645 } 646 647 //---------------------------------------------------------------------------- 648 // 649 // RectFromPointsMinSize 650 // 651 //---------------------------------------------------------------------------- 652 #ifdef _MSC_VER 653 // avoid making RectFromPointsMinSize constexpr since that leads to link errors 654 #pragma warning(push) 655 #pragma warning(disable: 26497) 656 #endif 657 658 RECT RectFromPointsMinSize( POINT a, POINT b, LONG minSize ) 659 { 660 RECT rect; 661 if( a.x <= b.x ) 662 { 663 rect.left = a.x; 664 rect.right = b.x + 1; 665 if( (rect.right - rect.left) < minSize ) 666 { 667 rect.right = rect.left + minSize; 668 } 669 } 670 else 671 { 672 rect.left = b.x; 673 rect.right = a.x + 1; 674 if( (rect.right - rect.left) < minSize ) 675 { 676 rect.left = rect.right - minSize; 677 } 678 } 679 if( a.y <= b.y ) 680 { 681 rect.top = a.y; 682 rect.bottom = b.y + 1; 683 if( (rect.bottom - rect.top) < minSize ) 684 { 685 rect.bottom = rect.top + minSize; 686 } 687 } 688 else 689 { 690 rect.top = b.y; 691 rect.bottom = a.y + 1; 692 if( (rect.bottom - rect.top) < minSize ) 693 { 694 rect.top = rect.bottom - minSize; 695 } 696 } 697 return rect; 698 } 699 #ifdef _MSC_VER 700 #pragma warning(pop) 701 #endif 702 //---------------------------------------------------------------------------- 703 // 704 // ScaleForDpi 705 // 706 //---------------------------------------------------------------------------- 707 int ScaleForDpi( int value, UINT dpi ) 708 { 709 return MulDiv( value, static_cast<int>(dpi), USER_DEFAULT_SCREEN_DPI ); 710 } 711 712 //---------------------------------------------------------------------------- 713 // 714 // ScalePointInRects 715 // 716 //---------------------------------------------------------------------------- 717 POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target ) 718 { 719 const SIZE sourceSize{ source.right - source.left, source.bottom - source.top }; 720 const POINT sourceCenter{ source.left + sourceSize.cx / 2, source.top + sourceSize.cy / 2 }; 721 const SIZE targetSize{ target.right - target.left, target.bottom - target.top }; 722 const POINT targetCenter{ target.left + targetSize.cx / 2, target.top + targetSize.cy / 2 }; 723 724 return { targetCenter.x + MulDiv( point.x - sourceCenter.x, targetSize.cx, sourceSize.cx ), 725 targetCenter.y + MulDiv( point.y - sourceCenter.y, targetSize.cy, sourceSize.cy ) }; 726 } 727 728 //---------------------------------------------------------------------------- 729 // 730 // ScaleDialogForDpi 731 // 732 // Scales a dialog and all its child controls for the specified DPI. 733 // oldDpi defaults to DPI_BASELINE (96) for initial scaling. 734 // 735 //---------------------------------------------------------------------------- 736 void ScaleDialogForDpi( HWND hDlg, UINT newDpi, UINT oldDpi ) 737 { 738 if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 ) 739 { 740 return; 741 } 742 743 // With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created. 744 // We only need to scale when moving between monitors with different DPIs. 745 // When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling. 746 if( oldDpi == DPI_BASELINE ) 747 { 748 return; 749 } 750 751 // Scale the dialog window itself 752 RECT dialogRect; 753 GetWindowRect( hDlg, &dialogRect ); 754 int dialogWidth = MulDiv( dialogRect.right - dialogRect.left, newDpi, oldDpi ); 755 int dialogHeight = MulDiv( dialogRect.bottom - dialogRect.top, newDpi, oldDpi ); 756 SetWindowPos( hDlg, nullptr, 0, 0, dialogWidth, dialogHeight, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE ); 757 758 // Enumerate and scale all child controls 759 HWND hChild = GetWindow( hDlg, GW_CHILD ); 760 while( hChild != nullptr ) 761 { 762 RECT childRect; 763 GetWindowRect( hChild, &childRect ); 764 MapWindowPoints( nullptr, hDlg, reinterpret_cast<LPPOINT>(&childRect), 2 ); 765 766 int x = MulDiv( childRect.left, newDpi, oldDpi ); 767 int y = MulDiv( childRect.top, newDpi, oldDpi ); 768 int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi ); 769 int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi ); 770 771 SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE ); 772 773 // Scale the font for the control 774 HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 )); 775 if( hFont != nullptr ) 776 { 777 LOGFONT lf{}; 778 if( GetObject( hFont, sizeof(lf), &lf ) ) 779 { 780 lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi ); 781 HFONT hNewFont = CreateFontIndirect( &lf ); 782 if( hNewFont ) 783 { 784 SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE ); 785 // Note: The old font might be shared, so we don't delete it here 786 // The system will clean up fonts when the dialog is destroyed 787 } 788 } 789 } 790 791 hChild = GetWindow( hChild, GW_HWNDNEXT ); 792 } 793 794 // Also scale the dialog's own font 795 HFONT hDialogFont = reinterpret_cast<HFONT>(SendMessage( hDlg, WM_GETFONT, 0, 0 )); 796 if( hDialogFont != nullptr ) 797 { 798 LOGFONT lf{}; 799 if( GetObject( hDialogFont, sizeof(lf), &lf ) ) 800 { 801 lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi ); 802 HFONT hNewFont = CreateFontIndirect( &lf ); 803 if( hNewFont ) 804 { 805 SendMessage( hDlg, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE ); 806 } 807 } 808 } 809 } 810 811 //---------------------------------------------------------------------------- 812 // 813 // ScaleChildControlsForDpi 814 // 815 // Scales a window's direct child controls (and their fonts) for the specified DPI. 816 // Unlike ScaleDialogForDpi, this does not resize the parent window itself. 817 // 818 // This is useful for child dialogs used as tab pages: the tab page window is 819 // already scaled when the parent options dialog is scaled, but the controls 820 // inside the page are not (because they are grandchildren of the options dialog). 821 // 822 //---------------------------------------------------------------------------- 823 void ScaleChildControlsForDpi( HWND hParent, UINT newDpi, UINT oldDpi ) 824 { 825 if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 ) 826 { 827 return; 828 } 829 830 // With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created. 831 // We only need to scale when moving between monitors with different DPIs. 832 // When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling. 833 if( oldDpi == DPI_BASELINE ) 834 { 835 return; 836 } 837 838 HWND hChild = GetWindow( hParent, GW_CHILD ); 839 while( hChild != nullptr ) 840 { 841 RECT childRect; 842 GetWindowRect( hChild, &childRect ); 843 MapWindowPoints( nullptr, hParent, reinterpret_cast<LPPOINT>(&childRect), 2 ); 844 845 int x = MulDiv( childRect.left, newDpi, oldDpi ); 846 int y = MulDiv( childRect.top, newDpi, oldDpi ); 847 int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi ); 848 int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi ); 849 850 SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE ); 851 852 // Scale the font for the control 853 HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 )); 854 if( hFont != nullptr ) 855 { 856 LOGFONT lf{}; 857 if( GetObject( hFont, sizeof(lf), &lf ) ) 858 { 859 lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi ); 860 HFONT hNewFont = CreateFontIndirect( &lf ); 861 if( hNewFont ) 862 { 863 SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE ); 864 } 865 } 866 } 867 868 hChild = GetWindow( hChild, GW_HWNDNEXT ); 869 } 870 } 871 872 //---------------------------------------------------------------------------- 873 // 874 // HandleDialogDpiChange 875 // 876 // Handles WM_DPICHANGED message for dialogs. Call this from the dialog's 877 // WndProc when WM_DPICHANGED is received. 878 // 879 //---------------------------------------------------------------------------- 880 void HandleDialogDpiChange( HWND hDlg, WPARAM wParam, LPARAM lParam, UINT& currentDpi ) 881 { 882 UINT newDpi = HIWORD( wParam ); 883 if( newDpi != currentDpi && newDpi != 0 ) 884 { 885 const RECT* pSuggestedRect = reinterpret_cast<const RECT*>(lParam); 886 887 // Scale the dialog controls from the current DPI to the new DPI 888 ScaleDialogForDpi( hDlg, newDpi, currentDpi ); 889 890 // Move and resize the dialog to the suggested rectangle 891 SetWindowPos( hDlg, nullptr, 892 pSuggestedRect->left, 893 pSuggestedRect->top, 894 pSuggestedRect->right - pSuggestedRect->left, 895 pSuggestedRect->bottom - pSuggestedRect->top, 896 SWP_NOZORDER | SWP_NOACTIVATE ); 897 898 currentDpi = newDpi; 899 } 900 }