2025-01-16 20:52:24 +00:00
//============================================================================
//
// Zoomit
// Copyright (C) Mark Russinovich
// Sysinternals - www.sysinternals.com
//
// Screen zoom and annotation tool.
//
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//============================================================================
# include "pch.h"
# include "zoomit.h"
# include "Utility.h"
# include "WindowsVersions.h"
# include "ZoomItSettings.h"
2025-11-10 11:57:13 -08:00
# include "GifRecordingSession.h"
2026-03-26 13:21:43 +01:00
# include "BreakTimer.h"
# include "PanoramaCapture.h"
# include <wtsapi32.h>
# include <tlhelp32.h>
# include <limits>
# include <vector>
2025-01-16 20:52:24 +00:00
# ifdef __ZOOMIT_POWERTOYS__
# include <common/interop/shared_constants.h>
# include <common/utils/ProcessWaiter.h>
# include <common/utils/process_path.h>
# include "../ZoomItModuleInterface/trace.h"
# include <common/Telemetry/EtwTrace/EtwTrace.h>
# include <common/logger/logger.h>
# include <common/utils/logger_helper.h>
# include <common/utils/winapi_error.h>
# include <common/utils/gpo.h>
2025-12-23 21:07:44 +08:00
# include <array>
# include <vector>
# endif // __ZOOMIT_POWERTOYS__
# ifdef __ZOOMIT_POWERTOYS__
enum class ZoomItCommand
{
Zoom ,
Draw ,
Break ,
LiveZoom ,
Snip ,
2026-03-26 13:21:43 +01:00
SnipOcr ,
2025-12-23 21:07:44 +08:00
Record ,
} ;
2025-01-16 20:52:24 +00:00
# endif // __ZOOMIT_POWERTOYS__
namespace winrt
{
using namespace Windows : : Foundation ;
using namespace Windows : : Graphics ;
using namespace Windows : : Graphics : : Capture ;
using namespace Windows : : Graphics : : Imaging ;
using namespace Windows : : Storage ;
using namespace Windows : : UI : : Composition ;
using namespace Windows : : Storage : : Pickers ;
using namespace Windows : : System ;
using namespace Windows : : Devices : : Enumeration ;
2026-03-26 13:21:43 +01:00
using namespace Windows : : Media : : Ocr ;
2025-01-16 20:52:24 +00:00
}
namespace util
{
using namespace robmikh : : common : : uwp ;
using namespace robmikh : : common : : desktop ;
}
// This workaround keeps live zoom enabled after zooming out at level 1 (not zoomed) and disables
2025-03-07 12:35:44 -08:00
// live zoom when recording is stopped
2025-01-16 20:52:24 +00:00
# define WINDOWS_CURSOR_RECORDING_WORKAROUND 1
HINSTANCE g_hInstance ;
COLORREF g_CustomColors [ 16 ] ;
# define ZOOM_HOTKEY 0
# define DRAW_HOTKEY 1
# define BREAK_HOTKEY 2
# define LIVE_HOTKEY 3
# define LIVE_DRAW_HOTKEY 4
# define RECORD_HOTKEY 5
# define RECORD_CROP_HOTKEY 6
# define RECORD_WINDOW_HOTKEY 7
# define SNIP_HOTKEY 8
# define SNIP_SAVE_HOTKEY 9
# define DEMOTYPE_HOTKEY 10
# define DEMOTYPE_RESET_HOTKEY 11
2025-11-10 11:57:13 -08:00
# define RECORD_GIF_HOTKEY 12
# define RECORD_GIF_WINDOW_HOTKEY 13
2026-02-03 13:05:31 -08:00
# define SAVE_IMAGE_HOTKEY 14
# define SAVE_CROP_HOTKEY 15
# define COPY_IMAGE_HOTKEY 16
# define COPY_CROP_HOTKEY 17
2026-03-26 13:21:43 +01:00
# define SNIP_OCR_HOTKEY 18
# define SNIP_PANORAMA_HOTKEY 19
# define SNIP_PANORAMA_SAVE_HOTKEY 20
2025-01-16 20:52:24 +00:00
# define ZOOM_PAGE 0
# define LIVE_PAGE 1
2026-03-26 13:21:43 +01:00
// Screensaver activation constant
# ifndef SC_SCREENSAVE
# define SC_SCREENSAVE 0xF140
# endif
2025-01-16 20:52:24 +00:00
# define DRAW_PAGE 2
# define TYPE_PAGE 3
# define DEMOTYPE_PAGE 4
# define BREAK_PAGE 5
# define RECORD_PAGE 6
# define SNIP_PAGE 7
2026-03-26 13:21:43 +01:00
# define PANORAMA_PAGE 8
2025-01-16 20:52:24 +00:00
OPTION_TABS g_OptionsTabs [ ] = {
{ _T ( " Zoom " ) , NULL } ,
{ _T ( " LiveZoom " ) , NULL } ,
{ _T ( " Draw " ) , NULL } ,
{ _T ( " Type " ) , NULL } ,
{ _T ( " DemoType " ) , NULL } ,
{ _T ( " Break " ) , NULL } ,
{ _T ( " Record " ) , NULL } ,
2026-03-26 13:21:43 +01:00
{ _T ( " Snip " ) , NULL } ,
{ _T ( " Panorama " ) , NULL }
2025-01-16 20:52:24 +00:00
} ;
2025-11-10 11:57:13 -08:00
static const TCHAR * g_RecordingFormats [ ] = {
_T ( " GIF " ) ,
_T ( " MP4 " )
} ;
2025-01-16 20:52:24 +00:00
float g_ZoomLevels [ ] = {
1.25 ,
1.50 ,
1.75 ,
2.00 ,
3.00 ,
4.00
} ;
DWORD g_FramerateOptions [ ] = {
2025-11-10 11:57:13 -08:00
15 ,
24 ,
2025-01-16 20:52:24 +00:00
30 ,
60
} ;
//
// For typing mode
//
typedef enum {
TypeModeOff = 0 ,
TypeModeLeftJustify ,
TypeModeRightJustify
} TypeModeState ;
const DWORD CURSOR_ARM_LENGTH = 4 ;
const float NORMAL_BLUR_RADIUS = 20 ;
const float STRONG_BLUR_RADIUS = 40 ;
DWORD g_ToggleMod ;
DWORD g_LiveZoomToggleMod ;
DWORD g_DrawToggleMod ;
2025-11-10 11:57:13 -08:00
DWORD g_BreakToggleMod ;
2025-01-16 20:52:24 +00:00
DWORD g_DemoTypeToggleMod ;
DWORD g_RecordToggleMod ;
DWORD g_SnipToggleMod ;
2026-03-26 13:21:43 +01:00
DWORD g_SnipPanoramaToggleMod ;
DWORD g_SnipOcrToggleMod ;
2025-01-16 20:52:24 +00:00
BOOLEAN g_ZoomOnLiveZoom = FALSE ;
DWORD g_PenWidth = PEN_WIDTH ;
float g_BlurRadius = NORMAL_BLUR_RADIUS ;
HWND hWndOptions = NULL ;
BOOLEAN g_DrawPointer = FALSE ;
BOOLEAN g_PenDown = FALSE ;
BOOLEAN g_PenInverted = FALSE ;
DWORD g_OsVersion ;
HWND g_hWndLiveZoom = NULL ;
HWND g_hWndLiveZoomMag = NULL ;
HWND g_hWndMain ;
int g_AlphaBlend = 0x80 ;
BOOL g_fullScreenWorkaround = FALSE ;
bool g_bSaveInProgress = false ;
2026-03-26 13:21:43 +01:00
bool g_PanoramaCaptureActive = false ;
bool g_PanoramaStopRequested = false ;
bool g_PanoramaDebugMode = false ;
2025-01-16 20:52:24 +00:00
std : : wstring g_TextBuffer ;
// This is useful in the context of right-justified text only.
std : : list < std : : wstring > g_TextBufferPreviousLines ;
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
bool g_LiveZoomLevelOne = false ;
# endif
// True if ZoomIt was started by PowerToys instead of standalone.
BOOLEAN g_StartedByPowerToys = FALSE ;
BOOLEAN g_running = TRUE ;
// Screen recording globals
# define DEFAULT_RECORDING_FILE L"Recording.mp4"
2025-11-10 11:57:13 -08:00
# define DEFAULT_GIF_RECORDING_FILE L"Recording.gif"
2026-02-03 13:05:31 -08:00
# define DEFAULT_SCREENSHOT_FILE L"ZoomIt.png"
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
BOOL g_RecordToggle = FALSE ;
BOOL g_RecordCropping = FALSE ;
SelectRectangle g_SelectRectangle ;
std : : wstring g_RecordingSaveLocation ;
2026-02-03 13:05:31 -08:00
std : : wstring g_ScreenshotSaveLocation ;
2025-01-16 20:52:24 +00:00
winrt : : IDirect3DDevice g_RecordDevice { nullptr } ;
std : : shared_ptr < VideoRecordingSession > g_RecordingSession = nullptr ;
2025-11-10 11:57:13 -08:00
std : : shared_ptr < GifRecordingSession > g_GifRecordingSession = nullptr ;
2025-01-16 20:52:24 +00:00
type_pGetMonitorInfo pGetMonitorInfo ;
type_MonitorFromPoint pMonitorFromPoint ;
type_pSHAutoComplete pSHAutoComplete ;
type_pSetLayeredWindowAttributes pSetLayeredWindowAttributes ;
type_pSetProcessDPIAware pSetProcessDPIAware ;
type_pMagSetWindowSource pMagSetWindowSource ;
type_pMagSetWindowTransform pMagSetWindowTransform ;
type_pMagSetFullscreenTransform pMagSetFullscreenTransform ;
type_pMagSetInputTransform pMagSetInputTransform ;
type_pMagShowSystemCursor pMagShowSystemCursor ;
type_pMagSetWindowFilterList pMagSetWindowFilterList ;
2025-10-07 11:20:00 -07:00
type_MagSetFullscreenUseBitmapSmoothing pMagSetFullscreenUseBitmapSmoothing ;
type_pMagSetLensUseBitmapSmoothing pMagSetLensUseBitmapSmoothing ;
2025-01-16 20:52:24 +00:00
type_pMagInitialize pMagInitialize ;
type_pDwmIsCompositionEnabled pDwmIsCompositionEnabled ;
type_pGetPointerType pGetPointerType ;
type_pGetPointerPenInfo pGetPointerPenInfo ;
type_pSystemParametersInfoForDpi pSystemParametersInfoForDpi ;
type_pGetDpiForWindow pGetDpiForWindow ;
type_pSHQueryUserNotificationState pSHQueryUserNotificationState ;
type_pCreateDirect3D11DeviceFromDXGIDevice pCreateDirect3D11DeviceFromDXGIDevice ;
type_pCreateDirect3D11SurfaceFromDXGISurface pCreateDirect3D11SurfaceFromDXGISurface ;
type_pD3D11CreateDevice pD3D11CreateDevice ;
ClassRegistry reg ( _T ( " Software \\ Sysinternals \\ " ) APPNAME ) ;
ComputerGraphicsInit g_GraphicsInit ;
2026-02-03 13:05:31 -08:00
// Event handler to set icon and extended style on dialog creation
class OpenSaveDialogEvents : public IFileDialogEvents
{
public :
OpenSaveDialogEvents ( bool showOnTaskbar = true ) : m_refCount ( 1 ) , m_initialized ( false ) , m_showOnTaskbar ( showOnTaskbar ) { }
// IUnknown
IFACEMETHODIMP QueryInterface ( REFIID riid , void * * ppv )
{
static const QITAB qit [ ] = {
QITABENT ( OpenSaveDialogEvents , IFileDialogEvents ) ,
{ 0 } ,
} ;
return QISearch ( this , qit , riid , ppv ) ;
}
IFACEMETHODIMP_ ( ULONG ) AddRef ( ) { return InterlockedIncrement ( & m_refCount ) ; }
IFACEMETHODIMP_ ( ULONG ) Release ( )
{
ULONG count = InterlockedDecrement ( & m_refCount ) ;
if ( count = = 0 ) delete this ;
return count ;
}
// IFileDialogEvents
IFACEMETHODIMP OnFileOk ( IFileDialog * ) { return S_OK ; }
IFACEMETHODIMP OnFolderChange ( IFileDialog * pfd )
{
if ( ! m_initialized )
{
m_initialized = true ;
wil : : com_ptr < IOleWindow > pWindow ;
if ( SUCCEEDED ( pfd - > QueryInterface ( IID_PPV_ARGS ( & pWindow ) ) ) )
{
HWND hwndDialog = nullptr ;
if ( SUCCEEDED ( pWindow - > GetWindow ( & hwndDialog ) ) & & hwndDialog )
{
if ( m_showOnTaskbar )
{
// Set WS_EX_APPWINDOW extended style
LONG_PTR exStyle = GetWindowLongPtr ( hwndDialog , GWL_EXSTYLE ) ;
SetWindowLongPtr ( hwndDialog , GWL_EXSTYLE , exStyle | WS_EX_APPWINDOW ) ;
}
// Set the dialog icon
HICON hIcon = LoadIcon ( g_hInstance , L " APPICON " ) ;
if ( hIcon )
{
SendMessage ( hwndDialog , WM_SETICON , ICON_BIG , reinterpret_cast < LPARAM > ( hIcon ) ) ;
SendMessage ( hwndDialog , WM_SETICON , ICON_SMALL , reinterpret_cast < LPARAM > ( hIcon ) ) ;
}
}
}
}
return S_OK ;
}
IFACEMETHODIMP OnFolderChanging ( IFileDialog * , IShellItem * ) { return S_OK ; }
IFACEMETHODIMP OnSelectionChange ( IFileDialog * ) { return S_OK ; }
IFACEMETHODIMP OnShareViolation ( IFileDialog * , IShellItem * , FDE_SHAREVIOLATION_RESPONSE * ) { return S_OK ; }
IFACEMETHODIMP OnTypeChange ( IFileDialog * ) { return S_OK ; }
IFACEMETHODIMP OnOverwrite ( IFileDialog * , IShellItem * , FDE_OVERWRITE_RESPONSE * ) { return S_OK ; }
private :
LONG m_refCount ;
bool m_initialized ;
bool m_showOnTaskbar ;
} ;
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
//
2025-11-10 11:57:13 -08:00
// Saves specified filePath to clipboard.
2025-01-16 20:52:24 +00:00
//
//----------------------------------------------------------------------------
bool SaveToClipboard ( const WCHAR * filePath , HWND hwnd )
{
if ( filePath = = NULL | | hwnd = = NULL | | wcslen ( filePath ) = = 0 )
{
return false ;
}
size_t size = sizeof ( DROPFILES ) + sizeof ( WCHAR ) * ( _tcslen ( filePath ) + 1 ) + sizeof ( WCHAR ) ;
HDROP hDrop = static_cast < HDROP > ( GlobalAlloc ( GHND , size ) ) ;
if ( hDrop = = NULL )
{
2025-11-10 11:57:13 -08:00
return false ;
2025-01-16 20:52:24 +00:00
}
DROPFILES * dFiles = static_cast < DROPFILES * > ( GlobalLock ( hDrop ) ) ;
if ( dFiles = = NULL )
{
GlobalFree ( hDrop ) ;
2025-11-10 11:57:13 -08:00
return false ;
2025-01-16 20:52:24 +00:00
}
dFiles - > pFiles = sizeof ( DROPFILES ) ;
2025-11-10 11:57:13 -08:00
dFiles - > fWide = TRUE ;
2025-01-16 20:52:24 +00:00
wcscpy ( reinterpret_cast < WCHAR * > ( & dFiles [ 1 ] ) , filePath ) ;
GlobalUnlock ( hDrop ) ;
if ( OpenClipboard ( hwnd ) )
{
EmptyClipboard ( ) ;
SetClipboardData ( CF_HDROP , hDrop ) ;
CloseClipboard ( ) ;
}
GlobalFree ( hDrop ) ;
return true ;
}
//----------------------------------------------------------------------
//
// OutputDebug
//
//----------------------------------------------------------------------
void OutputDebug ( const TCHAR * format , . . . )
{
# if _DEBUG
TCHAR msg [ 1024 ] ;
va_list va ;
# ifdef _MSC_VER
// For some reason, ARM64 Debug builds causes an analyzer error on va_start: "error C26492: Don't use const_cast to cast away const or volatile (type.3)."
# pragma warning(push)
# pragma warning(disable : 26492)
# endif
va_start ( va , format ) ;
# ifdef _MSC_VER
# pragma warning(pop)
# endif
_vstprintf_s ( msg , format , va ) ;
va_end ( va ) ;
OutputDebugString ( msg ) ;
# endif
}
2026-03-26 13:21:43 +01:00
const wchar_t * HotkeyIdToString ( WPARAM hotkeyId )
{
switch ( hotkeyId )
{
case ZOOM_HOTKEY : return L " ZOOM_HOTKEY " ;
case DRAW_HOTKEY : return L " DRAW_HOTKEY " ;
case BREAK_HOTKEY : return L " BREAK_HOTKEY " ;
case LIVE_HOTKEY : return L " LIVE_HOTKEY " ;
case LIVE_DRAW_HOTKEY : return L " LIVE_DRAW_HOTKEY " ;
case RECORD_HOTKEY : return L " RECORD_HOTKEY " ;
case RECORD_CROP_HOTKEY : return L " RECORD_CROP_HOTKEY " ;
case RECORD_WINDOW_HOTKEY : return L " RECORD_WINDOW_HOTKEY " ;
case SNIP_HOTKEY : return L " SNIP_HOTKEY " ;
case SNIP_SAVE_HOTKEY : return L " SNIP_SAVE_HOTKEY " ;
case DEMOTYPE_HOTKEY : return L " DEMOTYPE_HOTKEY " ;
case DEMOTYPE_RESET_HOTKEY : return L " DEMOTYPE_RESET_HOTKEY " ;
case RECORD_GIF_HOTKEY : return L " RECORD_GIF_HOTKEY " ;
case RECORD_GIF_WINDOW_HOTKEY : return L " RECORD_GIF_WINDOW_HOTKEY " ;
case SAVE_IMAGE_HOTKEY : return L " SAVE_IMAGE_HOTKEY " ;
case SAVE_CROP_HOTKEY : return L " SAVE_CROP_HOTKEY " ;
case COPY_IMAGE_HOTKEY : return L " COPY_IMAGE_HOTKEY " ;
case COPY_CROP_HOTKEY : return L " COPY_CROP_HOTKEY " ;
case SNIP_OCR_HOTKEY : return L " SNIP_OCR_HOTKEY " ;
case SNIP_PANORAMA_HOTKEY : return L " SNIP_PANORAMA_HOTKEY " ;
case SNIP_PANORAMA_SAVE_HOTKEY : return L " SNIP_PANORAMA_SAVE_HOTKEY " ;
default : return L " UNKNOWN_HOTKEY " ;
}
}
static void LogHotkeyRegistrationResult ( const wchar_t * phase , HWND hWnd , int hotkeyId , UINT modifiers , UINT key , BOOL success )
{
# if _DEBUG
const DWORD lastError = success ? 0 : GetLastError ( ) ;
OutputDebug ( L " [Hotkey/%s] hwnd=%p id=%d(%s) mods=0x%X key=0x%X success=%d err=%lu \n " ,
phase ,
hWnd ,
hotkeyId ,
HotkeyIdToString ( hotkeyId ) ,
modifiers ,
key ,
success ? 1 : 0 ,
lastError ) ;
# else
UNREFERENCED_PARAMETER ( phase ) ;
UNREFERENCED_PARAMETER ( hWnd ) ;
UNREFERENCED_PARAMETER ( hotkeyId ) ;
UNREFERENCED_PARAMETER ( modifiers ) ;
UNREFERENCED_PARAMETER ( key ) ;
UNREFERENCED_PARAMETER ( success ) ;
# endif
}
static void LogPanoramaState ( const wchar_t * phase , WPARAM hotkeyId = static_cast < WPARAM > ( - 1 ) )
{
# if _DEBUG
const auto mainWindowStyle = ( g_hWndMain ! = nullptr ) ? static_cast < unsigned long long > ( GetWindowLongPtr ( g_hWndMain , GWL_EXSTYLE ) ) : 0ULL ;
OutputDebug ( L " [Panorama/State] %s hotkey=%ld(%s) active=%d stop=%d recordCropping=%d selectActive=%d mainExStyle=0x%llX \n " ,
phase ,
static_cast < long > ( hotkeyId ) ,
HotkeyIdToString ( hotkeyId ) ,
g_PanoramaCaptureActive ? 1 : 0 ,
g_PanoramaStopRequested ? 1 : 0 ,
g_RecordCropping ? 1 : 0 ,
g_SelectRectangle . IsActive ( ) ? 1 : 0 ,
mainWindowStyle ) ;
# else
UNREFERENCED_PARAMETER ( phase ) ;
UNREFERENCED_PARAMETER ( hotkeyId ) ;
# endif
}
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
//
// InitializeFonts
//
// Return a bold equivalent of either a DPI aware font face for GUI text or
// just the stock object for DEFAULT_GUI_FONT.
//
//----------------------------------------------------------------------------
void InitializeFonts ( HWND hwnd , HFONT * bold )
{
LOGFONT logFont ;
bool haveLogFont = false ;
if ( * bold )
{
DeleteObject ( * bold ) ;
* bold = nullptr ;
}
if ( pSystemParametersInfoForDpi & & pGetDpiForWindow )
{
NONCLIENTMETRICSW metrics { } ;
metrics . cbSize = sizeof ( metrics ) ;
if ( pSystemParametersInfoForDpi ( SPI_GETNONCLIENTMETRICS , sizeof ( metrics ) , & metrics , 0 , pGetDpiForWindow ( hwnd ) ) )
{
CopyMemory ( & logFont , & metrics . lfMessageFont , sizeof ( logFont ) ) ;
haveLogFont = true ;
}
}
if ( ! haveLogFont )
{
auto normal = static_cast < HFONT > ( GetStockObject ( DEFAULT_GUI_FONT ) ) ;
GetObject ( normal , sizeof ( logFont ) , & logFont ) ;
haveLogFont = true ; // for correctness
}
logFont . lfWeight = FW_BOLD ;
* bold = CreateFontIndirect ( & logFont ) ;
}
//----------------------------------------------------------------------------
//
// EnsureForeground
//
//----------------------------------------------------------------------------
void EnsureForeground ( )
{
if ( ! IsWindowVisible ( g_hWndMain ) )
SetForegroundWindow ( g_hWndMain ) ;
}
//----------------------------------------------------------------------------
//
// RestoreForeground
//
//----------------------------------------------------------------------------
void RestoreForeground ( )
{
2025-03-07 12:35:44 -08:00
// If the main window is not visible, move foreground to the next window.
2025-01-16 20:52:24 +00:00
if ( ! IsWindowVisible ( g_hWndMain ) ) {
Updates for check-spelling v0.0.25 (#40386)
## Summary of the Pull Request
- #39572 updated check-spelling but ignored:
> 🐣 Breaking Changes
[Code Scanning action requires a Code Scanning
Ruleset](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset)
If you use SARIF reporting, then instead of the workflow yielding an ❌
when it fails, it will rely on [github-advanced-security
🤖](https://github.com/apps/github-advanced-security) to report the
failure. You will need to adjust your checks for PRs.
This means that check-spelling hasn't been properly doing its job 😦.
I'm sorry, I should have pushed a thing to this repo earlier,...
Anyway, as with most refreshes, this comes with a number of fixes, some
are fixes for typos that snuck in before the 0.0.25 upgrade, some are
for things that snuck in after, some are based on new rules in
spell-check-this, and some are hand written patterns based on running
through this repository a few times.
About the 🐣 **breaking change**: someone needs to create a ruleset for
this repository (see [Code Scanning action requires a Code Scanning
Ruleset: Sample ruleset
](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset#sample-ruleset)).
The alternative to adding a ruleset is to change the condition to not
use sarif for this repository. In general, I think the github
integration from sarif is prettier/more helpful, so I think that it's
the better choice.
You can see an example of it working in:
- https://github.com/check-spelling-sandbox/PowerToys/pull/23
---------
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
2025-07-08 18:16:52 -04:00
// Activate the next window by showing and hiding the main window.
2025-01-16 20:52:24 +00:00
MoveWindow ( g_hWndMain , 0 , 0 , 0 , 0 , FALSE ) ;
ShowWindow ( g_hWndMain , SW_SHOWNA ) ;
ShowWindow ( g_hWndMain , SW_HIDE ) ;
OutputDebug ( L " RESTORE FOREGROUND \n " ) ;
}
}
//----------------------------------------------------------------------------
//
// ErrorDialog
//
//----------------------------------------------------------------------------
VOID ErrorDialog ( HWND hParent , PCTSTR message , DWORD _Error )
{
LPTSTR lpMsgBuf ;
TCHAR errmsg [ 1024 ] ;
FormatMessage ( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM ,
2025-11-10 11:57:13 -08:00
NULL , _Error ,
2025-01-16 20:52:24 +00:00
MAKELANGID ( LANG_NEUTRAL , SUBLANG_DEFAULT ) ,
reinterpret_cast < LPTSTR > ( & lpMsgBuf ) , 0 , NULL ) ;
_stprintf ( errmsg , L " %s: %s " , message , lpMsgBuf ) ;
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Logger : : error ( errmsg ) ;
}
# endif // __ZOOMIT_POWERTOYS__
MessageBox ( hParent , errmsg , APPNAME , MB_OK | MB_ICONERROR ) ;
}
//----------------------------------------------------------------------------
//
// ErrorDialogString
//
//----------------------------------------------------------------------------
VOID ErrorDialogString ( HWND hParent , PCTSTR Message , const wchar_t * _Error )
{
TCHAR errmsg [ 1024 ] ;
_stprintf_s ( errmsg , _countof ( errmsg ) , L " %s: %s " , Message , _Error ) ;
if ( hParent = = g_hWndMain )
{
EnsureForeground ( ) ;
}
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Logger : : error ( errmsg ) ;
}
# endif // __ZOOMIT_POWERTOYS__
MessageBox ( hParent , errmsg , APPNAME , MB_OK | MB_ICONERROR ) ;
if ( hParent = = g_hWndMain )
{
RestoreForeground ( ) ;
}
}
//--------------------------------------------------------------------
//
// SetAutostartFilePath
//
// Sets the file path for later autostart config.
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//--------------------------------------------------------------------
void SetAutostartFilePath ( )
{
HKEY hZoomit ;
DWORD error ;
TCHAR imageFile [ MAX_PATH ] = { 0 } ;
error = RegCreateKeyEx ( HKEY_CURRENT_USER , _T ( " Software \\ Sysinternals \\ Zoomit " ) , 0 ,
0 , 0 , KEY_SET_VALUE , NULL , & hZoomit , NULL ) ;
if ( error = = ERROR_SUCCESS ) {
GetModuleFileName ( NULL , imageFile + 1 , _countof ( imageFile ) - 2 ) ;
imageFile [ 0 ] = ' " ' ;
* ( _tcschr ( imageFile , 0 ) ) = ' " ' ;
error = RegSetValueEx ( hZoomit , L " FilePath " , 0 , REG_SZ , ( BYTE * ) imageFile ,
static_cast < DWORD > ( _tcslen ( imageFile ) + 1 ) * sizeof ( TCHAR ) ) ;
RegCloseKey ( hZoomit ) ;
}
}
//--------------------------------------------------------------------
//
// ConfigureAutostart
//
// Enables or disables Zoomit autostart for the current image file.
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//--------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
bool ConfigureAutostart ( HWND hParent , bool Enable )
2025-01-16 20:52:24 +00:00
{
HKEY hRunKey , hZoomit ;
DWORD error , length , type ;
TCHAR imageFile [ MAX_PATH ] ;
2025-11-10 11:57:13 -08:00
error = RegOpenKeyEx ( HKEY_CURRENT_USER , L " Software \\ Microsoft \\ Windows \\ CurrentVersion \\ Run " ,
2025-01-16 20:52:24 +00:00
0 , KEY_SET_VALUE , & hRunKey ) ;
if ( error = = ERROR_SUCCESS ) {
if ( Enable ) {
2025-11-10 11:57:13 -08:00
error = RegOpenKeyEx ( HKEY_CURRENT_USER , _T ( " Software \\ Sysinternals \\ Zoomit " ) , 0 ,
2025-01-16 20:52:24 +00:00
KEY_QUERY_VALUE , & hZoomit ) ;
if ( error = = ERROR_SUCCESS ) {
length = sizeof ( imageFile ) ;
# ifdef _WIN64
// Unconditionally reset filepath in case this was already set by 32 bit version
2025-11-10 11:57:13 -08:00
SetAutostartFilePath ( ) ;
2025-01-16 20:52:24 +00:00
# endif
error = RegQueryValueEx ( hZoomit , _T ( " Filepath " ) , 0 , & type , ( BYTE * ) imageFile , & length ) ;
RegCloseKey ( hZoomit ) ;
2025-11-10 11:57:13 -08:00
if ( error = = ERROR_SUCCESS ) {
2025-01-16 20:52:24 +00:00
error = RegSetValueEx ( hRunKey , APPNAME , 0 , REG_SZ , ( BYTE * ) imageFile ,
static_cast < DWORD > ( _tcslen ( imageFile ) + 1 ) * sizeof ( TCHAR ) ) ;
}
}
} else {
error = RegDeleteValue ( hRunKey , APPNAME ) ;
if ( error = = ERROR_FILE_NOT_FOUND ) error = ERROR_SUCCESS ;
}
RegCloseKey ( hRunKey ) ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
if ( error ! = ERROR_SUCCESS ) {
ErrorDialog ( hParent , L " Error configuring auto start " , error ) ;
}
return error = = ERROR_SUCCESS ;
}
//--------------------------------------------------------------------
//
// IsAutostartConfigured
//
// Is this version of zoomit configured to autostart.
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//--------------------------------------------------------------------
bool IsAutostartConfigured ( )
{
HKEY hRunKey ;
2025-11-10 11:57:13 -08:00
TCHAR imageFile [ MAX_PATH ] ;
2025-01-16 20:52:24 +00:00
DWORD error , imageFileLength , type ;
2025-11-10 11:57:13 -08:00
error = RegOpenKeyEx ( HKEY_CURRENT_USER , L " Software \\ Microsoft \\ Windows \\ CurrentVersion \\ Run " ,
2025-01-16 20:52:24 +00:00
0 , KEY_QUERY_VALUE , & hRunKey ) ;
if ( error = = ERROR_SUCCESS ) {
imageFileLength = sizeof ( imageFile ) ;
error = RegQueryValueEx ( hRunKey , _T ( " Zoomit " ) , 0 , & type , ( BYTE * ) imageFile , & imageFileLength ) ;
RegCloseKey ( hRunKey ) ;
}
return error = = ERROR_SUCCESS ;
}
# ifndef _WIN64
//--------------------------------------------------------------------
//
// RunningOnWin64
//
// Returns true if this is the 32-bit version of the executable
2025-03-07 12:35:44 -08:00
// and we're on 64-bit Windows.
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//--------------------------------------------------------------------
typedef BOOL ( __stdcall * P_IS_WOW64PROCESS ) (
HANDLE hProcess ,
PBOOL Wow64Process
) ;
2025-11-10 11:57:13 -08:00
BOOL
2025-01-16 20:52:24 +00:00
RunningOnWin64 (
2025-11-10 11:57:13 -08:00
VOID
2025-01-16 20:52:24 +00:00
)
{
P_IS_WOW64PROCESS pIsWow64Process ;
BOOL isWow64 = FALSE ;
pIsWow64Process = ( P_IS_WOW64PROCESS ) GetProcAddress ( GetModuleHandle ( _T ( " kernel32.dll " ) ) ,
" IsWow64Process " ) ;
if ( pIsWow64Process ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
pIsWow64Process ( GetCurrentProcess ( ) , & isWow64 ) ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
return isWow64 ;
}
//--------------------------------------------------------------------
//
// ExtractImageResource
//
2025-11-10 11:57:13 -08:00
// Extracts the specified file that is located in a resource for
2025-01-16 20:52:24 +00:00
// this executable.
//
//--------------------------------------------------------------------
BOOLEAN ExtractImageResource ( PTCHAR ResourceName , PTCHAR TargetFile )
{
HRSRC hResource ;
HGLOBAL hImageResource ;
2025-11-10 11:57:13 -08:00
DWORD dwImageSize ;
2025-01-16 20:52:24 +00:00
LPVOID lpvImage ;
FILE * hFile ;
// Locate the resource
2025-11-10 11:57:13 -08:00
hResource = FindResource ( NULL , ResourceName , _T ( " BINRES " ) ) ;
if ( ! hResource )
2025-01-16 20:52:24 +00:00
return FALSE ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
hImageResource = LoadResource ( NULL , hResource ) ;
dwImageSize = SizeofResource ( NULL , hResource ) ;
lpvImage = LockResource ( hImageResource ) ;
// Now copy it out
_tfopen_s ( & hFile , TargetFile , _T ( " wb " ) ) ;
if ( hFile = = NULL ) return FALSE ;
fwrite ( lpvImage , 1 , dwImageSize , hFile ) ;
fclose ( hFile ) ;
return TRUE ;
}
//--------------------------------------------------------------------
//
// Run64bitVersion
//
// Returns true if this is the 32-bit version of the executable
2025-03-07 12:35:44 -08:00
// and we're on 64-bit Windows.
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//--------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
DWORD
Run64bitVersion (
2025-01-16 20:52:24 +00:00
void
)
{
TCHAR szPath [ MAX_PATH ] ;
TCHAR originalPath [ MAX_PATH ] ;
TCHAR tmpPath [ MAX_PATH ] ;
SHELLEXECUTEINFO info = { 0 } ;
if ( GetModuleFileName ( NULL , szPath , sizeof ( szPath ) / sizeof ( TCHAR ) ) = = 0 ) {
return - 1 ;
}
_tcscpy_s ( originalPath , _countof ( originalPath ) , szPath ) ;
* _tcsrchr ( originalPath , ' . ' ) = 0 ;
_tcscat_s ( originalPath , _countof ( szPath ) , _T ( " 64.exe " ) ) ;
//
// Extract the 64-bit version
//
ExpandEnvironmentStrings ( L " %TEMP% " , tmpPath , sizeof tmpPath / sizeof ( TCHAR ) ) ;
_tcscat_s ( tmpPath , _countof ( tmpPath ) , _tcsrchr ( originalPath , ' \\ ' ) ) ;
_tcscpy_s ( szPath , _countof ( szPath ) , tmpPath ) ;
if ( ! ExtractImageResource ( _T ( " RCZOOMIT64 " ) , szPath ) ) {
if ( GetFileAttributes ( szPath ) = = INVALID_FILE_ATTRIBUTES ) {
ErrorDialog ( NULL , _T ( " Error launching 64-bit version " ) , GetLastError ( ) ) ;
return - 1 ;
}
}
info . cbSize = sizeof ( info ) ;
info . fMask = SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS ;
info . lpFile = szPath ;
info . lpParameters = GetCommandLine ( ) ;
info . nShow = SW_SHOWNORMAL ;
if ( ! ShellExecuteEx ( & info ) ) {
ErrorDialog ( NULL , _T ( " Error launching 64-bit version " ) , GetLastError ( ) ) ;
DeleteFile ( szPath ) ;
return - 1 ;
}
WaitForSingleObject ( info . hProcess , INFINITE ) ;
DWORD result ;
GetExitCodeProcess ( info . hProcess , & result ) ;
CloseHandle ( info . hProcess ) ;
DeleteFile ( szPath ) ;
return result ;
}
# endif
//----------------------------------------------------------------------------
//
// IsPresentationMode
//
//----------------------------------------------------------------------------
BOOLEAN IsPresentationMode ( )
{
QUERY_USER_NOTIFICATION_STATE pUserState ;
pSHQueryUserNotificationState ( & pUserState ) ;
return pUserState = = QUNS_PRESENTATION_MODE ;
}
//----------------------------------------------------------------------------
//
// EnableDisableSecondaryDisplay
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
// Creates a second display on the secondary monitor for displaying the
2025-11-10 11:57:13 -08:00
// break timer.
2025-01-16 20:52:24 +00:00
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
LONG EnableDisableSecondaryDisplay ( HWND hWnd , BOOLEAN Enable ,
PDEVMODE OriginalDevMode )
2025-01-16 20:52:24 +00:00
{
LONG result ;
DEVMODE devMode { } ;
if ( Enable ) {
//
// Prepare the position of Display 2 to be right to the right of Display 1
//
devMode . dmSize = sizeof ( devMode ) ;
devMode . dmDriverExtra = 0 ;
2025-11-10 11:57:13 -08:00
EnumDisplaySettings ( NULL , ENUM_CURRENT_SETTINGS , & devMode ) ;
2025-01-16 20:52:24 +00:00
* OriginalDevMode = devMode ;
//
// Enable display 2 in the registry
//
devMode . dmPosition . x = devMode . dmPelsWidth ;
devMode . dmFields = DM_POSITION |
DM_DISPLAYORIENTATION |
DM_BITSPERPEL |
DM_PELSWIDTH |
DM_PELSHEIGHT |
DM_DISPLAYFLAGS |
2025-11-10 11:57:13 -08:00
DM_DISPLAYFREQUENCY ;
2025-01-16 20:52:24 +00:00
result = ChangeDisplaySettingsEx ( L " \\ \\ . \\ DISPLAY2 " ,
& devMode ,
NULL ,
CDS_NORESET | CDS_UPDATEREGISTRY ,
NULL ) ;
} else {
OriginalDevMode - > dmFields = DM_POSITION |
DM_DISPLAYORIENTATION |
DM_BITSPERPEL |
DM_PELSWIDTH |
DM_PELSHEIGHT |
DM_DISPLAYFLAGS |
DM_DISPLAYFREQUENCY ;
result = ChangeDisplaySettingsEx ( L " \\ \\ . \\ DISPLAY2 " ,
OriginalDevMode ,
NULL ,
CDS_NORESET | CDS_UPDATEREGISTRY ,
NULL ) ;
}
//
// Update the hardware
//
if ( result = = DISP_CHANGE_SUCCESSFUL ) {
if ( ! ChangeDisplaySettingsEx ( NULL , NULL , NULL , 0 , NULL ) ) {
result = GetLastError ( ) ;
}
//
// If enabling, move zoomit to the second monitor
//
if ( Enable & & result = = DISP_CHANGE_SUCCESSFUL ) {
SetWindowPos ( FindWindowW ( L " ZoomitClass " , NULL ) ,
NULL ,
devMode . dmPosition . x ,
0 ,
0 ,
0 ,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE ) ;
SetCursorPos ( devMode . dmPosition . x + 1 , devMode . dmPosition . y + 1 ) ;
}
}
return result ;
}
//----------------------------------------------------------------------------
//
// GetLineBounds
//
// Gets the rectangle bounding a line, taking into account pen width
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
Gdiplus : : Rect GetLineBounds ( POINT p1 , POINT p2 , int penWidth )
{
2025-11-10 11:57:13 -08:00
Gdiplus : : Rect rect ( min ( p1 . x , p2 . x ) , min ( p1 . y , p2 . y ) ,
2025-01-16 20:52:24 +00:00
abs ( p1 . x - p2 . x ) , abs ( p1 . y - p2 . y ) ) ;
rect . Inflate ( penWidth , penWidth ) ;
return rect ;
}
//----------------------------------------------------------------------------
//
// InvalidateGdiplusRect
//
// Invalidate portion of window specified by Gdiplus::Rect
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
void InvalidateGdiplusRect ( HWND hWnd , Gdiplus : : Rect BoundsRect )
{
RECT lineBoundsGdi ;
lineBoundsGdi . left = BoundsRect . X ;
lineBoundsGdi . top = BoundsRect . Y ;
lineBoundsGdi . right = BoundsRect . X + BoundsRect . Width ;
lineBoundsGdi . bottom = BoundsRect . Y + BoundsRect . Height ;
InvalidateRect ( hWnd , & lineBoundsGdi , FALSE ) ;
}
//----------------------------------------------------------------------------
//
// CreateGdiplusBitmap
//
2025-11-10 11:57:13 -08:00
// Creates a gdiplus bitmap of the specified region of the HDC.
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
Gdiplus : : Bitmap * CreateGdiplusBitmap ( HDC hDc , int x , int y , int Width , int Height )
{
HBITMAP hBitmap = CreateCompatibleBitmap ( hDc , Width , Height ) ;
// Create a device context for the new bitmap
HDC hdcNewBitmap = CreateCompatibleDC ( hDc ) ;
SelectObject ( hdcNewBitmap , hBitmap ) ;
// Copy from the oldest undo bitmap to the new bitmap using the lineBounds as the source rectangle
BitBlt ( hdcNewBitmap , 0 , 0 , Width , Height , hDc , x , y , SRCCOPY ) ;
Gdiplus : : Bitmap * blurBitmap = new Gdiplus : : Bitmap ( hBitmap , NULL ) ;
DeleteDC ( hdcNewBitmap ) ;
DeleteObject ( hBitmap ) ;
2025-11-10 11:57:13 -08:00
return blurBitmap ;
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// CreateBitmapMemoryDIB
//
// Creates a memory DC and DIB for the specified region of the screen.
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
BYTE * CreateBitmapMemoryDIB ( HDC hdcScreenCompat , HDC hBitmapDc , Gdiplus : : Rect * lineBounds ,
HDC * hdcMem , HBITMAP * hDIBOrig , HBITMAP * hPreviousBitmap )
{
// Create a memory DIB for the relevant region of the original bitmap
BITMAPINFO bmiOrig = { 0 } ;
bmiOrig . bmiHeader . biSize = sizeof ( BITMAPINFOHEADER ) ;
bmiOrig . bmiHeader . biWidth = lineBounds - > Width ;
bmiOrig . bmiHeader . biHeight = - lineBounds - > Height ; // Top-down DIB
bmiOrig . bmiHeader . biPlanes = 1 ;
bmiOrig . bmiHeader . biBitCount = 32 ; // 32 bits per pixel
bmiOrig . bmiHeader . biCompression = BI_RGB ;
VOID * pDIBBitsOrig ;
* hDIBOrig = CreateDIBSection ( hdcScreenCompat , & bmiOrig , DIB_RGB_COLORS , & pDIBBitsOrig , NULL , 0 ) ;
if ( * hDIBOrig = = NULL ) {
OutputDebug ( L " NULL DIB: %d \n " , GetLastError ( ) ) ;
OutputDebug ( L " lineBounds: %d %d %d %d \n " , lineBounds - > X , lineBounds - > Y , lineBounds - > Width , lineBounds - > Height ) ;
return NULL ;
}
* hdcMem = CreateCompatibleDC ( hdcScreenCompat ) ;
* hPreviousBitmap = static_cast < HBITMAP > ( SelectObject ( * hdcMem , * hDIBOrig ) ) ;
// Copy the relevant part of hdcScreenCompat to the DIB
BitBlt ( * hdcMem , 0 , 0 , lineBounds - > Width , lineBounds - > Height , hBitmapDc , lineBounds - > X , lineBounds - > Y , SRCCOPY ) ;
// Pointer to the DIB bits
return static_cast < BYTE * > ( pDIBBitsOrig ) ;
}
//----------------------------------------------------------------------------
//
// LockGdiPlusBitmap
//
2025-11-10 11:57:13 -08:00
// Locks the Gdi+ bitmap so that we can access its pixels in memory.
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
# ifdef _MSC_VER
// Analyzers want us to use a scoped object instead of new. But given all the operations done in Bitmaps it seems better to leave it as a heap object.
# pragma warning(push)
# pragma warning(disable : 26402)
# endif
Gdiplus : : BitmapData * LockGdiPlusBitmap ( Gdiplus : : Bitmap * Bitmap )
{
Gdiplus : : BitmapData * lineData = new Gdiplus : : BitmapData ( ) ;
Bitmap - > GetPixelFormat ( ) ;
Gdiplus : : Rect lineBitmapBounds ( 0 , 0 , Bitmap - > GetWidth ( ) , Bitmap - > GetHeight ( ) ) ;
Bitmap - > LockBits ( & lineBitmapBounds , Gdiplus : : ImageLockModeRead ,
Bitmap - > GetPixelFormat ( ) , lineData ) ;
2025-11-10 11:57:13 -08:00
return lineData ;
2025-01-16 20:52:24 +00:00
}
# ifdef _MSC_VER
# pragma warning(pop)
# endif
//----------------------------------------------------------------------------
//
// BlurScreen
//
2025-11-10 11:57:13 -08:00
// Blur the portion of the screen by copying a blurred bitmap with the
// specified shape.
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
void BlurScreen ( HDC hdcScreenCompat , Gdiplus : : Rect * lineBounds ,
2025-01-16 20:52:24 +00:00
Gdiplus : : Bitmap * BlurBitmap , BYTE * pPixels )
{
HDC hdcDIB ;
HBITMAP hDibOrigBitmap , hDibBitmap ;
BYTE * pDestPixels = CreateBitmapMemoryDIB ( hdcScreenCompat , hdcScreenCompat , lineBounds ,
& hdcDIB , & hDibBitmap , & hDibOrigBitmap ) ;
// Iterate through the pixels
for ( int y = 0 ; y < lineBounds - > Height ; + + y ) {
for ( int x = 0 ; x < lineBounds - > Width ; + + x ) {
int index = ( y * lineBounds - > Width * 4 ) + ( x * 4 ) ; // Assuming 4 bytes per pixel
// BYTE b = pPixels[index + 0]; // Blue channel
// BYTE g = pPixels[index + 1]; // Green channel
// BYTE r = pPixels[index + 2]; // Red channel
BYTE a = pPixels [ index + 3 ] ; // Alpha channel
// Check if this is a drawn pixel
if ( a ! = 0 ) {
// get the blur pixel
Gdiplus : : Color pixel ;
BlurBitmap - > GetPixel ( x , y , & pixel ) ;
COLORREF newPixel = pixel . GetValue ( ) & 0xFFFFFF ;
pDestPixels [ index + 0 ] = GetRValue ( newPixel ) ;
pDestPixels [ index + 1 ] = GetGValue ( newPixel ) ;
pDestPixels [ index + 2 ] = GetBValue ( newPixel ) ;
}
}
}
// Copy the updated DIB back to hdcScreenCompat
BitBlt ( hdcScreenCompat , lineBounds - > X , lineBounds - > Y , lineBounds - > Width , lineBounds - > Height , hdcDIB , 0 , 0 , SRCCOPY ) ;
// Clean up
SelectObject ( hdcDIB , hDibOrigBitmap ) ;
DeleteObject ( hDibBitmap ) ;
DeleteDC ( hdcDIB ) ;
}
//----------------------------------------------------------------------------
//
// BitmapBlur
//
2025-11-10 11:57:13 -08:00
// Blurs the bitmap.
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
void BitmapBlur ( Gdiplus : : Bitmap * hBitmap )
{
// Git bitmap size
Gdiplus : : Size bitmapSize ;
bitmapSize . Width = hBitmap - > GetWidth ( ) ;
bitmapSize . Height = hBitmap - > GetHeight ( ) ;
// Blur the new bitmap
Gdiplus : : Blur blurObject ;
Gdiplus : : BlurParams blurParams ;
blurParams . radius = g_BlurRadius ;
blurParams . expandEdge = FALSE ;
blurObject . SetParameters ( & blurParams ) ;
// Apply blur to image
RECT linesRect ;
linesRect . left = 0 ;
linesRect . top = 0 ;
linesRect . right = bitmapSize . Width ;
linesRect . bottom = bitmapSize . Height ;
hBitmap - > ApplyEffect ( & blurObject , & linesRect ) ;
}
//----------------------------------------------------------------------------
//
// DrawBlurredShape
//
// Blur a shaped region of the screen.
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
void DrawBlurredShape ( DWORD Shape , Gdiplus : : Pen * pen , HDC hdcScreenCompat , Gdiplus : : Graphics * dstGraphics ,
int x1 , int y1 , int x2 , int y2 )
{
// Create a new bitmap that's the size of the area covered by the line + 2 * g_PenWidth
Gdiplus : : Rect lineBounds ( min ( x1 , x2 ) , min ( y1 , y2 ) , abs ( x2 - x1 ) , abs ( y2 - y1 ) ) ;
// Expand for line drawing
2025-11-10 11:57:13 -08:00
if ( Shape = = DRAW_LINE )
2025-01-16 20:52:24 +00:00
lineBounds . Inflate ( static_cast < int > ( g_PenWidth / 2 ) , static_cast < int > ( g_PenWidth / 2 ) ) ;
Gdiplus : : Bitmap * lineBitmap = new Gdiplus : : Bitmap ( lineBounds . Width , lineBounds . Height , PixelFormat32bppARGB ) ;
Gdiplus : : Graphics lineGraphics ( lineBitmap ) ;
static const auto blackBrush = Gdiplus : : SolidBrush ( Gdiplus : : Color : : Black ) ;
switch ( Shape ) {
case DRAW_RECTANGLE :
lineGraphics . FillRectangle ( & blackBrush , 0 , 0 , lineBounds . Width , lineBounds . Height ) ;
break ;
case DRAW_ELLIPSE :
lineGraphics . FillEllipse ( & blackBrush , 0 , 0 , lineBounds . Width , lineBounds . Height ) ;
break ;
case DRAW_LINE :
OutputDebug ( L " BLUR_LINE: %d %d \n " , lineBounds . Width , lineBounds . Height ) ;
lineGraphics . DrawLine ( pen , x1 - lineBounds . X , y1 - lineBounds . Y , x2 - lineBounds . X , y2 - lineBounds . Y ) ;
break ;
}
Gdiplus : : BitmapData * lineData = LockGdiPlusBitmap ( lineBitmap ) ;
BYTE * pPixels = static_cast < BYTE * > ( lineData - > Scan0 ) ;
// Create a GDI bitmap that's the size of the lineBounds rectangle
Gdiplus : : Bitmap * blurBitmap = CreateGdiplusBitmap ( hdcScreenCompat ,
lineBounds . X , lineBounds . Y , lineBounds . Width , lineBounds . Height ) ;
// Blur it
BitmapBlur ( blurBitmap ) ;
BlurScreen ( hdcScreenCompat , & lineBounds , blurBitmap , pPixels ) ;
// Unlock the bits
lineBitmap - > UnlockBits ( lineData ) ;
delete lineBitmap ;
delete blurBitmap ;
}
//----------------------------------------------------------------------------
//
// CreateDrawingBitmap
//
// Create a bitmap to draw on.
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
Gdiplus : : Bitmap * CreateDrawingBitmap ( Gdiplus : : Rect lineBounds )
{
Gdiplus : : Bitmap * lineBitmap = new Gdiplus : : Bitmap ( lineBounds . Width , lineBounds . Height , PixelFormat32bppARGB ) ;
Gdiplus : : Graphics lineGraphics ( lineBitmap ) ;
return lineBitmap ;
}
//----------------------------------------------------------------------------
//
// DrawBitmapLine
//
2025-11-10 11:57:13 -08:00
// Creates a bitmap and draws a line on it.
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
Gdiplus : : Bitmap * DrawBitmapLine ( Gdiplus : : Rect lineBounds , POINT p1 , POINT p2 , Gdiplus : : Pen * pen )
{
Gdiplus : : Bitmap * lineBitmap = new Gdiplus : : Bitmap ( lineBounds . Width , lineBounds . Height , PixelFormat32bppARGB ) ;
Gdiplus : : Graphics lineGraphics ( lineBitmap ) ;
// Draw the line on the temporary bitmap
lineGraphics . DrawLine ( pen , static_cast < INT > ( p1 . x - lineBounds . X ) , static_cast < INT > ( p1 . y - lineBounds . Y ) ,
static_cast < INT > ( p2 . x - lineBounds . X ) , static_cast < INT > ( p2 . y - lineBounds . Y ) ) ;
return lineBitmap ;
}
//----------------------------------------------------------------------------
//
// ColorFromColorRef
//
// Returns a color object from the colorRef that includes the alpha channel
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
Gdiplus : : Color ColorFromColorRef ( DWORD colorRef ) {
BYTE a = ( colorRef > > 24 ) & 0xFF ; // Extract the alpha channel value
BYTE b = ( colorRef > > 16 ) & 0xFF ; // Extract the red channel value
BYTE g = ( colorRef > > 8 ) & 0xFF ; // Extract the green channel value
BYTE r = colorRef & 0xFF ; // Extract the blue channel value
OutputDebug ( L " ColorFromColorRef: %d %d %d %d \n " , a , r , g , b ) ;
return Gdiplus : : Color ( a , r , g , b ) ;
}
//----------------------------------------------------------------------------
//
// AdjustHighlighterColor
//
2025-11-10 11:57:13 -08:00
// Lighten the color.
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
void AdjustHighlighterColor ( BYTE * red , BYTE * green , BYTE * blue ) {
// Adjust the color to be more visible
* red = min ( 0xFF , * red ? * red + 0x40 : * red + 0x80 ) ;
* green = min ( 0xFF , * green ? * green + 0x40 : * green + 0x80 ) ;
* blue = min ( 0xFF , * blue ? * blue + 0x40 : * blue + 0x80 ) ;
}
//----------------------------------------------------------------------------
//
// BlendColors
//
// Blends two colors together using the alpha channel of the second color.
2025-11-10 11:57:13 -08:00
// The highlighter is the second color.
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
COLORREF BlendColors ( COLORREF color1 , const Gdiplus : : Color & color2 ) {
BYTE redResult , greenResult , blueResult ;
// Extract the channels from the COLORREF
BYTE red1 = GetRValue ( color1 ) ;
BYTE green1 = GetGValue ( color1 ) ;
BYTE blue1 = GetBValue ( color1 ) ;
// Get the channels and alpha from the Gdiplus::Color
BYTE blue2 = color2 . GetRed ( ) ;
BYTE green2 = color2 . GetGreen ( ) ;
BYTE red2 = color2 . GetBlue ( ) ;
float alpha2 = color2 . GetAlpha ( ) / 255.0f ; // Normalize to [0, 1]
//alpha2 /= 2; // Use half the alpha for higher contrast
// Don't blend grey's as much
// int minValue = min(red1, min(green1, blue1));
// int maxValue = max(red1, max(green1, blue1));
if ( TRUE ) { // red1 > 0x10 && red1 < 0xC0 && (maxValue - minValue < 0x40)) {
2025-11-10 11:57:13 -08:00
// This does a standard bright highlight
2025-01-16 20:52:24 +00:00
alpha2 = 0 ;
AdjustHighlighterColor ( & red2 , & green2 , & blue2 ) ;
redResult = red2 & red1 ;
greenResult = green2 & green1 ;
blueResult = blue2 & blue1 ;
}
else {
// Blend each channel
redResult = static_cast < BYTE > ( red2 * alpha2 + red1 * ( 1 - alpha2 ) ) ;
greenResult = static_cast < BYTE > ( green2 * alpha2 + green1 * ( 1 - alpha2 ) ) ;
blueResult = static_cast < BYTE > ( blue2 * alpha2 + blue1 * ( 1 - alpha2 ) ) ;
}
// Combine the result channels back into a COLORREF
return RGB ( redResult , greenResult , blueResult ) ;
}
//----------------------------------------------------------------------------
//
// DrawHighlightedShape
//
// Draws the shape with the highlighter color.
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
void DrawHighlightedShape ( DWORD Shape , HDC hdcScreenCompat , Gdiplus : : Brush * pBrush ,
2025-01-16 20:52:24 +00:00
Gdiplus : : Pen * pPen , int x1 , int y1 , int x2 , int y2 )
{
// Create a new bitmap that's the size of the area covered by the line + 2 * g_PenWidth
Gdiplus : : Rect lineBounds ( min ( x1 , x2 ) , min ( y1 , y2 ) , abs ( x2 - x1 ) , abs ( y2 - y1 ) ) ;
2025-10-07 11:20:00 -07:00
OutputDebug ( L " DrawHighlightedShape \n " ) ;
2025-01-16 20:52:24 +00:00
// Expand for line drawing
if ( Shape = = DRAW_LINE )
lineBounds . Inflate ( static_cast < int > ( g_PenWidth / 2 ) , static_cast < int > ( g_PenWidth / 2 ) ) ;
Gdiplus : : Bitmap * lineBitmap = CreateDrawingBitmap ( lineBounds ) ;
Gdiplus : : Graphics lineGraphics ( lineBitmap ) ;
switch ( Shape ) {
case DRAW_RECTANGLE :
lineGraphics . FillRectangle ( pBrush , 0 , 0 , lineBounds . Width , lineBounds . Height ) ;
break ;
case DRAW_ELLIPSE :
lineGraphics . FillEllipse ( pBrush , 0 , 0 , lineBounds . Width , lineBounds . Height ) ;
2025-11-10 11:57:13 -08:00
break ;
2025-01-16 20:52:24 +00:00
case DRAW_LINE :
lineGraphics . DrawLine ( pPen , x1 - lineBounds . X , y1 - lineBounds . Y , x2 - lineBounds . X , y2 - lineBounds . Y ) ;
break ;
}
Gdiplus : : BitmapData * lineData = LockGdiPlusBitmap ( lineBitmap ) ;
BYTE * pPixels = static_cast < BYTE * > ( lineData - > Scan0 ) ;
// Create a DIB section for efficient pixel manipulation
BITMAPINFO bmi = { 0 } ;
bmi . bmiHeader . biSize = sizeof ( BITMAPINFOHEADER ) ;
bmi . bmiHeader . biWidth = lineBounds . Width ;
bmi . bmiHeader . biHeight = - lineBounds . Height ; // Top-down DIB
bmi . bmiHeader . biPlanes = 1 ;
bmi . bmiHeader . biBitCount = 32 ; // 32 bits per pixel
bmi . bmiHeader . biCompression = BI_RGB ;
VOID * pDIBBits ;
HBITMAP hDIB = CreateDIBSection ( hdcScreenCompat , & bmi , DIB_RGB_COLORS , & pDIBBits , NULL , 0 ) ;
HDC hdcDIB = CreateCompatibleDC ( hdcScreenCompat ) ;
SelectObject ( hdcDIB , hDIB ) ;
// Copy the relevant part of hdcScreenCompat to the DIB
BitBlt ( hdcDIB , 0 , 0 , lineBounds . Width , lineBounds . Height , hdcScreenCompat , lineBounds . X , lineBounds . Y , SRCCOPY ) ;
// Pointer to the DIB bits
BYTE * pDestPixels = static_cast < BYTE * > ( pDIBBits ) ;
// Pointer to screen bits
HDC hdcDIBOrig ;
HBITMAP hDibOrigBitmap , hDibBitmap ;
BYTE * pDestPixels2 = CreateBitmapMemoryDIB ( hdcScreenCompat , hdcScreenCompat , & lineBounds ,
& hdcDIBOrig , & hDibBitmap , & hDibOrigBitmap ) ;
for ( int y = 0 ; y < lineBounds . Height ; + + y ) {
for ( int x = 0 ; x < lineBounds . Width ; + + x ) {
int index = ( y * lineBounds . Width * 4 ) + ( x * 4 ) ; // Assuming 4 bytes per pixel
// BYTE b = pPixels[index + 0]; // Blue channel
// BYTE g = pPixels[index + 1]; // Green channel
// BYTE r = pPixels[index + 2]; // Red channel
BYTE a = pPixels [ index + 3 ] ; // Alpha channel
// Check if this is a drawn pixel
if ( a ! = 0 ) {
// Assuming pDestPixels is a valid pointer to the destination bitmap's pixel data
BYTE destB = pDestPixels2 [ index + 0 ] ; // Blue channel
BYTE destG = pDestPixels2 [ index + 1 ] ; // Green channel
BYTE destR = pDestPixels2 [ index + 2 ] ; // Red channel
// Create a COLORREF value from the destination pixel data
COLORREF currentPixel = RGB ( destR , destG , destB ) ;
// Blend the colors
COLORREF newPixel = BlendColors ( currentPixel , g_PenColor ) ;
// Update the destination pixel data with the new color
pDestPixels [ index + 0 ] = GetBValue ( newPixel ) ;
pDestPixels [ index + 1 ] = GetGValue ( newPixel ) ;
pDestPixels [ index + 2 ] = GetRValue ( newPixel ) ;
}
}
}
// Copy the updated DIB back to hdcScreenCompat
BitBlt ( hdcScreenCompat , lineBounds . X , lineBounds . Y , lineBounds . Width , lineBounds . Height , hdcDIB , 0 , 0 , SRCCOPY ) ;
// Clean up
DeleteObject ( hDIB ) ;
DeleteDC ( hdcDIB ) ;
SelectObject ( hdcDIBOrig , hDibOrigBitmap ) ;
DeleteObject ( hDibBitmap ) ;
DeleteDC ( hdcDIBOrig ) ;
// Invalidate the updated rectangle
2025-10-07 11:20:00 -07:00
//InvalidateGdiplusRect(hWnd, lineBounds);
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// CreateFadedDesktopBackground
//
2025-11-10 11:57:13 -08:00
// Creates a snapshot of the desktop that's faded and alpha blended with
2025-01-16 20:52:24 +00:00
// black.
//
//----------------------------------------------------------------------------
HBITMAP CreateFadedDesktopBackground ( HDC hdc , LPRECT rcScreen , LPRECT rcCrop )
{
// create bitmap
int width = rcScreen - > right - rcScreen - > left ;
int height = rcScreen - > bottom - rcScreen - > top ;
HDC hdcScreen = hdc ;
HDC hdcMem = CreateCompatibleDC ( hdcScreen ) ;
HBITMAP hBitmap = CreateCompatibleBitmap ( hdcScreen , width , height ) ;
HBITMAP hOld = static_cast < HBITMAP > ( SelectObject ( hdcMem , hBitmap ) ) ;
HBRUSH hBrush = CreateSolidBrush ( RGB ( 0 , 0 , 0 ) ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// start with black background
FillRect ( hdcMem , rcScreen , hBrush ) ;
if ( rcCrop ! = NULL & & rcCrop - > left ! = - 1 ) {
// copy screen contents that are not cropped
BitBlt ( hdcMem , rcCrop - > left , rcCrop - > top , rcCrop - > right - rcCrop - > left ,
rcCrop - > bottom - rcCrop - > top , hdcScreen , rcCrop - > left , rcCrop - > top , SRCCOPY ) ;
}
// blend screen contents into it
BLENDFUNCTION blend = { 0 } ;
blend . BlendOp = AC_SRC_OVER ;
blend . BlendFlags = 0 ;
blend . SourceConstantAlpha = 0x4F ;
blend . AlphaFormat = 0 ;
2025-11-10 11:57:13 -08:00
AlphaBlend ( hdcMem , 0 , 0 , width , height ,
hdcScreen , rcScreen - > left , rcScreen - > top ,
2025-01-16 20:52:24 +00:00
width , height , blend ) ;
SelectObject ( hdcMem , hOld ) ;
DeleteDC ( hdcMem ) ;
DeleteObject ( hBrush ) ;
ReleaseDC ( NULL , hdcScreen ) ;
return hBitmap ;
}
//----------------------------------------------------------------------------
//
// AdjustToMoveBoundary
//
// Shifts to accomodate move boundary.
//
//----------------------------------------------------------------------------
void AdjustToMoveBoundary ( float zoomLevel , int * coordinate , int cursor , int size , int max )
{
int diff = static_cast < int > ( static_cast < float > ( size ) / static_cast < float > ( LIVEZOOM_MOVE_REGIONS ) ) ;
2025-11-10 11:57:13 -08:00
if ( cursor - * coordinate < diff )
* coordinate = max ( 0 , cursor - diff ) ;
else if ( ( * coordinate + size ) - cursor < diff )
2025-01-16 20:52:24 +00:00
* coordinate = min ( cursor + diff - size , max - size ) ;
}
//----------------------------------------------------------------------------
//
// GetZoomedTopLeftCoordinates
//
// Gets the left top coordinate of the zoomed area of the screen
//
//----------------------------------------------------------------------------
void GetZoomedTopLeftCoordinates ( float zoomLevel , POINT * cursorPos , int * x , int width , int * y , int height )
{
// smoother and more natural zoom in
float scaledWidth = width / zoomLevel ;
float scaledHeight = height / zoomLevel ;
* x = max ( 0 , min ( ( int ) ( width - scaledWidth ) , ( int ) ( cursorPos - > x - ( int ) ( ( ( float ) cursorPos - > x / ( float ) width ) * scaledWidth ) ) ) ) ;
AdjustToMoveBoundary ( zoomLevel , x , cursorPos - > x , static_cast < int > ( scaledWidth ) , width ) ;
* y = max ( 0 , min ( ( int ) ( height - scaledHeight ) , ( int ) ( cursorPos - > y - ( int ) ( ( ( float ) cursorPos - > y / ( float ) height ) * scaledHeight ) ) ) ) ;
AdjustToMoveBoundary ( zoomLevel , y , cursorPos - > y , static_cast < int > ( scaledHeight ) , height ) ;
}
//----------------------------------------------------------------------------
//
// ScaleImage
//
2025-11-10 11:57:13 -08:00
// Use gdi+ for anti-aliased bitmap stretching.
2025-01-16 20:52:24 +00:00
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
void ScaleImage ( HDC hdcDst , float xDst , float yDst , float wDst , float hDst ,
2025-01-16 20:52:24 +00:00
HBITMAP bmSrc , float xSrc , float ySrc , float wSrc , float hSrc )
{
Gdiplus : : Graphics dstGraphics ( hdcDst ) ;
{
Gdiplus : : Bitmap srcBitmap ( bmSrc , NULL ) ;
2025-10-07 11:20:00 -07:00
// Use high quality interpolation when smooth image is enabled
if ( g_SmoothImage ) {
dstGraphics . SetInterpolationMode ( Gdiplus : : InterpolationModeHighQuality ) ;
} else {
dstGraphics . SetInterpolationMode ( Gdiplus : : InterpolationModeLowQuality ) ;
}
2025-01-16 20:52:24 +00:00
dstGraphics . SetPixelOffsetMode ( Gdiplus : : PixelOffsetModeHalf ) ;
dstGraphics . DrawImage ( & srcBitmap , Gdiplus : : RectF ( xDst , yDst , wDst , hDst ) , xSrc , ySrc , wSrc , hSrc , Gdiplus : : UnitPixel ) ;
}
}
//----------------------------------------------------------------------------
//
// GetEncoderClsid
//
//----------------------------------------------------------------------------
int GetEncoderClsid ( const WCHAR * format , CLSID * pClsid )
{
UINT num = 0 ; // number of image encoders
UINT size = 0 ; // size of the image encoder array in bytes
using namespace Gdiplus ;
ImageCodecInfo * pImageCodecInfo = NULL ;
GetImageEncodersSize ( & num , & size ) ;
if ( size = = 0 )
return - 1 ; // Failure
pImageCodecInfo = static_cast < ImageCodecInfo * > ( malloc ( size ) ) ;
if ( pImageCodecInfo = = NULL )
return - 1 ; // Failure
GetImageEncoders ( num , size , pImageCodecInfo ) ;
for ( UINT j = 0 ; j < num ; + + j )
{
if ( wcscmp ( pImageCodecInfo [ j ] . MimeType , format ) = = 0 )
{
* pClsid = pImageCodecInfo [ j ] . Clsid ;
free ( pImageCodecInfo ) ;
return j ; // Success
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
}
free ( pImageCodecInfo ) ;
return - 1 ; // Failure
}
//----------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
// ConvertToUnicode
//
//----------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
void
ConvertToUnicode (
PCHAR aString ,
PWCHAR wString ,
DWORD wStringLength
2025-01-16 20:52:24 +00:00
)
{
size_t len ;
2025-11-10 11:57:13 -08:00
len = MultiByteToWideChar ( CP_ACP , 0 , aString , static_cast < int > ( strlen ( aString ) ) ,
2025-01-16 20:52:24 +00:00
wString , wStringLength ) ;
wString [ len ] = 0 ;
}
//----------------------------------------------------------------------------
//
// LoadImageFile
//
// Use gdi+ to load an image.
//
//----------------------------------------------------------------------------
HBITMAP LoadImageFile ( PTCHAR Filename )
{
HBITMAP hBmp ;
Gdiplus : : Bitmap * bitmap ;
bitmap = Gdiplus : : Bitmap : : FromFile ( Filename ) ;
if ( bitmap - > GetHBITMAP ( NULL , & hBmp ) ) {
return NULL ;
}
delete bitmap ;
return hBmp ;
}
//----------------------------------------------------------------------------
//
// SavePng
//
// Use gdi+ to save a PNG.
//
//----------------------------------------------------------------------------
2026-02-03 13:05:31 -08:00
DWORD SavePng ( LPCTSTR Filename , HBITMAP hBitmap )
2025-01-16 20:52:24 +00:00
{
Gdiplus : : Bitmap bitmap ( hBitmap , NULL ) ;
CLSID pngClsid ;
GetEncoderClsid ( L " image/png " , & pngClsid ) ;
if ( bitmap . Save ( Filename , & pngClsid , NULL ) ) {
return GetLastError ( ) ;
}
return ERROR_SUCCESS ;
}
2026-03-26 13:21:43 +01:00
//----------------------------------------------------------------------------
//
// OcrFromHBITMAP
//
// Perform OCR on an HBITMAP and return the recognized text.
// Uses WIC to convert HBITMAP to IWICBitmap, then ISoftwareBitmapNativeFactory
// for zero-copy conversion to SoftwareBitmap, then Windows.Media.Ocr.
//
//----------------------------------------------------------------------------
std : : wstring OcrFromHBITMAP ( HBITMAP hBitmap )
{
try
{
// The main thread is STA (CoInitialize), but RecognizeAsync().get()
// asserts if called on an STA thread. Run the entire OCR pipeline
// on a background MTA thread.
auto future = std : : async ( std : : launch : : async , [ hBitmap ] ( ) - > std : : wstring
{
// Initialize COM as MTA on this worker thread
CoInitializeEx ( nullptr , COINIT_MULTITHREADED ) ;
auto comCleanup = wil : : scope_exit ( [ ] { CoUninitialize ( ) ; } ) ;
// Create WIC factory
wil : : com_ptr < IWICImagingFactory > wicFactory ;
THROW_IF_FAILED ( CoCreateInstance (
CLSID_WICImagingFactory , nullptr , CLSCTX_INPROC_SERVER ,
IID_PPV_ARGS ( wicFactory . put ( ) ) ) ) ;
// Create IWICBitmap from HBITMAP
wil : : com_ptr < IWICBitmap > wicBitmap ;
THROW_IF_FAILED ( wicFactory - > CreateBitmapFromHBITMAP (
hBitmap , nullptr , WICBitmapIgnoreAlpha , wicBitmap . put ( ) ) ) ;
// Convert to SoftwareBitmap via ISoftwareBitmapNativeFactory (zero-copy)
auto softwareBitmap = winrt : : capture < winrt : : SoftwareBitmap > (
winrt : : create_instance < ISoftwareBitmapNativeFactory > ( CLSID_SoftwareBitmapNativeFactory ) ,
& ISoftwareBitmapNativeFactory : : CreateFromWICBitmap ,
wicBitmap . get ( ) ,
false ) ;
// OcrEngine requires Gray8 or Bgra8 pixel format
if ( softwareBitmap . BitmapPixelFormat ( ) ! = winrt : : BitmapPixelFormat : : Bgra8 | |
softwareBitmap . BitmapAlphaMode ( ) ! = winrt : : BitmapAlphaMode : : Premultiplied )
{
softwareBitmap = winrt : : SoftwareBitmap : : Convert (
softwareBitmap ,
winrt : : BitmapPixelFormat : : Bgra8 ,
winrt : : BitmapAlphaMode : : Premultiplied ) ;
}
// Scale down if image exceeds OCR engine's max dimension (2600px)
const uint32_t maxDim = winrt : : OcrEngine : : MaxImageDimension ( ) ;
if ( softwareBitmap . PixelWidth ( ) > static_cast < int32_t > ( maxDim ) | |
softwareBitmap . PixelHeight ( ) > static_cast < int32_t > ( maxDim ) )
{
// Calculate scale factor to fit within max dimensions
float scale = min (
static_cast < float > ( maxDim ) / softwareBitmap . PixelWidth ( ) ,
static_cast < float > ( maxDim ) / softwareBitmap . PixelHeight ( ) ) ;
int32_t newWidth = static_cast < int32_t > ( softwareBitmap . PixelWidth ( ) * scale ) ;
int32_t newHeight = static_cast < int32_t > ( softwareBitmap . PixelHeight ( ) * scale ) ;
// Use WIC scaler for high-quality downscale
wil : : com_ptr < IWICBitmapScaler > scaler ;
THROW_IF_FAILED ( wicFactory - > CreateBitmapScaler ( scaler . put ( ) ) ) ;
THROW_IF_FAILED ( scaler - > Initialize (
wicBitmap . get ( ) , newWidth , newHeight , WICBitmapInterpolationModeHighQualityCubic ) ) ;
// Create new WICBitmap from scaled result
wil : : com_ptr < IWICBitmap > scaledBitmap ;
THROW_IF_FAILED ( wicFactory - > CreateBitmapFromSource (
scaler . get ( ) , WICBitmapCacheOnDemand , scaledBitmap . put ( ) ) ) ;
softwareBitmap = winrt : : capture < winrt : : SoftwareBitmap > (
winrt : : create_instance < ISoftwareBitmapNativeFactory > ( CLSID_SoftwareBitmapNativeFactory ) ,
& ISoftwareBitmapNativeFactory : : CreateFromWICBitmap ,
scaledBitmap . get ( ) ,
false ) ;
if ( softwareBitmap . BitmapPixelFormat ( ) ! = winrt : : BitmapPixelFormat : : Bgra8 | |
softwareBitmap . BitmapAlphaMode ( ) ! = winrt : : BitmapAlphaMode : : Premultiplied )
{
softwareBitmap = winrt : : SoftwareBitmap : : Convert (
softwareBitmap ,
winrt : : BitmapPixelFormat : : Bgra8 ,
winrt : : BitmapAlphaMode : : Premultiplied ) ;
}
}
// Create OCR engine from user profile languages
winrt : : OcrEngine engine = winrt : : OcrEngine : : TryCreateFromUserProfileLanguages ( ) ;
if ( ! engine )
{
return std : : wstring { } ;
}
// Run OCR — safe to call .get() on an MTA thread
winrt : : OcrResult result = engine . RecognizeAsync ( softwareBitmap ) . get ( ) ;
return std : : wstring ( result . Text ( ) ) ;
} ) ;
return future . get ( ) ;
}
catch ( . . . )
{
OutputDebug ( L " OcrFromHBITMAP failed \n " ) ;
return { } ;
}
}
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
//
// EnableDisableTrayIcon
//
//----------------------------------------------------------------------------
void EnableDisableTrayIcon ( HWND hWnd , BOOLEAN Enable )
{
NOTIFYICONDATA tNotifyIconData ;
memset ( & tNotifyIconData , 0 , sizeof ( tNotifyIconData ) ) ;
2025-11-10 11:57:13 -08:00
tNotifyIconData . cbSize = sizeof ( NOTIFYICONDATA ) ;
tNotifyIconData . hWnd = hWnd ;
tNotifyIconData . uID = 1 ;
tNotifyIconData . uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP ;
tNotifyIconData . uCallbackMessage = WM_USER_TRAY_ACTIVATE ;
tNotifyIconData . hIcon = LoadIcon ( g_hInstance , L " APPICON " ) ;
2025-01-16 20:52:24 +00:00
lstrcpyn ( tNotifyIconData . szTip , APPNAME , sizeof ( APPNAME ) ) ;
2025-11-10 11:57:13 -08:00
Shell_NotifyIcon ( Enable ? NIM_ADD : NIM_DELETE , & tNotifyIconData ) ;
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// EnableDisableOpacity
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
void EnableDisableOpacity ( HWND hWnd , BOOLEAN Enable )
2025-01-16 20:52:24 +00:00
{
DWORD exStyle ;
if ( pSetLayeredWindowAttributes & & g_BreakOpacity ! = 100 ) {
if ( Enable ) {
exStyle = GetWindowLong ( hWnd , GWL_EXSTYLE ) ;
SetWindowLong ( hWnd , GWL_EXSTYLE , ( exStyle | WS_EX_LAYERED ) ) ;
pSetLayeredWindowAttributes ( hWnd , 0 , static_cast < BYTE > ( ( 255 * g_BreakOpacity ) / 100 ) , LWA_ALPHA ) ;
RedrawWindow ( hWnd , 0 , 0 , RDW_ERASE | RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN ) ;
} else {
exStyle = GetWindowLong ( hWnd , GWL_EXSTYLE ) ;
SetWindowLong ( hWnd , GWL_EXSTYLE , ( exStyle & ~ WS_EX_LAYERED ) ) ;
}
}
}
//----------------------------------------------------------------------------
//
// EnableDisableScreenSaver
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
void EnableDisableScreenSaver ( BOOLEAN Enable )
2025-01-16 20:52:24 +00:00
{
2025-11-10 11:57:13 -08:00
SystemParametersInfo ( SPI_SETSCREENSAVEACTIVE , Enable , 0 , 0 ) ;
SystemParametersInfo ( SPI_SETPOWEROFFACTIVE , Enable , 0 , 0 ) ;
SystemParametersInfo ( SPI_SETLOWPOWERACTIVE , Enable , 0 , 0 ) ;
2025-01-16 20:52:24 +00:00
}
2026-03-26 13:21:43 +01:00
//----------------------------------------------------------------------------
//
// SaveBitmapToFile
//
// Save an HBITMAP to a BMP file using GDI+.
//
//----------------------------------------------------------------------------
static BOOLEAN SaveBitmapToFile ( HBITMAP hBitmap , PTCHAR filePath )
{
Gdiplus : : Bitmap bitmap ( hBitmap , NULL ) ;
if ( bitmap . GetLastStatus ( ) ! = Gdiplus : : Ok )
return FALSE ;
// Find the BMP encoder CLSID.
UINT num = 0 , size = 0 ;
Gdiplus : : GetImageEncodersSize ( & num , & size ) ;
if ( size = = 0 ) return FALSE ;
Gdiplus : : ImageCodecInfo * pInfo = static_cast < Gdiplus : : ImageCodecInfo * > ( malloc ( size ) ) ;
if ( ! pInfo ) return FALSE ;
Gdiplus : : GetImageEncoders ( num , size , pInfo ) ;
CLSID clsid = { } ;
BOOLEAN found = FALSE ;
for ( UINT i = 0 ; i < num ; i + + )
{
if ( wcscmp ( pInfo [ i ] . MimeType , L " image/bmp " ) = = 0 )
{
clsid = pInfo [ i ] . Clsid ;
found = TRUE ;
break ;
}
}
free ( pInfo ) ;
if ( ! found ) return FALSE ;
return bitmap . Save ( filePath , & clsid , NULL ) = = Gdiplus : : Ok ;
}
//----------------------------------------------------------------------------
//
// ExtractResourceToFile
//
// Extracts a BINRES resource to a file on disk.
//
//----------------------------------------------------------------------------
static BOOLEAN ExtractResourceToFile ( LPCTSTR resName , LPCTSTR resType , LPCTSTR destPath )
{
HRSRC hRes = FindResource ( NULL , resName , resType ) ;
if ( ! hRes ) return FALSE ;
HGLOBAL hData = LoadResource ( NULL , hRes ) ;
if ( ! hData ) return FALSE ;
LPVOID pData = LockResource ( hData ) ;
DWORD size = SizeofResource ( NULL , hRes ) ;
if ( ! pData | | size = = 0 ) return FALSE ;
HANDLE hFile = CreateFile ( destPath , GENERIC_WRITE , 0 , NULL ,
CREATE_ALWAYS , FILE_ATTRIBUTE_NORMAL , NULL ) ;
if ( hFile = = INVALID_HANDLE_VALUE ) return FALSE ;
DWORD written ;
BOOL ok = WriteFile ( hFile , pData , size , & written , NULL ) ;
CloseHandle ( hFile ) ;
return ok & & written = = size ;
}
//----------------------------------------------------------------------------
//
// Screensaver settings save / restore for crash recovery.
//
// Before swapping in our .scr, save the user's current screensaver
// settings. On startup and WM_ENDSESSION, check for orphaned settings
// and restore them.
//
//----------------------------------------------------------------------------
# define SCREENSAVER_BACKUP_KEY L"Software\\Sysinternals\\ZoomIt\\SavedScreenSaver"
struct ScreenSaverSnapshot
{
TCHAR scrPath [ MAX_PATH ] = { } ;
TCHAR screenSaveActive [ 8 ] = { } ;
TCHAR secure [ 8 ] = { } ;
TCHAR timeoutText [ 16 ] = { } ;
DWORD timeoutSeconds = 0 ;
BOOLEAN hasScrPath = FALSE ;
BOOLEAN hasScreenSaveActive = FALSE ;
BOOLEAN hasSecure = FALSE ;
BOOLEAN hasTimeoutText = FALSE ;
BOOLEAN screenSaveActiveRuntime = TRUE ;
BOOLEAN valid = FALSE ;
} ;
// Per-run snapshot captured immediately before we swap in ZoomIt's .scr.
// This avoids relying only on registry backup timing for unlock restore.
static ScreenSaverSnapshot g_PreBreakScreenSaverSnapshot ;
static BOOLEAN CaptureCurrentScreenSaverSettings ( ScreenSaverSnapshot * snapshot )
{
if ( snapshot = = nullptr )
return FALSE ;
ScreenSaverSnapshot localSnapshot ;
DWORD cbPath = sizeof ( localSnapshot . scrPath ) ;
localSnapshot . hasScrPath =
( RegGetValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " , L " SCRNSAVE.EXE " ,
RRF_RT_REG_SZ , NULL , localSnapshot . scrPath , & cbPath ) = = ERROR_SUCCESS ) ;
DWORD cbScreenSaveActive = sizeof ( localSnapshot . screenSaveActive ) ;
localSnapshot . hasScreenSaveActive =
( RegGetValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " , L " ScreenSaveActive " ,
RRF_RT_REG_SZ , NULL , localSnapshot . screenSaveActive , & cbScreenSaveActive ) = = ERROR_SUCCESS ) ;
DWORD cbSecure = sizeof ( localSnapshot . secure ) ;
localSnapshot . hasSecure =
( RegGetValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " , L " ScreenSaverIsSecure " ,
RRF_RT_REG_SZ , NULL , localSnapshot . secure , & cbSecure ) = = ERROR_SUCCESS ) ;
DWORD cbTimeoutText = sizeof ( localSnapshot . timeoutText ) ;
localSnapshot . hasTimeoutText =
( RegGetValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " , L " ScreenSaveTimeOut " ,
RRF_RT_REG_SZ , NULL , localSnapshot . timeoutText , & cbTimeoutText ) = = ERROR_SUCCESS ) ;
UINT timeout = 0 ;
SystemParametersInfo ( SPI_GETSCREENSAVETIMEOUT , 0 , & timeout , 0 ) ;
localSnapshot . timeoutSeconds = timeout ;
BOOL screenSaveActive = TRUE ;
SystemParametersInfo ( SPI_GETSCREENSAVEACTIVE , 0 , & screenSaveActive , 0 ) ;
localSnapshot . screenSaveActiveRuntime = screenSaveActive ? TRUE : FALSE ;
if ( localSnapshot . hasTimeoutText )
{
const int parsed = _tstoi ( localSnapshot . timeoutText ) ;
if ( parsed > 0 )
{
localSnapshot . timeoutSeconds = static_cast < DWORD > ( parsed ) ;
}
}
localSnapshot . valid = TRUE ;
* snapshot = localSnapshot ;
return TRUE ;
}
static void ApplyScreenSaverSnapshot ( const ScreenSaverSnapshot * snapshot )
{
if ( snapshot = = nullptr | | ! snapshot - > valid )
return ;
if ( snapshot - > hasScrPath )
{
RegSetKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " SCRNSAVE.EXE " , REG_SZ , snapshot - > scrPath ,
static_cast < DWORD > ( ( _tcslen ( snapshot - > scrPath ) + 1 ) * sizeof ( TCHAR ) ) ) ;
}
else
{
RegDeleteKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " , L " SCRNSAVE.EXE " ) ;
}
if ( snapshot - > hasScreenSaveActive )
{
RegSetKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " ScreenSaveActive " , REG_SZ , snapshot - > screenSaveActive ,
static_cast < DWORD > ( ( _tcslen ( snapshot - > screenSaveActive ) + 1 ) * sizeof ( TCHAR ) ) ) ;
}
else
{
RegDeleteKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " , L " ScreenSaveActive " ) ;
}
SystemParametersInfo ( SPI_SETSCREENSAVEACTIVE , snapshot - > screenSaveActiveRuntime ? TRUE : FALSE , 0 , SPIF_SENDCHANGE ) ;
if ( snapshot - > hasSecure )
{
RegSetKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " ScreenSaverIsSecure " , REG_SZ , snapshot - > secure ,
static_cast < DWORD > ( ( _tcslen ( snapshot - > secure ) + 1 ) * sizeof ( TCHAR ) ) ) ;
}
else
{
RegDeleteKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " , L " ScreenSaverIsSecure " ) ;
}
if ( snapshot - > hasTimeoutText )
{
RegSetKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " ScreenSaveTimeOut " , REG_SZ , snapshot - > timeoutText ,
static_cast < DWORD > ( ( _tcslen ( snapshot - > timeoutText ) + 1 ) * sizeof ( TCHAR ) ) ) ;
}
else
{
RegDeleteKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " , L " ScreenSaveTimeOut " ) ;
}
SystemParametersInfo ( SPI_SETSCREENSAVETIMEOUT , snapshot - > timeoutSeconds , 0 , SPIF_SENDCHANGE ) ;
}
static void SaveScreenSaverSettings ( void )
{
CaptureCurrentScreenSaverSettings ( & g_PreBreakScreenSaverSnapshot ) ;
HKEY hKey ;
if ( RegCreateKeyEx ( HKEY_CURRENT_USER , SCREENSAVER_BACKUP_KEY , 0 , NULL ,
0 , KEY_SET_VALUE , NULL , & hKey , NULL ) = = ERROR_SUCCESS )
{
const ScreenSaverSnapshot & snap = g_PreBreakScreenSaverSnapshot ;
DWORD has = snap . hasScrPath ;
RegSetValueEx ( hKey , L " HasSCRNSAVE.EXE " , 0 , REG_DWORD ,
reinterpret_cast < const BYTE * > ( & has ) , sizeof ( DWORD ) ) ;
RegSetValueEx ( hKey , L " SCRNSAVE.EXE " , 0 , REG_SZ ,
reinterpret_cast < const BYTE * > ( snap . scrPath ) ,
static_cast < DWORD > ( ( _tcslen ( snap . scrPath ) + 1 ) * sizeof ( TCHAR ) ) ) ;
has = snap . hasScreenSaveActive ;
RegSetValueEx ( hKey , L " HasScreenSaveActive " , 0 , REG_DWORD ,
reinterpret_cast < const BYTE * > ( & has ) , sizeof ( DWORD ) ) ;
RegSetValueEx ( hKey , L " ScreenSaveActive " , 0 , REG_SZ ,
reinterpret_cast < const BYTE * > ( snap . screenSaveActive ) ,
static_cast < DWORD > ( ( _tcslen ( snap . screenSaveActive ) + 1 ) * sizeof ( TCHAR ) ) ) ;
has = snap . hasSecure ;
RegSetValueEx ( hKey , L " HasScreenSaverIsSecure " , 0 , REG_DWORD ,
reinterpret_cast < const BYTE * > ( & has ) , sizeof ( DWORD ) ) ;
RegSetValueEx ( hKey , L " ScreenSaverIsSecure " , 0 , REG_SZ ,
reinterpret_cast < const BYTE * > ( snap . secure ) ,
static_cast < DWORD > ( ( _tcslen ( snap . secure ) + 1 ) * sizeof ( TCHAR ) ) ) ;
has = snap . hasTimeoutText ;
RegSetValueEx ( hKey , L " HasScreenSaveTimeOut " , 0 , REG_DWORD ,
reinterpret_cast < const BYTE * > ( & has ) , sizeof ( DWORD ) ) ;
RegSetValueEx ( hKey , L " ScreenSaveTimeOutText " , 0 , REG_SZ ,
reinterpret_cast < const BYTE * > ( snap . timeoutText ) ,
static_cast < DWORD > ( ( _tcslen ( snap . timeoutText ) + 1 ) * sizeof ( TCHAR ) ) ) ;
RegSetValueEx ( hKey , L " ScreenSaveTimeOut " , 0 , REG_DWORD ,
reinterpret_cast < const BYTE * > ( & snap . timeoutSeconds ) , sizeof ( DWORD ) ) ;
DWORD runtimeActive = snap . screenSaveActiveRuntime ;
RegSetValueEx ( hKey , L " ScreenSaveActiveRuntime " , 0 , REG_DWORD ,
reinterpret_cast < const BYTE * > ( & runtimeActive ) , sizeof ( DWORD ) ) ;
RegCloseKey ( hKey ) ;
}
}
static void RestoreScreenSaverSettings ( void )
{
bool restored = false ;
// Preferred restore path: exact values captured in-memory before this run
// applied ZoomIt's screensaver settings.
if ( g_PreBreakScreenSaverSnapshot . valid )
{
ApplyScreenSaverSnapshot ( & g_PreBreakScreenSaverSnapshot ) ;
g_PreBreakScreenSaverSnapshot . valid = FALSE ;
restored = true ;
RegDeleteKey ( HKEY_CURRENT_USER , SCREENSAVER_BACKUP_KEY ) ;
}
else
{
// Crash-recovery fallback path.
HKEY hKey ;
if ( RegOpenKeyEx ( HKEY_CURRENT_USER , SCREENSAVER_BACKUP_KEY , 0 ,
KEY_QUERY_VALUE , & hKey ) = = ERROR_SUCCESS )
{
ScreenSaverSnapshot backupSnapshot ;
DWORD has = 1 ;
DWORD cbHas = sizeof ( has ) ;
if ( RegQueryValueEx ( hKey , L " HasSCRNSAVE.EXE " , NULL , NULL ,
reinterpret_cast < BYTE * > ( & has ) , & cbHas ) = = ERROR_SUCCESS )
{
backupSnapshot . hasScrPath = has ? TRUE : FALSE ;
}
else
{
backupSnapshot . hasScrPath = TRUE ;
}
DWORD cbPath = sizeof ( backupSnapshot . scrPath ) ;
if ( RegQueryValueEx ( hKey , L " SCRNSAVE.EXE " , NULL , NULL ,
reinterpret_cast < BYTE * > ( backupSnapshot . scrPath ) , & cbPath ) ! = ERROR_SUCCESS )
{
backupSnapshot . scrPath [ 0 ] = 0 ;
}
has = 1 ;
cbHas = sizeof ( has ) ;
if ( RegQueryValueEx ( hKey , L " HasScreenSaveActive " , NULL , NULL ,
reinterpret_cast < BYTE * > ( & has ) , & cbHas ) = = ERROR_SUCCESS )
{
backupSnapshot . hasScreenSaveActive = has ? TRUE : FALSE ;
}
else
{
backupSnapshot . hasScreenSaveActive = TRUE ;
}
DWORD cbScreenSaveActive = sizeof ( backupSnapshot . screenSaveActive ) ;
if ( RegQueryValueEx ( hKey , L " ScreenSaveActive " , NULL , NULL ,
reinterpret_cast < BYTE * > ( backupSnapshot . screenSaveActive ) , & cbScreenSaveActive ) ! = ERROR_SUCCESS )
{
backupSnapshot . screenSaveActive [ 0 ] = 0 ;
}
has = 1 ;
cbHas = sizeof ( has ) ;
if ( RegQueryValueEx ( hKey , L " HasScreenSaverIsSecure " , NULL , NULL ,
reinterpret_cast < BYTE * > ( & has ) , & cbHas ) = = ERROR_SUCCESS )
{
backupSnapshot . hasSecure = has ? TRUE : FALSE ;
}
else
{
backupSnapshot . hasSecure = TRUE ;
}
DWORD cbSecure = sizeof ( backupSnapshot . secure ) ;
if ( RegQueryValueEx ( hKey , L " ScreenSaverIsSecure " , NULL , NULL ,
reinterpret_cast < BYTE * > ( backupSnapshot . secure ) , & cbSecure ) ! = ERROR_SUCCESS )
{
backupSnapshot . secure [ 0 ] = 0 ;
}
has = 1 ;
cbHas = sizeof ( has ) ;
if ( RegQueryValueEx ( hKey , L " HasScreenSaveTimeOut " , NULL , NULL ,
reinterpret_cast < BYTE * > ( & has ) , & cbHas ) = = ERROR_SUCCESS )
{
backupSnapshot . hasTimeoutText = has ? TRUE : FALSE ;
}
else
{
backupSnapshot . hasTimeoutText = TRUE ;
}
DWORD cbTimeoutText = sizeof ( backupSnapshot . timeoutText ) ;
if ( RegQueryValueEx ( hKey , L " ScreenSaveTimeOutText " , NULL , NULL ,
reinterpret_cast < BYTE * > ( backupSnapshot . timeoutText ) , & cbTimeoutText ) ! = ERROR_SUCCESS )
{
backupSnapshot . timeoutText [ 0 ] = 0 ;
}
DWORD dwTimeout = 0 ;
DWORD cbTimeout = sizeof ( dwTimeout ) ;
if ( RegQueryValueEx ( hKey , L " ScreenSaveTimeOut " , NULL , NULL ,
reinterpret_cast < BYTE * > ( & dwTimeout ) , & cbTimeout ) = = ERROR_SUCCESS )
{
backupSnapshot . timeoutSeconds = dwTimeout ;
}
else
{
UINT timeout = 0 ;
SystemParametersInfo ( SPI_GETSCREENSAVETIMEOUT , 0 , & timeout , 0 ) ;
backupSnapshot . timeoutSeconds = timeout ;
}
DWORD runtimeActive = TRUE ;
DWORD cbRuntimeActive = sizeof ( runtimeActive ) ;
if ( RegQueryValueEx ( hKey , L " ScreenSaveActiveRuntime " , NULL , NULL ,
reinterpret_cast < BYTE * > ( & runtimeActive ) , & cbRuntimeActive ) = = ERROR_SUCCESS )
{
backupSnapshot . screenSaveActiveRuntime = runtimeActive ? TRUE : FALSE ;
}
else
{
BOOL active = TRUE ;
SystemParametersInfo ( SPI_GETSCREENSAVEACTIVE , 0 , & active , 0 ) ;
backupSnapshot . screenSaveActiveRuntime = active ? TRUE : FALSE ;
}
backupSnapshot . valid = TRUE ;
ApplyScreenSaverSnapshot ( & backupSnapshot ) ;
restored = true ;
RegCloseKey ( hKey ) ;
RegDeleteKey ( HKEY_CURRENT_USER , SCREENSAVER_BACKUP_KEY ) ;
}
}
if ( restored )
{
// Broadcast settings change so Windows picks up all registry-only
// values (SCRNSAVE.EXE, ScreenSaverIsSecure) and refreshed timeout.
SendNotifyMessage ( HWND_BROADCAST , WM_SETTINGCHANGE , 0 ,
reinterpret_cast < LPARAM > ( L " Control Panel \\ Desktop " ) ) ;
SendNotifyMessage ( HWND_BROADCAST , WM_SETTINGCHANGE , SPI_SETSCREENSAVEACTIVE , 0 ) ;
}
}
static BOOLEAN HasOrphanedScreenSaverSettings ( void )
{
HKEY hKey ;
if ( RegOpenKeyEx ( HKEY_CURRENT_USER , SCREENSAVER_BACKUP_KEY , 0 ,
KEY_QUERY_VALUE , & hKey ) = = ERROR_SUCCESS )
{
RegCloseKey ( hKey ) ;
return TRUE ;
}
return FALSE ;
}
static BOOLEAN IsBreakScreenSaverRunning ( void )
{
HANDLE hSnap = CreateToolhelp32Snapshot ( TH32CS_SNAPPROCESS , 0 ) ;
if ( hSnap = = INVALID_HANDLE_VALUE )
{
return FALSE ;
}
PROCESSENTRY32 pe = { sizeof ( pe ) } ;
BOOLEAN running = FALSE ;
if ( Process32First ( hSnap , & pe ) )
{
do
{
if ( _tcsicmp ( pe . szExeFile , L " ZoomItBreak.scr " ) = = 0 )
{
running = TRUE ;
break ;
}
}
while ( Process32Next ( hSnap , & pe ) ) ;
}
CloseHandle ( hSnap ) ;
return running ;
}
//----------------------------------------------------------------------------
//
// ActivateBreakScreenSaver
//
// Activates the break timer as a secure screensaver using the registry-swap
// approach:
// 1. Extracts the embedded .scr to %TEMP%
// 2. Writes config file with break timer settings
// 3. Saves user's current screensaver settings
// 4. Configures system to use our .scr with password protection
// 5. Triggers screensaver via SC_SCREENSAVE
//
// The screensaver runs on the ScreenSaver Desktop and displays the countdown
// timer. With ScreenSaverIsSecure=1, students must authenticate to dismiss it.
// The user's original settings are restored on session unlock (WTS_SESSION_UNLOCK)
// or on ZoomIt shutdown (crash recovery).
//
//----------------------------------------------------------------------------
static BOOLEAN ActivateBreakScreenSaver ( HWND hWnd , int breakTimeoutSeconds )
{
TCHAR tempDir [ MAX_PATH ] ;
GetTempPath ( MAX_PATH , tempDir ) ;
{
TCHAR dbg [ 256 ] ;
_stprintf ( dbg , L " [ZoomIt] ActivateBreakScreenSaver: timeout=%d \n " , breakTimeoutSeconds ) ;
OutputDebugString ( dbg ) ;
}
//
// 1. Extract the embedded .scr to %TEMP%.
// Kill any previous instance first — the file may be locked by a
// still-running screensaver process (ERROR_SHARING_VIOLATION).
//
TCHAR scrPath [ MAX_PATH ] ;
_stprintf ( scrPath , L " %sZoomItBreak.scr " , tempDir ) ;
// Try extracting; if the file is locked, find and kill the old process.
if ( ! ExtractResourceToFile ( L " RCZOOMITSCR " , L " BINRES " , scrPath ) )
{
DWORD err = GetLastError ( ) ;
if ( err = = ERROR_SHARING_VIOLATION )
{
OutputDebugString ( L " [ZoomIt] .scr file locked, killing previous instance \n " ) ;
// Find and terminate any running ZoomItBreak.scr process.
HANDLE hSnap = CreateToolhelp32Snapshot ( TH32CS_SNAPPROCESS , 0 ) ;
if ( hSnap ! = INVALID_HANDLE_VALUE )
{
PROCESSENTRY32 pe = { sizeof ( pe ) } ;
if ( Process32First ( hSnap , & pe ) )
{
do
{
if ( _tcsicmp ( pe . szExeFile , L " ZoomItBreak.scr " ) = = 0 )
{
HANDLE hProc = OpenProcess ( PROCESS_TERMINATE , FALSE , pe . th32ProcessID ) ;
if ( hProc )
{
TerminateProcess ( hProc , 0 ) ;
WaitForSingleObject ( hProc , 2000 ) ;
CloseHandle ( hProc ) ;
}
}
}
while ( Process32Next ( hSnap , & pe ) ) ;
}
CloseHandle ( hSnap ) ;
}
Sleep ( 200 ) ; // Brief wait for file handle release.
// Retry extraction.
if ( ! ExtractResourceToFile ( L " RCZOOMITSCR " , L " BINRES " , scrPath ) )
{
TCHAR dbg [ 256 ] ;
_stprintf ( dbg , L " [ZoomIt] ExtractResourceToFile FAILED (retry), err=%lu \n " , GetLastError ( ) ) ;
OutputDebugString ( dbg ) ;
return FALSE ;
}
}
else
{
TCHAR dbg [ 256 ] ;
_stprintf ( dbg , L " [ZoomIt] ExtractResourceToFile FAILED, err=%lu \n " , err ) ;
OutputDebugString ( dbg ) ;
return FALSE ;
}
}
//
// 2. Capture desktop screenshot if the user wants "show desktop" background.
//
TCHAR screenshotPath [ MAX_PATH ] = { } ;
if ( g_BreakShowBackgroundFile & & g_BreakShowDesktop )
{
_stprintf ( screenshotPath , L " %sZoomItBreakBg.bmp " , tempDir ) ;
HDC hdcDesktop = GetDC ( NULL ) ;
MONITORINFO monInfo = { sizeof ( monInfo ) } ;
POINT pt ;
GetCursorPos ( & pt ) ;
HMONITOR hMon = MonitorFromPoint ( pt , MONITOR_DEFAULTTONEAREST ) ;
GetMonitorInfo ( hMon , & monInfo ) ;
HBITMAP hBmp = CreateFadedDesktopBackground ( hdcDesktop , & monInfo . rcMonitor , NULL ) ;
ReleaseDC ( NULL , hdcDesktop ) ;
if ( hBmp )
{
SaveBitmapToFile ( hBmp , screenshotPath ) ;
DeleteObject ( hBmp ) ;
}
}
//
// 3. Write the config file for the screensaver to read.
//
BreakScrConfig config = { } ;
config . magic = BREAKSCR_CONFIG_MAGIC ;
config . timeoutSeconds = breakTimeoutSeconds ;
config . settings . penColor = g_BreakPenColor ;
config . settings . backgroundColor = g_BreakBackgroundColor ;
config . settings . timerPosition = g_BreakTimerPosition ;
config . settings . opacity = g_BreakOpacity ;
config . settings . showExpiredTime = g_ShowExpiredTime ;
config . settings . smoothImage = g_SmoothImage ;
config . settings . backgroundStretch = g_BreakBackgroundStretch ;
config . settings . playSound = g_BreakPlaySoundFile ;
_tcscpy ( config . settings . soundFile , g_BreakSoundFile ) ;
config . settings . showDesktop = g_BreakShowDesktop ;
config . settings . showBackgroundFile = g_BreakShowBackgroundFile ;
_tcscpy ( config . settings . backgroundFile , g_BreakBackgroundFile ) ;
config . settings . logFont = g_LogFont ;
_tcscpy ( config . screenshotPath , screenshotPath ) ;
// If a desktop screenshot was captured, tell the screensaver to load it
// as its background file.
if ( screenshotPath [ 0 ] )
{
config . settings . showBackgroundFile = TRUE ;
config . settings . showDesktop = FALSE ;
_tcscpy ( config . settings . backgroundFile , screenshotPath ) ;
}
if ( ! BreakScrConfig_Write ( & config ) )
{
OutputDebugString ( L " [ZoomIt] BreakScrConfig_Write FAILED \n " ) ;
return FALSE ;
}
//
// 4. Save current screensaver settings for later restoration.
// Always take a fresh snapshot immediately before swapping in
// ZoomIt's .scr so unlock cleanup can reliably restore the user's
// pre-break values (path, active flag, secure flag, timeout).
//
// If a stale backup exists from an interrupted prior run, restore it
// first so we don't snapshot already-overridden ZoomIt values.
if ( HasOrphanedScreenSaverSettings ( ) )
{
RestoreScreenSaverSettings ( ) ;
}
SaveScreenSaverSettings ( ) ;
//
// 4a. Ensure screensaver is fully reset before reconfiguring.
// Disable screensaver, wait briefly, then reconfigure. This ensures
// Windows processes the state change and allows subsequent activations.
//
SystemParametersInfo ( SPI_SETSCREENSAVEACTIVE , FALSE , 0 , SPIF_SENDCHANGE ) ;
Sleep ( 200 ) ; // Allow Windows to process the disable
//
// 5. Configure system to use our screensaver.
//
// Set our .scr as the system screensaver
if ( RegSetKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " SCRNSAVE.EXE " , REG_SZ , scrPath ,
static_cast < DWORD > ( ( _tcslen ( scrPath ) + 1 ) * sizeof ( TCHAR ) ) ) ! = ERROR_SUCCESS )
{
OutputDebugString ( L " [ZoomIt] Failed to set SCRNSAVE.EXE \n " ) ;
RestoreScreenSaverSettings ( ) ;
return FALSE ;
}
// Set screensaver timeout to 15 seconds.
// The initial activation uses SC_SCREENSAVE for immediate launch.
// This timeout controls how quickly the screensaver REACTIVATES on the lock
// screen after a student triggers the credential provider but doesn't
// authenticate and walks away. 1 second may be below Windows' enforced
// minimum, so we use 15 seconds as a practical value.
// ScreenSaveTimeOut is stored as REG_SZ in Control Panel\Desktop.
RegSetKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " ScreenSaveTimeOut " , REG_SZ , L " 15 " ,
static_cast < DWORD > ( 3 * sizeof ( TCHAR ) ) ) ;
SystemParametersInfo ( SPI_SETSCREENSAVETIMEOUT , 15 , 0 , SPIF_SENDCHANGE ) ;
// Force password protection for classroom security
TCHAR secure [ ] = L " 1 " ;
if ( RegSetKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " ScreenSaverIsSecure " , REG_SZ , secure ,
static_cast < DWORD > ( ( _tcslen ( secure ) + 1 ) * sizeof ( TCHAR ) ) ) ! = ERROR_SUCCESS )
{
OutputDebugString ( L " [ZoomIt] Failed to set ScreenSaverIsSecure \n " ) ;
RestoreScreenSaverSettings ( ) ;
return FALSE ;
}
// Activate screensaver (must be enabled for SC_SCREENSAVE to work).
// Set both the registry value and runtime state — Winlogon reads the
// registry directly and ignores the SPI runtime flag.
RegSetKeyValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " ScreenSaveActive " , REG_SZ , L " 1 " ,
static_cast < DWORD > ( 2 * sizeof ( TCHAR ) ) ) ;
SystemParametersInfo ( SPI_SETSCREENSAVEACTIVE , TRUE , 0 , SPIF_SENDCHANGE ) ;
// Broadcast and give Windows time to process all registry changes before
// triggering the screensaver. If we trigger too quickly, Windows may not
// see ScreenSaverIsSecure=1 and the screensaver won't require authentication.
SendNotifyMessage ( HWND_BROADCAST , WM_SETTINGCHANGE , SPI_SETSCREENSAVEACTIVE ,
reinterpret_cast < LPARAM > ( L " Policy " ) ) ;
Sleep ( 500 ) ;
//
// 6. Trigger the screensaver via system mechanism.
// This causes Windows/Winlogon to launch our .scr on the ScreenSaver Desktop.
//
// IMPORTANT: Use DefWindowProc on our own window rather than
// SendMessage(HWND_BROADCAST). Broadcasting WM_SYSCOMMAND is synchronous
// and blocks our message loop for the entire screensaver session, making
// the desktop unresponsive. It also delivers SC_SCREENSAVE to ZoomIt's
// own window, triggering unwanted LiveZoom activation.
//
DefWindowProc ( hWnd , WM_SYSCOMMAND , SC_SCREENSAVE , 0 ) ;
// Defensive cleanup path: if session-change notifications are delayed or
// missed, poll until the break .scr exits and then restore original values.
// This ensures ScreenSaveTimeOut/SCRNSAVE.EXE don't remain overridden.
SetTimer ( hWnd , 4 , 2000 , NULL ) ;
return TRUE ;
}
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
//
// EnableDisableStickyKeys
//
//----------------------------------------------------------------------------
void EnableDisableStickyKeys ( BOOLEAN Enable )
{
static STICKYKEYS prevStickyKeyValue = { 0 } ;
STICKYKEYS newStickyKeyValue = { 0 } ;
2025-11-10 11:57:13 -08:00
// Need to do this on Vista tablet to stop sticky key popup when you
2025-01-16 20:52:24 +00:00
// hold down the shift key and draw with the pen.
if ( Enable ) {
if ( prevStickyKeyValue . cbSize = = sizeof ( STICKYKEYS ) ) {
2025-11-10 11:57:13 -08:00
SystemParametersInfo ( SPI_SETSTICKYKEYS ,
2025-01-16 20:52:24 +00:00
sizeof ( STICKYKEYS ) , & prevStickyKeyValue , SPIF_SENDCHANGE ) ;
}
} else {
prevStickyKeyValue . cbSize = sizeof ( STICKYKEYS ) ;
2025-11-10 11:57:13 -08:00
if ( SystemParametersInfo ( SPI_GETSTICKYKEYS , sizeof ( STICKYKEYS ) ,
2025-01-16 20:52:24 +00:00
& prevStickyKeyValue , 0 ) ) {
newStickyKeyValue . cbSize = sizeof ( STICKYKEYS ) ;
newStickyKeyValue . dwFlags = 0 ;
2025-11-10 11:57:13 -08:00
if ( ! SystemParametersInfo ( SPI_SETSTICKYKEYS ,
2025-01-16 20:52:24 +00:00
sizeof ( STICKYKEYS ) , & newStickyKeyValue , SPIF_SENDCHANGE ) ) {
// DWORD error = GetLastError();
}
}
}
}
//----------------------------------------------------------------------------
//
// GetKeyMod
//
//----------------------------------------------------------------------------
constexpr DWORD GetKeyMod ( DWORD Key )
{
DWORD keyMod = 0 ;
if ( ( Key > > 8 ) & HOTKEYF_ALT ) keyMod | = MOD_ALT ;
if ( ( Key > > 8 ) & HOTKEYF_CONTROL ) keyMod | = MOD_CONTROL ;
if ( ( Key > > 8 ) & HOTKEYF_SHIFT ) keyMod | = MOD_SHIFT ;
if ( ( Key > > 8 ) & HOTKEYF_EXT ) keyMod | = MOD_WIN ;
return keyMod ;
}
//----------------------------------------------------------------------------
//
// AdvancedBreakProc
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
INT_PTR CALLBACK AdvancedBreakProc ( HWND hDlg , UINT message , WPARAM wParam , LPARAM lParam )
2025-01-16 20:52:24 +00:00
{
TCHAR opacity [ 10 ] ;
static TCHAR newSoundFile [ MAX_PATH ] ;
static TCHAR newBackgroundFile [ MAX_PATH ] ;
TCHAR filePath [ MAX_PATH ] , initDir [ MAX_PATH ] ;
DWORD i ;
2026-02-03 13:05:31 -08:00
static UINT currentDpi = DPI_BASELINE ;
2025-01-16 20:52:24 +00:00
switch ( message ) {
case WM_INITDIALOG :
2026-02-03 13:05:31 -08:00
// Set the dialog icon
{
HICON hIcon = LoadIcon ( g_hInstance , L " APPICON " ) ;
if ( hIcon )
{
SendMessage ( hDlg , WM_SETICON , ICON_BIG , reinterpret_cast < LPARAM > ( hIcon ) ) ;
SendMessage ( hDlg , WM_SETICON , ICON_SMALL , reinterpret_cast < LPARAM > ( hIcon ) ) ;
}
}
2025-01-16 20:52:24 +00:00
if ( pSHAutoComplete ) {
pSHAutoComplete ( GetDlgItem ( hDlg , IDC_SOUND_FILE ) , SHACF_FILESYSTEM ) ;
pSHAutoComplete ( GetDlgItem ( hDlg , IDC_BACKGROUND_FILE ) , SHACF_FILESYSTEM ) ;
}
2025-11-10 11:57:13 -08:00
CheckDlgButton ( hDlg , IDC_CHECK_BACKGROUND_FILE ,
2025-01-16 20:52:24 +00:00
g_BreakShowBackgroundFile ? BST_CHECKED : BST_UNCHECKED ) ;
2025-11-10 11:57:13 -08:00
CheckDlgButton ( hDlg , IDC_CHECK_SOUND_FILE ,
2025-01-16 20:52:24 +00:00
g_BreakPlaySoundFile ? BST_CHECKED : BST_UNCHECKED ) ;
CheckDlgButton ( hDlg , IDC_CHECK_SHOW_EXPIRED ,
g_ShowExpiredTime ? BST_CHECKED : BST_UNCHECKED ) ;
CheckDlgButton ( hDlg , IDC_CHECK_BACKGROUND_STRETCH ,
g_BreakBackgroundStretch ? BST_CHECKED : BST_UNCHECKED ) ;
#if 0
CheckDlgButton ( hDlg , IDC_CHECK_SECONDARYDISPLAY ,
g_BreakOnSecondary ? BST_CHECKED : BST_UNCHECKED ) ;
# endif
if ( pSetLayeredWindowAttributes = = NULL ) {
EnableWindow ( GetDlgItem ( hDlg , IDC_OPACITY ) , FALSE ) ;
}
// sound file
if ( ! g_BreakPlaySoundFile ) {
EnableWindow ( GetDlgItem ( hDlg , IDC_STATIC_SOUND_FILE ) , FALSE ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_SOUND_FILE ) , FALSE ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_SOUND_BROWSE ) , FALSE ) ;
}
_tcscpy ( newSoundFile , g_BreakSoundFile ) ;
_tcscpy ( filePath , g_BreakSoundFile ) ;
if ( _tcsrchr ( filePath , ' \\ ' ) ) _tcscpy ( filePath , _tcsrchr ( g_BreakSoundFile , ' \\ ' ) + 1 ) ;
if ( _tcsrchr ( filePath , ' . ' ) ) * _tcsrchr ( filePath , ' . ' ) = 0 ;
SetDlgItemText ( hDlg , IDC_SOUND_FILE , filePath ) ;
// background file
if ( ! g_BreakShowBackgroundFile ) {
EnableWindow ( GetDlgItem ( hDlg , IDC_STATIC_DESKTOP_BACKGROUND ) , FALSE ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_STATIC_DESKTOP_BACKGROUND ) , FALSE ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_STATIC_BACKGROUND_FILE ) , FALSE ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_BACKGROUND_FILE ) , FALSE ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_BACKGROUND_BROWSE ) , FALSE ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_CHECK_BACKGROUND_STRETCH ) , FALSE ) ;
}
2025-11-10 11:57:13 -08:00
CheckDlgButton ( hDlg ,
2025-01-16 20:52:24 +00:00
g_BreakShowDesktop ? IDC_STATIC_DESKTOP_BACKGROUND : IDC_STATIC_BACKGROUND_FILE , BST_CHECKED ) ;
_tcscpy ( newBackgroundFile , g_BreakBackgroundFile ) ;
SetDlgItemText ( hDlg , IDC_BACKGROUND_FILE , g_BreakBackgroundFile ) ;
CheckDlgButton ( hDlg , IDC_TIMER_POS1 + g_BreakTimerPosition , BST_CHECKED ) ;
for ( i = 10 ; i < = 100 ; i + = 10 ) {
_stprintf ( opacity , L " %d%% " , i ) ;
2025-11-10 11:57:13 -08:00
SendMessage ( GetDlgItem ( hDlg , IDC_OPACITY ) , CB_ADDSTRING , 0 ,
2025-01-16 20:52:24 +00:00
reinterpret_cast < LPARAM > ( opacity ) ) ;
}
2025-11-10 11:57:13 -08:00
SendMessage ( GetDlgItem ( hDlg , IDC_OPACITY ) , CB_SETCURSEL ,
2025-01-16 20:52:24 +00:00
g_BreakOpacity / 10 - 1 , 0 ) ;
2026-02-03 13:05:31 -08:00
// Apply DPI scaling to the dialog
currentDpi = GetDpiForWindowHelper ( hDlg ) ;
if ( currentDpi ! = DPI_BASELINE )
{
ScaleDialogForDpi ( hDlg , currentDpi , DPI_BASELINE ) ;
}
// Apply dark mode
ApplyDarkModeToDialog ( hDlg ) ;
2025-01-16 20:52:24 +00:00
return TRUE ;
2026-02-03 13:05:31 -08:00
case WM_DPICHANGED :
HandleDialogDpiChange ( hDlg , wParam , lParam , currentDpi ) ;
return TRUE ;
case WM_ERASEBKGND :
if ( IsDarkModeEnabled ( ) )
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
RECT rc ;
GetClientRect ( hDlg , & rc ) ;
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
return TRUE ;
}
break ;
case WM_CTLCOLORDLG :
case WM_CTLCOLORSTATIC :
case WM_CTLCOLORBTN :
case WM_CTLCOLOREDIT :
case WM_CTLCOLORLISTBOX :
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
HWND hCtrl = reinterpret_cast < HWND > ( lParam ) ;
HBRUSH hBrush = HandleDarkModeCtlColor ( hdc , hCtrl , message ) ;
if ( hBrush )
{
return reinterpret_cast < INT_PTR > ( hBrush ) ;
}
break ;
}
2025-01-16 20:52:24 +00:00
case WM_COMMAND :
switch ( HIWORD ( wParam ) ) {
case BN_CLICKED :
if ( LOWORD ( wParam ) = = IDC_CHECK_SOUND_FILE ) {
2025-11-10 11:57:13 -08:00
EnableWindow ( GetDlgItem ( hDlg , IDC_STATIC_SOUND_FILE ) ,
2025-01-16 20:52:24 +00:00
IsDlgButtonChecked ( hDlg , IDC_CHECK_SOUND_FILE ) = = BST_CHECKED ) ;
2025-11-10 11:57:13 -08:00
EnableWindow ( GetDlgItem ( hDlg , IDC_SOUND_FILE ) ,
IsDlgButtonChecked ( hDlg , IDC_CHECK_SOUND_FILE ) = = BST_CHECKED ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_SOUND_BROWSE ) ,
2025-01-16 20:52:24 +00:00
IsDlgButtonChecked ( hDlg , IDC_CHECK_SOUND_FILE ) = = BST_CHECKED ) ;
}
if ( LOWORD ( wParam ) = = IDC_CHECK_BACKGROUND_FILE ) {
2025-11-10 11:57:13 -08:00
EnableWindow ( GetDlgItem ( hDlg , IDC_CHECK_BACKGROUND_STRETCH ) ,
IsDlgButtonChecked ( hDlg , IDC_CHECK_BACKGROUND_FILE ) = = BST_CHECKED ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_STATIC_DESKTOP_BACKGROUND ) ,
2025-01-16 20:52:24 +00:00
IsDlgButtonChecked ( hDlg , IDC_CHECK_BACKGROUND_FILE ) = = BST_CHECKED ) ;
2025-11-10 11:57:13 -08:00
EnableWindow ( GetDlgItem ( hDlg , IDC_STATIC_BACKGROUND_FILE ) ,
2025-01-16 20:52:24 +00:00
IsDlgButtonChecked ( hDlg , IDC_CHECK_BACKGROUND_FILE ) = = BST_CHECKED ) ;
2025-11-10 11:57:13 -08:00
EnableWindow ( GetDlgItem ( hDlg , IDC_BACKGROUND_FILE ) ,
2025-01-16 20:52:24 +00:00
IsDlgButtonChecked ( hDlg , IDC_CHECK_BACKGROUND_FILE ) = = BST_CHECKED ) ;
2025-11-10 11:57:13 -08:00
EnableWindow ( GetDlgItem ( hDlg , IDC_BACKGROUND_BROWSE ) ,
2025-01-16 20:52:24 +00:00
IsDlgButtonChecked ( hDlg , IDC_CHECK_BACKGROUND_FILE ) = = BST_CHECKED ) ;
}
break ;
}
switch ( LOWORD ( wParam ) ) {
case IDC_SOUND_BROWSE :
2026-02-03 13:05:31 -08:00
{
auto openDialog = wil : : CoCreateInstance < IFileOpenDialog > ( CLSID_FileOpenDialog ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
FILEOPENDIALOGOPTIONS options ;
if ( SUCCEEDED ( openDialog - > GetOptions ( & options ) ) )
openDialog - > SetOptions ( options | FOS_FORCEFILESYSTEM ) ;
COMDLG_FILTERSPEC fileTypes [ ] = {
{ L " Sounds " , L " *.wav " } ,
{ L " All Files " , L " *.* " }
} ;
openDialog - > SetFileTypes ( _countof ( fileTypes ) , fileTypes ) ;
openDialog - > SetFileTypeIndex ( 1 ) ;
openDialog - > SetDefaultExtension ( L " wav " ) ;
openDialog - > SetTitle ( L " ZoomIt: Specify Sound File... " ) ;
// Set initial folder
GetDlgItemText ( hDlg , IDC_SOUND_FILE , filePath , _countof ( filePath ) ) ;
if ( _tcsrchr ( filePath , ' \\ ' ) )
{
2025-01-16 20:52:24 +00:00
_tcscpy ( initDir , filePath ) ;
2026-02-03 13:05:31 -08:00
* ( _tcsrchr ( initDir , ' \\ ' ) + 1 ) = 0 ;
}
else
{
2025-01-16 20:52:24 +00:00
_tcscpy ( filePath , L " %WINDIR% \\ Media " ) ;
2026-02-03 13:05:31 -08:00
ExpandEnvironmentStrings ( filePath , initDir , _countof ( initDir ) ) ;
}
wil : : com_ptr < IShellItem > folderItem ;
if ( SUCCEEDED ( SHCreateItemFromParsingName ( initDir , nullptr , IID_PPV_ARGS ( & folderItem ) ) ) )
{
openDialog - > SetFolder ( folderItem . get ( ) ) ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
OpenSaveDialogEvents * pEvents = new OpenSaveDialogEvents ( false ) ;
DWORD dwCookie = 0 ;
openDialog - > Advise ( pEvents , & dwCookie ) ;
if ( SUCCEEDED ( openDialog - > Show ( hDlg ) ) )
{
wil : : com_ptr < IShellItem > resultItem ;
if ( SUCCEEDED ( openDialog - > GetResult ( & resultItem ) ) )
{
wil : : unique_cotaskmem_string pathStr ;
if ( SUCCEEDED ( resultItem - > GetDisplayName ( SIGDN_FILESYSPATH , & pathStr ) ) )
{
_tcscpy ( newSoundFile , pathStr . get ( ) ) ;
_tcscpy ( filePath , pathStr . get ( ) ) ;
if ( _tcsrchr ( filePath , ' \\ ' ) ) _tcscpy ( filePath , _tcsrchr ( filePath , ' \\ ' ) + 1 ) ;
if ( _tcsrchr ( filePath , ' . ' ) ) * _tcsrchr ( filePath , ' . ' ) = 0 ;
SetDlgItemText ( hDlg , IDC_SOUND_FILE , filePath ) ;
}
}
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
openDialog - > Unadvise ( dwCookie ) ;
pEvents - > Release ( ) ;
2025-01-16 20:52:24 +00:00
break ;
2026-02-03 13:05:31 -08:00
}
2025-01-16 20:52:24 +00:00
case IDC_BACKGROUND_BROWSE :
2026-02-03 13:05:31 -08:00
{
auto openDialog = wil : : CoCreateInstance < IFileOpenDialog > ( CLSID_FileOpenDialog ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
FILEOPENDIALOGOPTIONS options ;
if ( SUCCEEDED ( openDialog - > GetOptions ( & options ) ) )
openDialog - > SetOptions ( options | FOS_FORCEFILESYSTEM ) ;
COMDLG_FILTERSPEC fileTypes [ ] = {
{ L " Bitmap Files (*.bmp;*.dib) " , L " *.bmp;*.dib " } ,
{ L " PNG (*.png) " , L " *.png " } ,
{ L " JPEG (*.jpg;*.jpeg;*.jpe;*.jfif) " , L " *.jpg;*.jpeg;*.jpe;*.jfif " } ,
{ L " GIF (*.gif) " , L " *.gif " } ,
{ L " All Picture Files " , L " *.bmp;*.dib;*.png;*.jpg;*.jpeg;*.jpe;*.jfif;*.gif " } ,
{ L " All Files " , L " *.* " }
} ;
openDialog - > SetFileTypes ( _countof ( fileTypes ) , fileTypes ) ;
openDialog - > SetFileTypeIndex ( 5 ) ; // Default to "All Picture Files"
openDialog - > SetTitle ( L " ZoomIt: Specify Background File... " ) ;
// Set initial folder
GetDlgItemText ( hDlg , IDC_BACKGROUND_FILE , filePath , _countof ( filePath ) ) ;
if ( _tcsrchr ( filePath , ' \\ ' ) )
{
2025-01-16 20:52:24 +00:00
_tcscpy ( initDir , filePath ) ;
2026-02-03 13:05:31 -08:00
* ( _tcsrchr ( initDir , ' \\ ' ) + 1 ) = 0 ;
}
else
{
2025-01-16 20:52:24 +00:00
_tcscpy ( filePath , L " %USERPROFILE% \\ Pictures " ) ;
2026-02-03 13:05:31 -08:00
ExpandEnvironmentStrings ( filePath , initDir , _countof ( initDir ) ) ;
}
wil : : com_ptr < IShellItem > folderItem ;
if ( SUCCEEDED ( SHCreateItemFromParsingName ( initDir , nullptr , IID_PPV_ARGS ( & folderItem ) ) ) )
{
openDialog - > SetFolder ( folderItem . get ( ) ) ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
OpenSaveDialogEvents * pEvents = new OpenSaveDialogEvents ( false ) ;
DWORD dwCookie = 0 ;
openDialog - > Advise ( pEvents , & dwCookie ) ;
if ( SUCCEEDED ( openDialog - > Show ( hDlg ) ) )
{
wil : : com_ptr < IShellItem > resultItem ;
if ( SUCCEEDED ( openDialog - > GetResult ( & resultItem ) ) )
{
wil : : unique_cotaskmem_string pathStr ;
if ( SUCCEEDED ( resultItem - > GetDisplayName ( SIGDN_FILESYSPATH , & pathStr ) ) )
{
_tcscpy ( newBackgroundFile , pathStr . get ( ) ) ;
SetDlgItemText ( hDlg , IDC_BACKGROUND_FILE , pathStr . get ( ) ) ;
}
}
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
openDialog - > Unadvise ( dwCookie ) ;
pEvents - > Release ( ) ;
2025-01-16 20:52:24 +00:00
break ;
2026-02-03 13:05:31 -08:00
}
2025-01-16 20:52:24 +00:00
case IDOK :
// sound file has to be valid
g_BreakPlaySoundFile = IsDlgButtonChecked ( hDlg , IDC_CHECK_SOUND_FILE ) = = BST_CHECKED ;
g_BreakShowBackgroundFile = IsDlgButtonChecked ( hDlg , IDC_CHECK_BACKGROUND_FILE ) = = BST_CHECKED ;
g_BreakBackgroundStretch = IsDlgButtonChecked ( hDlg , IDC_CHECK_BACKGROUND_STRETCH ) = = BST_CHECKED ;
#if 0
g_BreakOnSecondary = IsDlgButtonChecked ( hDlg , IDC_CHECK_SECONDARYDISPLAY ) = = BST_CHECKED ;
# endif
if ( g_BreakPlaySoundFile & & GetFileAttributes ( newSoundFile ) = = - 1 ) {
2025-11-10 11:57:13 -08:00
MessageBox ( hDlg , L " The specified sound file is inaccessible " ,
2025-01-16 20:52:24 +00:00
L " Advanced Break Options Error " , MB_ICONERROR ) ;
break ;
}
_tcscpy ( g_BreakSoundFile , newSoundFile ) ;
// Background file
g_BreakShowDesktop = IsDlgButtonChecked ( hDlg , IDC_STATIC_DESKTOP_BACKGROUND ) = = BST_CHECKED ;
if ( ! g_BreakShowDesktop & & g_BreakShowBackgroundFile & & GetFileAttributes ( newBackgroundFile ) = = - 1 ) {
2025-11-10 11:57:13 -08:00
MessageBox ( hDlg , L " The specified background file is inaccessible " ,
2025-01-16 20:52:24 +00:00
L " Advanced Break Options Error " , MB_ICONERROR ) ;
break ;
}
_tcscpy ( g_BreakBackgroundFile , newBackgroundFile ) ;
for ( i = 0 ; i < 10 ; i + + ) {
if ( IsDlgButtonChecked ( hDlg , IDC_TIMER_POS1 + i ) = = BST_CHECKED ) {
g_BreakTimerPosition = i ;
break ;
}
}
2025-11-10 11:57:13 -08:00
GetDlgItemText ( hDlg , IDC_OPACITY , opacity , sizeof ( opacity ) / sizeof ( opacity [ 0 ] ) ) ;
2025-01-16 20:52:24 +00:00
_stscanf ( opacity , L " %d%% " , & g_BreakOpacity ) ;
reg . WriteRegSettings ( RegSettings ) ;
EndDialog ( hDlg , 0 ) ;
break ;
case IDCANCEL :
EndDialog ( hDlg , 0 ) ;
return TRUE ;
}
break ;
default :
break ;
}
return FALSE ;
}
//----------------------------------------------------------------------------
//
// OptionsTabProc
//
//----------------------------------------------------------------------------
2026-02-03 13:05:31 -08:00
static UINT_PTR CALLBACK ChooseFontHookProc ( HWND hDlg , UINT message , WPARAM wParam , LPARAM lParam )
{
switch ( message )
{
case WM_INITDIALOG :
// Set the dialog icon
{
HICON hIcon = LoadIcon ( g_hInstance , L " APPICON " ) ;
if ( hIcon )
{
SendMessage ( hDlg , WM_SETICON , ICON_BIG , reinterpret_cast < LPARAM > ( hIcon ) ) ;
SendMessage ( hDlg , WM_SETICON , ICON_SMALL , reinterpret_cast < LPARAM > ( hIcon ) ) ;
}
}
// Basic (incomplete) dark mode attempt: theme the main common dialog window.
ApplyDarkModeToDialog ( hDlg ) ;
return 0 ;
case WM_CTLCOLORDLG :
case WM_CTLCOLORSTATIC :
case WM_CTLCOLORBTN :
case WM_CTLCOLOREDIT :
case WM_CTLCOLORLISTBOX :
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
HWND hCtrl = reinterpret_cast < HWND > ( lParam ) ;
HBRUSH hBrush = HandleDarkModeCtlColor ( hdc , hCtrl , message ) ;
if ( hBrush )
{
return reinterpret_cast < UINT_PTR > ( hBrush ) ;
}
break ;
}
}
return 0 ;
}
2025-11-10 11:57:13 -08:00
INT_PTR CALLBACK OptionsTabProc ( HWND hDlg , UINT message ,
WPARAM wParam , LPARAM lParam )
2025-01-16 20:52:24 +00:00
{
HDC hDC ;
LOGFONT lf ;
CHOOSEFONT chooseFont ;
HFONT hFont ;
2025-11-10 11:57:13 -08:00
PAINTSTRUCT ps ;
2025-01-16 20:52:24 +00:00
HWND hTextPreview ;
HDC hDc ;
RECT previewRc ;
switch ( message ) {
case WM_INITDIALOG :
return TRUE ;
2026-02-03 13:05:31 -08:00
case WM_ERASEBKGND :
if ( IsDarkModeEnabled ( ) )
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
RECT rc ;
GetClientRect ( hDlg , & rc ) ;
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
return TRUE ;
}
break ;
case WM_CTLCOLORDLG :
case WM_CTLCOLORSTATIC :
case WM_CTLCOLORBTN :
case WM_CTLCOLOREDIT :
case WM_CTLCOLORLISTBOX :
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
HWND hCtrl = reinterpret_cast < HWND > ( lParam ) ;
HBRUSH hBrush = HandleDarkModeCtlColor ( hdc , hCtrl , message ) ;
if ( hBrush )
{
return reinterpret_cast < INT_PTR > ( hBrush ) ;
}
break ;
}
2025-01-16 20:52:24 +00:00
case WM_COMMAND :
2025-11-10 11:57:13 -08:00
// Handle combo box selection changes
if ( HIWORD ( wParam ) = = CBN_SELCHANGE ) {
if ( LOWORD ( wParam ) = = IDC_RECORD_SCALING ) {
int format = static_cast < int > ( SendMessage ( GetDlgItem ( hDlg , IDC_RECORD_FORMAT ) , CB_GETCURSEL , 0 , 0 ) ) ;
int scale = static_cast < int > ( SendMessage ( GetDlgItem ( hDlg , IDC_RECORD_SCALING ) , CB_GETCURSEL , 0 , 0 ) ) ;
if ( format = = 0 )
{
g_RecordScalingGIF = static_cast < BYTE > ( ( scale + 1 ) * 10 ) ;
}
else
{
g_RecordScalingMP4 = static_cast < BYTE > ( ( scale + 1 ) * 10 ) ;
}
}
else if ( LOWORD ( wParam ) = = IDC_RECORD_FORMAT ) {
// Get the currently selected format
int selection = static_cast < int > ( SendMessage ( GetDlgItem ( hDlg , IDC_RECORD_FORMAT ) ,
CB_GETCURSEL , 0 , 0 ) ) ;
// Get the selected text to check if it's GIF
TCHAR selectedText [ 32 ] = { 0 } ;
SendMessage ( GetDlgItem ( hDlg , IDC_RECORD_FORMAT ) ,
CB_GETLBTEXT , selection , reinterpret_cast < LPARAM > ( selectedText ) ) ;
// Check if GIF is selected by comparing the text
bool isGifSelected = ( wcscmp ( selectedText , L " GIF " ) = = 0 ) ;
2026-02-03 13:05:31 -08:00
// If GIF is selected, set the scaling to the g_RecordScalingGIF value
2025-11-10 11:57:13 -08:00
if ( isGifSelected ) {
g_RecordScaling = g_RecordScalingGIF ;
} else {
g_RecordScaling = g_RecordScalingMP4 ;
}
for ( int i = 0 ; i < 10 ; i + + ) {
int scalingValue = ( i + 1 ) * 10 ;
if ( scalingValue = = static_cast < int > ( g_RecordScaling ) ) {
SendMessage ( GetDlgItem ( hDlg , IDC_RECORD_SCALING ) ,
CB_SETCURSEL , i , 0 ) ;
break ;
}
}
2026-02-03 13:05:31 -08:00
// Enable/disable audio controls based on selection (GIF has no audio)
EnableWindow ( GetDlgItem ( hDlg , IDC_CAPTURE_SYSTEM_AUDIO ) , ! isGifSelected ) ;
2025-11-10 11:57:13 -08:00
EnableWindow ( GetDlgItem ( hDlg , IDC_CAPTURE_AUDIO ) , ! isGifSelected ) ;
2026-02-03 13:05:31 -08:00
EnableWindow ( GetDlgItem ( hDlg , IDC_MICROPHONE_LABEL ) , ! isGifSelected ) ;
EnableWindow ( GetDlgItem ( hDlg , IDC_MICROPHONE ) , ! isGifSelected ) ;
2025-11-10 11:57:13 -08:00
}
}
2025-01-16 20:52:24 +00:00
switch ( LOWORD ( wParam ) ) {
case IDC_ADVANCED_BREAK :
DialogBox ( g_hInstance , L " ADVANCED_BREAK " , hDlg , AdvancedBreakProc ) ;
break ;
case IDC_FONT :
hDC = GetDC ( hDlg ) ;
lf = g_LogFont ;
lf . lfHeight = - 21 ;
chooseFont . hDC = CreateCompatibleDC ( hDC ) ;
ReleaseDC ( hDlg , hDC ) ;
chooseFont . lStructSize = sizeof ( CHOOSEFONT ) ;
chooseFont . hwndOwner = hDlg ;
chooseFont . lpLogFont = & lf ;
2026-02-03 13:05:31 -08:00
chooseFont . Flags = CF_SCREENFONTS | CF_ENABLETEMPLATE | CF_ENABLEHOOK |
2025-11-10 11:57:13 -08:00
CF_INITTOLOGFONTSTRUCT | CF_LIMITSIZE ;
2025-01-16 20:52:24 +00:00
chooseFont . rgbColors = RGB ( 0 , 0 , 0 ) ;
chooseFont . lCustData = 0 ;
chooseFont . nSizeMin = 16 ;
chooseFont . nSizeMax = 16 ;
chooseFont . hInstance = g_hInstance ;
chooseFont . lpszStyle = static_cast < LPTSTR > ( NULL ) ;
chooseFont . nFontType = SCREEN_FONTTYPE ;
2026-02-03 13:05:31 -08:00
chooseFont . lpfnHook = ChooseFontHookProc ;
2025-01-16 20:52:24 +00:00
chooseFont . lpTemplateName = static_cast < LPTSTR > ( MAKEINTRESOURCE ( FORMATDLGORD31 ) ) ;
if ( ChooseFont ( & chooseFont ) ) {
g_LogFont = lf ;
InvalidateRect ( hDlg , NULL , TRUE ) ;
}
break ;
case IDC_DEMOTYPE_BROWSE :
2026-02-03 13:05:31 -08:00
{
auto openDialog = wil : : CoCreateInstance < IFileOpenDialog > ( CLSID_FileOpenDialog ) ;
FILEOPENDIALOGOPTIONS options ;
if ( SUCCEEDED ( openDialog - > GetOptions ( & options ) ) )
openDialog - > SetOptions ( options | FOS_FORCEFILESYSTEM ) ;
COMDLG_FILTERSPEC fileTypes [ ] = {
{ L " All Files " , L " *.* " }
} ;
openDialog - > SetFileTypes ( _countof ( fileTypes ) , fileTypes ) ;
openDialog - > SetFileTypeIndex ( 1 ) ;
openDialog - > SetTitle ( L " ZoomIt: Specify DemoType File... " ) ;
OpenSaveDialogEvents * pEvents = new OpenSaveDialogEvents ( false ) ;
DWORD dwCookie = 0 ;
openDialog - > Advise ( pEvents , & dwCookie ) ;
if ( SUCCEEDED ( openDialog - > Show ( hDlg ) ) )
{
wil : : com_ptr < IShellItem > resultItem ;
if ( SUCCEEDED ( openDialog - > GetResult ( & resultItem ) ) )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
wil : : unique_cotaskmem_string pathStr ;
if ( SUCCEEDED ( resultItem - > GetDisplayName ( SIGDN_FILESYSPATH , & pathStr ) ) )
{
if ( GetFileAttributes ( pathStr . get ( ) ) = = INVALID_FILE_ATTRIBUTES )
{
MessageBox ( hDlg , L " The specified file is inaccessible " , APPNAME , MB_ICONERROR ) ;
}
else
{
SetDlgItemText ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_FILE , pathStr . get ( ) ) ;
_tcscpy ( g_DemoTypeFile , pathStr . get ( ) ) ;
}
}
2025-01-16 20:52:24 +00:00
}
}
2026-02-03 13:05:31 -08:00
openDialog - > Unadvise ( dwCookie ) ;
pEvents - > Release ( ) ;
2025-01-16 20:52:24 +00:00
break ;
}
2026-03-26 13:21:43 +01:00
case IDC_TRIM_FILE :
{
// Open a video file for trimming
auto openDialog = wil : : CoCreateInstance < IFileOpenDialog > ( CLSID_FileOpenDialog ) ;
FILEOPENDIALOGOPTIONS options ;
if ( SUCCEEDED ( openDialog - > GetOptions ( & options ) ) )
openDialog - > SetOptions ( options | FOS_FORCEFILESYSTEM ) ;
COMDLG_FILTERSPEC fileTypes [ ] = {
{ L " Video Files (*.mp4;*.gif) " , L " *.mp4;*.gif " } ,
{ L " MP4 Video (*.mp4) " , L " *.mp4 " } ,
{ L " GIF Animation (*.gif) " , L " *.gif " }
} ;
openDialog - > SetFileTypes ( _countof ( fileTypes ) , fileTypes ) ;
openDialog - > SetFileTypeIndex ( 1 ) ;
openDialog - > SetTitle ( L " ZoomIt: Open Video for Trimming... " ) ;
wil : : com_ptr < IShellItem > videosFolder ;
if ( SUCCEEDED ( SHGetKnownFolderItem ( FOLDERID_Videos , KF_FLAG_DEFAULT , nullptr , IID_PPV_ARGS ( & videosFolder ) ) ) )
openDialog - > SetDefaultFolder ( videosFolder . get ( ) ) ;
OpenSaveDialogEvents * pEvents = new OpenSaveDialogEvents ( false ) ;
DWORD dwCookie = 0 ;
openDialog - > Advise ( pEvents , & dwCookie ) ;
if ( SUCCEEDED ( openDialog - > Show ( hDlg ) ) )
{
wil : : com_ptr < IShellItem > resultItem ;
if ( SUCCEEDED ( openDialog - > GetResult ( & resultItem ) ) )
{
wil : : unique_cotaskmem_string pathStr ;
if ( SUCCEEDED ( resultItem - > GetDisplayName ( SIGDN_FILESYSPATH , & pathStr ) ) )
{
std : : wstring videoPath ( pathStr . get ( ) ) ;
winrt : : Windows : : Foundation : : TimeSpan trimStart { 0 } ;
winrt : : Windows : : Foundation : : TimeSpan trimEnd { 0 } ;
VideoRecordingSession : : ShowTrimDialog (
GetParent ( hDlg ) , videoPath , trimStart , trimEnd , true ) ;
}
}
}
openDialog - > Unadvise ( dwCookie ) ;
pEvents - > Release ( ) ;
break ;
}
2026-02-03 13:05:31 -08:00
}
2025-01-16 20:52:24 +00:00
break ;
case WM_PAINT :
2025-02-13 20:45:52 +01:00
if ( ( hTextPreview = GetDlgItem ( hDlg , IDC_TEXT_FONT ) ) ! = 0 ) {
2025-01-16 20:52:24 +00:00
// 16-pt preview
LOGFONT _lf = g_LogFont ;
_lf . lfHeight = - 21 ;
hFont = CreateFontIndirect ( & _lf ) ;
2025-11-10 11:57:13 -08:00
hDc = BeginPaint ( hDlg , & ps ) ;
2025-01-16 20:52:24 +00:00
SelectObject ( hDc , hFont ) ;
GetWindowRect ( hTextPreview , & previewRc ) ;
2025-11-10 11:57:13 -08:00
MapWindowPoints ( NULL , hDlg , reinterpret_cast < LPPOINT > ( & previewRc ) , 2 ) ;
2025-01-16 20:52:24 +00:00
previewRc . top + = 6 ;
2026-02-03 13:05:31 -08:00
// Set text color based on dark mode
if ( IsDarkModeEnabled ( ) )
{
SetTextColor ( hDc , DarkMode : : TextColor ) ;
SetBkMode ( hDc , TRANSPARENT ) ;
}
DrawText ( hDc , L " AaBbYyZz " , static_cast < int > ( _tcslen ( L " AaBbYyZz " ) ) , & previewRc ,
2025-01-16 20:52:24 +00:00
DT_CENTER | DT_VCENTER | DT_SINGLELINE ) ;
EndPaint ( hDlg , & ps ) ;
DeleteObject ( hFont ) ;
}
2025-11-10 11:57:13 -08:00
break ;
2025-01-16 20:52:24 +00:00
default :
break ;
}
return FALSE ;
}
2026-02-03 13:05:31 -08:00
//----------------------------------------------------------------------------
//
// RepositionTabPages
//
// Reposition tab pages to fit current tab control size (call after DPI change)
//
//----------------------------------------------------------------------------
VOID RepositionTabPages ( HWND hTabCtrl )
{
RECT rc , pageRc ;
GetWindowRect ( hTabCtrl , & rc ) ;
TabCtrl_AdjustRect ( hTabCtrl , FALSE , & rc ) ;
// Inset the display area to leave room for border in dark mode
if ( IsDarkModeEnabled ( ) )
{
rc . left + = 2 ;
rc . top + = 2 ;
rc . right - = 2 ;
rc . bottom - = 2 ;
}
// Get the parent dialog to convert coordinates correctly
HWND hParent = GetParent ( hTabCtrl ) ;
for ( int i = 0 ; i < sizeof ( g_OptionsTabs ) / sizeof ( g_OptionsTabs [ 0 ] ) ; i + + ) {
if ( g_OptionsTabs [ i ] . hPage ) {
pageRc = rc ;
// Convert screen coords to parent dialog client coords
MapWindowPoints ( NULL , hParent , reinterpret_cast < LPPOINT > ( & pageRc ) , 2 ) ;
SetWindowPos ( g_OptionsTabs [ i ] . hPage ,
HWND_TOP ,
pageRc . left , pageRc . top ,
pageRc . right - pageRc . left , pageRc . bottom - pageRc . top ,
SWP_NOACTIVATE | SWP_NOZORDER ) ;
}
}
}
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
//
// OptionsAddTabs
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
VOID OptionsAddTabs ( HWND hOptionsDlg , HWND hTabCtrl )
2025-01-16 20:52:24 +00:00
{
int i ;
TCITEM tcItem ;
RECT rc , pageRc ;
GetWindowRect ( hTabCtrl , & rc ) ;
for ( i = 0 ; i < sizeof ( g_OptionsTabs ) / sizeof ( g_OptionsTabs [ 0 ] ) ; i + + ) {
tcItem . mask = TCIF_TEXT ;
tcItem . pszText = g_OptionsTabs [ i ] . TabTitle ;
TabCtrl_InsertItem ( hTabCtrl , i , & tcItem ) ;
2025-11-10 11:57:13 -08:00
g_OptionsTabs [ i ] . hPage = CreateDialog ( g_hInstance , g_OptionsTabs [ i ] . TabTitle ,
2025-01-16 20:52:24 +00:00
hOptionsDlg , OptionsTabProc ) ;
}
2026-02-03 13:05:31 -08:00
2025-01-16 20:52:24 +00:00
TabCtrl_AdjustRect ( hTabCtrl , FALSE , & rc ) ;
2026-02-03 13:05:31 -08:00
// Inset the display area to leave room for border in dark mode
// Need 2 pixels so tab pages don't cover the 1-pixel border
if ( IsDarkModeEnabled ( ) )
{
rc . left + = 2 ;
rc . top + = 2 ;
rc . right - = 2 ;
rc . bottom - = 2 ;
}
2025-01-16 20:52:24 +00:00
for ( i = 0 ; i < sizeof ( g_OptionsTabs ) / sizeof ( g_OptionsTabs [ 0 ] ) ; i + + ) {
pageRc = rc ;
2026-02-03 13:05:31 -08:00
// Convert screen coords to parent dialog client coords.
MapWindowPoints ( NULL , hOptionsDlg , reinterpret_cast < LPPOINT > ( & pageRc ) , 2 ) ;
2025-01-16 20:52:24 +00:00
SetWindowPos ( g_OptionsTabs [ i ] . hPage ,
HWND_TOP ,
pageRc . left , pageRc . top ,
pageRc . right - pageRc . left , pageRc . bottom - pageRc . top ,
2026-02-03 13:05:31 -08:00
SWP_NOACTIVATE | SWP_HIDEWINDOW ) ;
2025-01-16 20:52:24 +00:00
if ( pEnableThemeDialogTexture ) {
2026-02-03 13:05:31 -08:00
if ( IsDarkModeEnabled ( ) ) {
// Disable theme dialog texture in dark mode - it interferes with dark backgrounds
pEnableThemeDialogTexture ( g_OptionsTabs [ i ] . hPage , ETDT_DISABLE ) ;
} else {
// Enable tab texturing in light mode
pEnableThemeDialogTexture ( g_OptionsTabs [ i ] . hPage , ETDT_ENABLETAB ) ;
}
2025-01-16 20:52:24 +00:00
}
}
2026-02-03 13:05:31 -08:00
// Show the initial page once positioned to reduce visible churn.
if ( g_OptionsTabs [ 0 ] . hPage )
{
ShowWindow ( g_OptionsTabs [ 0 ] . hPage , SW_SHOW ) ;
}
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// UnregisterAllHotkeys
//
//----------------------------------------------------------------------------
void UnregisterAllHotkeys ( HWND hWnd )
{
2026-03-26 13:21:43 +01:00
auto unregisterHotkey = [ hWnd ] ( int id )
{
const BOOL unregistered = UnregisterHotKey ( hWnd , id ) ;
LogHotkeyRegistrationResult ( L " unregister " , hWnd , id , 0 , 0 , unregistered ) ;
} ;
unregisterHotkey ( ZOOM_HOTKEY ) ;
unregisterHotkey ( LIVE_HOTKEY ) ;
unregisterHotkey ( LIVE_DRAW_HOTKEY ) ;
unregisterHotkey ( DRAW_HOTKEY ) ;
unregisterHotkey ( BREAK_HOTKEY ) ;
unregisterHotkey ( RECORD_HOTKEY ) ;
unregisterHotkey ( RECORD_CROP_HOTKEY ) ;
unregisterHotkey ( RECORD_WINDOW_HOTKEY ) ;
unregisterHotkey ( SNIP_HOTKEY ) ;
unregisterHotkey ( SNIP_SAVE_HOTKEY ) ;
unregisterHotkey ( SNIP_PANORAMA_HOTKEY ) ;
unregisterHotkey ( SNIP_PANORAMA_SAVE_HOTKEY ) ;
unregisterHotkey ( SNIP_OCR_HOTKEY ) ;
unregisterHotkey ( DEMOTYPE_HOTKEY ) ;
unregisterHotkey ( DEMOTYPE_RESET_HOTKEY ) ;
unregisterHotkey ( RECORD_GIF_HOTKEY ) ;
unregisterHotkey ( RECORD_GIF_WINDOW_HOTKEY ) ;
unregisterHotkey ( SAVE_IMAGE_HOTKEY ) ;
unregisterHotkey ( SAVE_CROP_HOTKEY ) ;
unregisterHotkey ( COPY_IMAGE_HOTKEY ) ;
unregisterHotkey ( COPY_CROP_HOTKEY ) ;
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// RegisterAllHotkeys
//
//----------------------------------------------------------------------------
void RegisterAllHotkeys ( HWND hWnd )
{
2026-03-26 13:21:43 +01:00
auto registerHotkey = [ hWnd ] ( int id , UINT modifiers , UINT key )
{
const BOOL registered = RegisterHotKey ( hWnd , id , modifiers , key ) ;
LogHotkeyRegistrationResult ( L " register " , hWnd , id , modifiers , key , registered ) ;
} ;
if ( g_ToggleKey ) registerHotkey ( ZOOM_HOTKEY , g_ToggleMod , g_ToggleKey & 0xFF ) ;
2025-01-16 20:52:24 +00:00
if ( g_LiveZoomToggleKey ) {
2026-03-26 13:21:43 +01:00
registerHotkey ( LIVE_HOTKEY , g_LiveZoomToggleMod , g_LiveZoomToggleKey & 0xFF ) ;
registerHotkey ( LIVE_DRAW_HOTKEY , ( g_LiveZoomToggleMod ^ MOD_SHIFT ) , g_LiveZoomToggleKey & 0xFF ) ;
2025-01-16 20:52:24 +00:00
}
2026-03-26 13:21:43 +01:00
if ( g_DrawToggleKey ) registerHotkey ( DRAW_HOTKEY , g_DrawToggleMod , g_DrawToggleKey & 0xFF ) ;
if ( g_BreakToggleKey ) registerHotkey ( BREAK_HOTKEY , g_BreakToggleMod , g_BreakToggleKey & 0xFF ) ;
2025-01-16 20:52:24 +00:00
if ( g_DemoTypeToggleKey ) {
2026-03-26 13:21:43 +01:00
registerHotkey ( DEMOTYPE_HOTKEY , g_DemoTypeToggleMod , g_DemoTypeToggleKey & 0xFF ) ;
registerHotkey ( DEMOTYPE_RESET_HOTKEY , ( g_DemoTypeToggleMod ^ MOD_SHIFT ) , g_DemoTypeToggleKey & 0xFF ) ;
2025-01-16 20:52:24 +00:00
}
if ( g_SnipToggleKey ) {
2026-03-26 13:21:43 +01:00
registerHotkey ( SNIP_HOTKEY , g_SnipToggleMod , g_SnipToggleKey & 0xFF ) ;
registerHotkey ( SNIP_SAVE_HOTKEY , ( g_SnipToggleMod ^ MOD_SHIFT ) , g_SnipToggleKey & 0xFF ) ;
}
if ( g_SnipPanoramaToggleKey & &
( g_SnipPanoramaToggleKey ! = g_SnipToggleKey | | g_SnipPanoramaToggleMod ! = g_SnipToggleMod ) ) {
registerHotkey ( SNIP_PANORAMA_HOTKEY , g_SnipPanoramaToggleMod | MOD_NOREPEAT , g_SnipPanoramaToggleKey & 0xFF ) ;
registerHotkey ( SNIP_PANORAMA_SAVE_HOTKEY , ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT , g_SnipPanoramaToggleKey & 0xFF ) ;
}
if ( g_SnipOcrToggleKey ) {
registerHotkey ( SNIP_OCR_HOTKEY , g_SnipOcrToggleMod , g_SnipOcrToggleKey & 0xFF ) ;
2025-01-16 20:52:24 +00:00
}
if ( g_RecordToggleKey ) {
2026-03-26 13:21:43 +01:00
registerHotkey ( RECORD_HOTKEY , g_RecordToggleMod | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) ;
registerHotkey ( RECORD_CROP_HOTKEY , ( g_RecordToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) ;
registerHotkey ( RECORD_WINDOW_HOTKEY , ( g_RecordToggleMod ^ MOD_ALT ) | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
// Note: COPY_IMAGE_HOTKEY, COPY_CROP_HOTKEY (Ctrl+C, Ctrl+Shift+C) and
// SAVE_IMAGE_HOTKEY, SAVE_CROP_HOTKEY (Ctrl+S, Ctrl+Shift+S) are registered
// only during static zoom mode to avoid blocking system-wide Ctrl+C/Ctrl+S
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// UpdateDrawTabHeaderFont
//
//----------------------------------------------------------------------------
void UpdateDrawTabHeaderFont ( )
{
static HFONT headerFont = nullptr ;
TCHAR text [ 64 ] ;
2026-02-03 13:05:31 -08:00
constexpr int headers [ ] = { IDC_PEN_CONTROL , IDC_COLORS , IDC_HIGHLIGHT_AND_BLUR , IDC_SHAPES , IDC_SCREEN } ;
HWND hPage = g_OptionsTabs [ DRAW_PAGE ] . hPage ;
if ( ! hPage )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
return ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
// Get the font from an actual body text control that has been DPI-scaled.
// This ensures headers use the exact same font as body text, just bold.
// Find the first static text child control (ID -1) to get the scaled body text font.
HFONT hBaseFont = nullptr ;
HWND hChild = GetWindow ( hPage , GW_CHILD ) ;
while ( hChild ! = nullptr )
{
if ( GetDlgCtrlID ( hChild ) = = - 1 ) // IDC_STATIC is -1
{
hBaseFont = reinterpret_cast < HFONT > ( SendMessage ( hChild , WM_GETFONT , 0 , 0 ) ) ;
if ( hBaseFont )
{
break ;
}
}
hChild = GetWindow ( hChild , GW_HWNDNEXT ) ;
}
if ( ! hBaseFont )
{
hBaseFont = static_cast < HFONT > ( GetStockObject ( DEFAULT_GUI_FONT ) ) ;
}
LOGFONT lf { } ;
if ( ! GetObject ( hBaseFont , sizeof ( LOGFONT ) , & lf ) )
{
GetObject ( GetStockObject ( DEFAULT_GUI_FONT ) , sizeof ( LOGFONT ) , & lf ) ;
}
lf . lfWeight = FW_BOLD ;
HFONT newHeaderFont = CreateFontIndirect ( & lf ) ;
if ( ! newHeaderFont )
{
return ;
}
// Swap fonts safely: apply the new font to all header controls first, then delete the old.
HFONT oldHeaderFont = headerFont ;
headerFont = newHeaderFont ;
2025-01-16 20:52:24 +00:00
for ( int i = 0 ; i < _countof ( headers ) ; i + + )
{
// Change the header font to bold
2026-02-03 13:05:31 -08:00
HWND hHeader = GetDlgItem ( hPage , headers [ i ] ) ;
if ( ! hHeader )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
continue ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
// StaticTextSubclassProc already supports a per-control font override via this property.
// Setting it here makes Draw tab headers resilient if something later overwrites WM_SETFONT.
SetPropW ( hHeader , L " ZoomIt.HeaderFont " , headerFont ) ;
SendMessage ( hHeader , WM_SETFONT , reinterpret_cast < WPARAM > ( headerFont ) , TRUE ) ;
2025-01-16 20:52:24 +00:00
// Resize the control to fit the text
GetWindowText ( hHeader , text , sizeof ( text ) / sizeof ( text [ 0 ] ) ) ;
RECT rc ;
GetWindowRect ( hHeader , & rc ) ;
MapWindowPoints ( NULL , g_OptionsTabs [ DRAW_PAGE ] . hPage , reinterpret_cast < LPPOINT > ( & rc ) , 2 ) ;
HDC hDC = GetDC ( hHeader ) ;
SelectFont ( hDC , headerFont ) ;
DrawText ( hDC , text , static_cast < int > ( _tcslen ( text ) ) , & rc , DT_CALCRECT | DT_SINGLELINE | DT_LEFT | DT_VCENTER ) ;
ReleaseDC ( hHeader , hDC ) ;
SetWindowPos ( hHeader , nullptr , 0 , 0 , rc . right - rc . left + ScaleForDpi ( 4 , GetDpiForWindowHelper ( hHeader ) ) , rc . bottom - rc . top , SWP_NOMOVE | SWP_NOZORDER ) ;
2026-02-03 13:05:31 -08:00
InvalidateRect ( hHeader , nullptr , TRUE ) ;
}
if ( oldHeaderFont )
{
DeleteObject ( oldHeaderFont ) ;
2025-01-16 20:52:24 +00:00
}
}
//----------------------------------------------------------------------------
//
2026-02-03 13:05:31 -08:00
// CheckboxSubclassProc
//
// Subclass procedure for checkbox and radio button controls to handle dark mode colors
2025-01-16 20:52:24 +00:00
//
//----------------------------------------------------------------------------
2026-02-03 13:05:31 -08:00
LRESULT CALLBACK CheckboxSubclassProc ( HWND hWnd , UINT uMsg , WPARAM wParam , LPARAM lParam , UINT_PTR uIdSubclass , DWORD_PTR dwRefData )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
switch ( uMsg )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
case WM_PAINT :
if ( IsDarkModeEnabled ( ) )
{
TCHAR dbgText [ 256 ] = { 0 } ;
GetWindowText ( hWnd , dbgText , _countof ( dbgText ) ) ;
bool dbgEnabled = IsWindowEnabled ( hWnd ) ;
LONG dbgStyle = GetWindowLong ( hWnd , GWL_STYLE ) ;
LONG dbgType = dbgStyle & BS_TYPEMASK ;
bool dbgIsRadio = ( dbgType = = BS_RADIOBUTTON | | dbgType = = BS_AUTORADIOBUTTON ) ;
OutputDebugStringW ( ( std : : wstring ( L " [Checkbox] WM_PAINT: " ) + dbgText +
L " enabled= " + ( dbgEnabled ? L " 1 " : L " 0 " ) +
L " isRadio= " + ( dbgIsRadio ? L " 1 " : L " 0 " ) + L " \n " ) . c_str ( ) ) ;
PAINTSTRUCT ps ;
HDC hdc = BeginPaint ( hWnd , & ps ) ;
RECT rc ;
GetClientRect ( hWnd , & rc ) ;
// Fill background
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
// Get button state and style
LRESULT state = SendMessage ( hWnd , BM_GETCHECK , 0 , 0 ) ;
bool isChecked = ( state = = BST_CHECKED ) ;
bool isEnabled = IsWindowEnabled ( hWnd ) ;
// Check if this is a radio button
LONG style = GetWindowLong ( hWnd , GWL_STYLE ) ;
LONG buttonType = style & BS_TYPEMASK ;
bool isRadioButton = ( buttonType = = BS_RADIOBUTTON | | buttonType = = BS_AUTORADIOBUTTON ) ;
// Check if checkbox should be on the right (BS_LEFTTEXT or WS_EX_RIGHT)
bool checkOnRight = ( style & BS_LEFTTEXT ) ! = 0 ;
LONG exStyle = GetWindowLong ( hWnd , GWL_EXSTYLE ) ;
if ( exStyle & WS_EX_RIGHT )
checkOnRight = true ;
// Get DPI for scaling
UINT dpi = GetDpiForWindowHelper ( hWnd ) ;
int checkSize = ScaleForDpi ( 13 , dpi ) ;
int margin = ScaleForDpi ( 2 , dpi ) ;
// Calculate checkbox/radio position
RECT rcCheck ;
if ( checkOnRight )
{
rcCheck . right = rc . right - margin ;
rcCheck . left = rcCheck . right - checkSize ;
}
else
{
rcCheck . left = rc . left + margin ;
rcCheck . right = rcCheck . left + checkSize ;
}
rcCheck . top = rc . top + ( rc . bottom - rc . top - checkSize ) / 2 ;
rcCheck . bottom = rcCheck . top + checkSize ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// Choose colors based on enabled state
COLORREF borderColor = isEnabled ? DarkMode : : BorderColor : RGB ( 60 , 60 , 60 ) ;
COLORREF fillColor = isChecked ? ( isEnabled ? DarkMode : : AccentColor : RGB ( 80 , 80 , 85 ) ) : DarkMode : : SurfaceColor ;
COLORREF textColor = isEnabled ? DarkMode : : TextColor : RGB ( 100 , 100 , 100 ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
if ( isRadioButton )
{
// Draw radio button (circle)
HPEN hPen = CreatePen ( PS_SOLID , 1 , borderColor ) ;
HPEN hOldPen = static_cast < HPEN > ( SelectObject ( hdc , hPen ) ) ;
HBRUSH hFillBrush = CreateSolidBrush ( isChecked ? fillColor : DarkMode : : SurfaceColor ) ;
HBRUSH hOldBrush = static_cast < HBRUSH > ( SelectObject ( hdc , hFillBrush ) ) ;
Ellipse ( hdc , rcCheck . left , rcCheck . top , rcCheck . right , rcCheck . bottom ) ;
SelectObject ( hdc , hOldBrush ) ;
SelectObject ( hdc , hOldPen ) ;
DeleteObject ( hPen ) ;
DeleteObject ( hFillBrush ) ;
// Draw inner circle if checked
if ( isChecked )
{
int innerMargin = ScaleForDpi ( 3 , dpi ) ;
HBRUSH hInnerBrush = CreateSolidBrush ( isEnabled ? RGB ( 255 , 255 , 255 ) : RGB ( 140 , 140 , 140 ) ) ;
HBRUSH hOldInnerBrush = static_cast < HBRUSH > ( SelectObject ( hdc , hInnerBrush ) ) ;
HPEN hNullPen = static_cast < HPEN > ( SelectObject ( hdc , GetStockObject ( NULL_PEN ) ) ) ;
Ellipse ( hdc , rcCheck . left + innerMargin , rcCheck . top + innerMargin ,
rcCheck . right - innerMargin , rcCheck . bottom - innerMargin ) ;
SelectObject ( hdc , hNullPen ) ;
SelectObject ( hdc , hOldInnerBrush ) ;
DeleteObject ( hInnerBrush ) ;
}
}
else
{
// Draw checkbox (rectangle)
HPEN hPen = CreatePen ( PS_SOLID , 1 , borderColor ) ;
HPEN hOldPen = static_cast < HPEN > ( SelectObject ( hdc , hPen ) ) ;
HBRUSH hFillBrush = CreateSolidBrush ( fillColor ) ;
HBRUSH hOldBrush = static_cast < HBRUSH > ( SelectObject ( hdc , hFillBrush ) ) ;
Rectangle ( hdc , rcCheck . left , rcCheck . top , rcCheck . right , rcCheck . bottom ) ;
SelectObject ( hdc , hOldBrush ) ;
SelectObject ( hdc , hOldPen ) ;
DeleteObject ( hPen ) ;
DeleteObject ( hFillBrush ) ;
// Draw checkmark if checked
if ( isChecked )
{
COLORREF checkColor = isEnabled ? RGB ( 255 , 255 , 255 ) : RGB ( 140 , 140 , 140 ) ;
HPEN hCheckPen = CreatePen ( PS_SOLID , ScaleForDpi ( 2 , dpi ) , checkColor ) ;
HPEN hOldCheckPen = static_cast < HPEN > ( SelectObject ( hdc , hCheckPen ) ) ;
// Draw checkmark
int x = rcCheck . left + ScaleForDpi ( 3 , dpi ) ;
int y = rcCheck . top + ScaleForDpi ( 6 , dpi ) ;
MoveToEx ( hdc , x , y , nullptr ) ;
LineTo ( hdc , x + ScaleForDpi ( 2 , dpi ) , y + ScaleForDpi ( 3 , dpi ) ) ;
LineTo ( hdc , x + ScaleForDpi ( 7 , dpi ) , y - ScaleForDpi ( 3 , dpi ) ) ;
SelectObject ( hdc , hOldCheckPen ) ;
DeleteObject ( hCheckPen ) ;
}
}
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// Draw text
TCHAR text [ 256 ] = { 0 } ;
GetWindowText ( hWnd , text , _countof ( text ) ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
SetBkMode ( hdc , TRANSPARENT ) ;
SetTextColor ( hdc , textColor ) ;
HFONT hFont = reinterpret_cast < HFONT > ( SendMessage ( hWnd , WM_GETFONT , 0 , 0 ) ) ;
HFONT hOldFont = nullptr ;
if ( hFont )
{
hOldFont = static_cast < HFONT > ( SelectObject ( hdc , hFont ) ) ;
}
RECT rcText = rc ;
UINT textFormat = DT_VCENTER | DT_SINGLELINE ;
if ( checkOnRight )
{
rcText . right = rcCheck . left - ScaleForDpi ( 4 , dpi ) ;
textFormat | = DT_RIGHT ;
}
else
{
rcText . left = rcCheck . right + ScaleForDpi ( 4 , dpi ) ;
textFormat | = DT_LEFT ;
}
DrawText ( hdc , text , - 1 , & rcText , textFormat ) ;
if ( hOldFont )
{
SelectObject ( hdc , hOldFont ) ;
}
EndPaint ( hWnd , & ps ) ;
return 0 ;
}
break ;
case WM_ENABLE :
if ( IsDarkModeEnabled ( ) )
{
// Let base window proc handle enable state change, but avoid any subclass chain
// that might trigger themed drawing
LRESULT result = DefWindowProc ( hWnd , uMsg , wParam , lParam ) ;
// Force immediate repaint with our custom painting
InvalidateRect ( hWnd , nullptr , TRUE ) ;
UpdateWindow ( hWnd ) ;
return result ;
}
break ;
case WM_NCDESTROY :
RemoveWindowSubclass ( hWnd , CheckboxSubclassProc , uIdSubclass ) ;
break ;
}
return DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
}
//----------------------------------------------------------------------------
//
// HotkeyControlSubclassProc
//
// Subclass procedure for hotkey controls to handle dark mode colors
//
//----------------------------------------------------------------------------
LRESULT CALLBACK HotkeyControlSubclassProc ( HWND hWnd , UINT uMsg , WPARAM wParam , LPARAM lParam , UINT_PTR uIdSubclass , DWORD_PTR dwRefData )
{
switch ( uMsg )
{
case WM_PAINT :
if ( IsDarkModeEnabled ( ) )
{
// Get the hotkey from the control using HKM_GETHOTKEY
LRESULT hk = SendMessage ( hWnd , HKM_GETHOTKEY , 0 , 0 ) ;
WORD hotkey = LOWORD ( hk ) ;
BYTE vk = LOBYTE ( hotkey ) ;
BYTE mods = HIBYTE ( hotkey ) ;
// Build the hotkey text
std : : wstring text ;
if ( vk ! = 0 )
{
if ( mods & HOTKEYF_CONTROL )
text + = L " Ctrl+ " ;
if ( mods & HOTKEYF_SHIFT )
text + = L " Shift+ " ;
if ( mods & HOTKEYF_ALT )
text + = L " Alt+ " ;
// Get key name using virtual key code
UINT scanCode = MapVirtualKeyW ( vk , MAPVK_VK_TO_VSC ) ;
if ( scanCode ! = 0 )
{
TCHAR keyName [ 64 ] = { 0 } ;
LONG lParamKey = ( scanCode < < 16 ) ;
// Set extended key bit for certain keys
if ( ( vk > = VK_PRIOR & & vk < = VK_DELETE ) | |
( vk > = VK_LWIN & & vk < = VK_APPS ) | |
vk = = VK_DIVIDE | | vk = = VK_NUMLOCK )
{
lParamKey | = ( 1 < < 24 ) ;
}
if ( GetKeyNameTextW ( lParamKey , keyName , _countof ( keyName ) ) > 0 )
{
text + = keyName ;
}
else
{
// Fallback: use the virtual key character for printable keys
if ( vk > = ' 0 ' & & vk < = ' 9 ' )
{
text + = static_cast < wchar_t > ( vk ) ;
}
else if ( vk > = ' A ' & & vk < = ' Z ' )
{
text + = static_cast < wchar_t > ( vk ) ;
}
else if ( vk > = VK_F1 & & vk < = VK_F24 )
{
text + = L " F " ;
text + = std : : to_wstring ( vk - VK_F1 + 1 ) ;
}
}
}
else
{
// No scan code, try direct character representation
if ( vk > = ' 0 ' & & vk < = ' 9 ' )
{
text + = static_cast < wchar_t > ( vk ) ;
}
else if ( vk > = ' A ' & & vk < = ' Z ' )
{
text + = static_cast < wchar_t > ( vk ) ;
}
else if ( vk > = VK_F1 & & vk < = VK_F24 )
{
text + = L " F " ;
text + = std : : to_wstring ( vk - VK_F1 + 1 ) ;
}
}
}
PAINTSTRUCT ps ;
HDC hdc = BeginPaint ( hWnd , & ps ) ;
RECT rc ;
GetClientRect ( hWnd , & rc ) ;
// Fill background with dark surface color
FillRect ( hdc , & rc , GetDarkModeSurfaceBrush ( ) ) ;
// No border in dark mode - just the filled background
// Draw text if we have any
if ( ! text . empty ( ) )
{
SetBkMode ( hdc , TRANSPARENT ) ;
SetTextColor ( hdc , DarkMode : : TextColor ) ;
HFONT hFont = reinterpret_cast < HFONT > ( SendMessage ( hWnd , WM_GETFONT , 0 , 0 ) ) ;
HFONT hOldFont = nullptr ;
if ( hFont )
{
hOldFont = static_cast < HFONT > ( SelectObject ( hdc , hFont ) ) ;
}
RECT rcText = rc ;
rcText . left + = 4 ;
rcText . right - = 4 ;
DrawTextW ( hdc , text . c_str ( ) , - 1 , & rcText , DT_LEFT | DT_VCENTER | DT_SINGLELINE ) ;
if ( hOldFont )
{
SelectObject ( hdc , hOldFont ) ;
}
}
EndPaint ( hWnd , & ps ) ;
return 0 ;
}
break ;
case WM_NCPAINT :
if ( IsDarkModeEnabled ( ) )
{
// Fill the non-client area with background color to hide the border
HDC hdc = GetWindowDC ( hWnd ) ;
if ( hdc )
{
RECT rc ;
GetWindowRect ( hWnd , & rc ) ;
int width = rc . right - rc . left ;
int height = rc . bottom - rc . top ;
rc . left = 0 ;
rc . top = 0 ;
rc . right = width ;
rc . bottom = height ;
// Get NC border size
RECT rcClient ;
GetClientRect ( hWnd , & rcClient ) ;
MapWindowPoints ( hWnd , nullptr , reinterpret_cast < LPPOINT > ( & rcClient ) , 2 ) ;
RECT rcWindow ;
GetWindowRect ( hWnd , & rcWindow ) ;
int borderLeft = rcClient . left - rcWindow . left ;
int borderTop = rcClient . top - rcWindow . top ;
int borderRight = rcWindow . right - rcClient . right ;
int borderBottom = rcWindow . bottom - rcClient . bottom ;
// Fill the entire NC border area with background color
HBRUSH hBrush = CreateSolidBrush ( DarkMode : : BackgroundColor ) ;
// Top border
RECT rcTop = { 0 , 0 , width , borderTop } ;
FillRect ( hdc , & rcTop , hBrush ) ;
// Bottom border
RECT rcBottom = { 0 , height - borderBottom , width , height } ;
FillRect ( hdc , & rcBottom , hBrush ) ;
// Left border
RECT rcLeft = { 0 , borderTop , borderLeft , height - borderBottom } ;
FillRect ( hdc , & rcLeft , hBrush ) ;
// Right border
RECT rcRight = { width - borderRight , borderTop , width , height - borderBottom } ;
FillRect ( hdc , & rcRight , hBrush ) ;
DeleteObject ( hBrush ) ;
// Draw thin border around the control
HPEN hPen = CreatePen ( PS_SOLID , 1 , DarkMode : : BorderColor ) ;
HPEN hOldPen = static_cast < HPEN > ( SelectObject ( hdc , hPen ) ) ;
HBRUSH hOldBrush = static_cast < HBRUSH > ( SelectObject ( hdc , GetStockObject ( NULL_BRUSH ) ) ) ;
Rectangle ( hdc , 0 , 0 , width , height ) ;
SelectObject ( hdc , hOldBrush ) ;
SelectObject ( hdc , hOldPen ) ;
DeleteObject ( hPen ) ;
ReleaseDC ( hWnd , hdc ) ;
}
return 0 ;
}
break ;
case WM_NCDESTROY :
RemoveWindowSubclass ( hWnd , HotkeyControlSubclassProc , uIdSubclass ) ;
break ;
}
return DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
}
//----------------------------------------------------------------------------
//
// EditControlSubclassProc
//
// Subclass procedure for edit controls to handle dark mode (no border)
//
//----------------------------------------------------------------------------
LRESULT CALLBACK EditControlSubclassProc ( HWND hWnd , UINT uMsg , WPARAM wParam , LPARAM lParam , UINT_PTR uIdSubclass , DWORD_PTR dwRefData )
{
// Helper to adjust formatting rectangle for vertical text centering
auto AdjustTextRect = [ ] ( HWND hEdit ) {
RECT rcClient ;
GetClientRect ( hEdit , & rcClient ) ;
// Get font metrics to calculate text height
HDC hdc = GetDC ( hEdit ) ;
HFONT hFont = reinterpret_cast < HFONT > ( SendMessage ( hEdit , WM_GETFONT , 0 , 0 ) ) ;
HFONT hOldFont = hFont ? static_cast < HFONT > ( SelectObject ( hdc , hFont ) ) : nullptr ;
TEXTMETRIC tm ;
GetTextMetrics ( hdc , & tm ) ;
int textHeight = tm . tmHeight ;
if ( hOldFont )
SelectObject ( hdc , hOldFont ) ;
ReleaseDC ( hEdit , hdc ) ;
// Calculate vertical offset to center text
int clientHeight = rcClient . bottom - rcClient . top ;
int topOffset = ( clientHeight - textHeight ) / 2 ;
if ( topOffset < 0 ) topOffset = 0 ;
RECT rcFormat = rcClient ;
rcFormat . top = topOffset ;
rcFormat . left + = 2 ; // Small left margin
rcFormat . right - = 2 ; // Small right margin
SendMessage ( hEdit , EM_SETRECT , 0 , reinterpret_cast < LPARAM > ( & rcFormat ) ) ;
} ;
switch ( uMsg )
{
case WM_SIZE :
{
// Adjust the formatting rectangle to vertically center text
LRESULT result = DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
AdjustTextRect ( hWnd ) ;
return result ;
}
case WM_SETFONT :
{
// After font is set, adjust formatting rectangle
LRESULT result = DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
AdjustTextRect ( hWnd ) ;
return result ;
}
case WM_NCPAINT :
if ( IsDarkModeEnabled ( ) )
{
OutputDebugStringW ( L " [Edit] WM_NCPAINT in dark mode \n " ) ;
// Get the window DC which includes NC area
HDC hdc = GetWindowDC ( hWnd ) ;
if ( hdc )
{
RECT rc ;
GetWindowRect ( hWnd , & rc ) ;
int width = rc . right - rc . left ;
int height = rc . bottom - rc . top ;
rc . left = 0 ;
rc . top = 0 ;
rc . right = width ;
rc . bottom = height ;
// Get NC border size
RECT rcClient ;
GetClientRect ( hWnd , & rcClient ) ;
MapWindowPoints ( hWnd , nullptr , reinterpret_cast < LPPOINT > ( & rcClient ) , 2 ) ;
RECT rcWindow ;
GetWindowRect ( hWnd , & rcWindow ) ;
int borderLeft = rcClient . left - rcWindow . left ;
int borderTop = rcClient . top - rcWindow . top ;
int borderRight = rcWindow . right - rcClient . right ;
int borderBottom = rcWindow . bottom - rcClient . bottom ;
OutputDebugStringW ( ( L " [Edit] Border: L= " + std : : to_wstring ( borderLeft ) + L " T= " + std : : to_wstring ( borderTop ) +
L " R= " + std : : to_wstring ( borderRight ) + L " B= " + std : : to_wstring ( borderBottom ) + L " \n " ) . c_str ( ) ) ;
// Fill the entire NC border area with background color
HBRUSH hBrush = CreateSolidBrush ( DarkMode : : BackgroundColor ) ;
// Top border
RECT rcTop = { 0 , 0 , width , borderTop } ;
FillRect ( hdc , & rcTop , hBrush ) ;
// Bottom border
RECT rcBottom = { 0 , height - borderBottom , width , height } ;
FillRect ( hdc , & rcBottom , hBrush ) ;
// Left border
RECT rcLeft = { 0 , borderTop , borderLeft , height - borderBottom } ;
FillRect ( hdc , & rcLeft , hBrush ) ;
// Right border
RECT rcRight = { width - borderRight , borderTop , width , height - borderBottom } ;
FillRect ( hdc , & rcRight , hBrush ) ;
DeleteObject ( hBrush ) ;
// Draw thin border around the control
HPEN hPen = CreatePen ( PS_SOLID , 1 , DarkMode : : BorderColor ) ;
HPEN hOldPen = static_cast < HPEN > ( SelectObject ( hdc , hPen ) ) ;
HBRUSH hOldBrush = static_cast < HBRUSH > ( SelectObject ( hdc , GetStockObject ( NULL_BRUSH ) ) ) ;
Rectangle ( hdc , 0 , 0 , width , height ) ;
SelectObject ( hdc , hOldBrush ) ;
SelectObject ( hdc , hOldPen ) ;
DeleteObject ( hPen ) ;
ReleaseDC ( hWnd , hdc ) ;
}
return 0 ;
}
break ;
case WM_NCDESTROY :
RemoveWindowSubclass ( hWnd , EditControlSubclassProc , uIdSubclass ) ;
break ;
}
return DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
}
//----------------------------------------------------------------------------
//
// SliderSubclassProc
//
// Subclass procedure for slider/trackbar controls to handle dark mode
//
//----------------------------------------------------------------------------
LRESULT CALLBACK SliderSubclassProc ( HWND hWnd , UINT uMsg , WPARAM wParam , LPARAM lParam , UINT_PTR uIdSubclass , DWORD_PTR dwRefData )
{
switch ( uMsg )
{
case WM_LBUTTONDOWN :
case WM_MOUSEMOVE :
case WM_LBUTTONUP :
if ( IsDarkModeEnabled ( ) )
{
// Let the default handler process the message first
LRESULT result = DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
// Force full repaint to avoid artifacts at high DPI
InvalidateRect ( hWnd , nullptr , TRUE ) ;
return result ;
}
break ;
case WM_PAINT :
if ( IsDarkModeEnabled ( ) )
{
PAINTSTRUCT ps ;
HDC hdc = BeginPaint ( hWnd , & ps ) ;
RECT rc ;
GetClientRect ( hWnd , & rc ) ;
// Fill background
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
// Get slider info
RECT rcChannel = { 0 } ;
SendMessage ( hWnd , TBM_GETCHANNELRECT , 0 , reinterpret_cast < LPARAM > ( & rcChannel ) ) ;
RECT rcThumb = { 0 } ;
SendMessage ( hWnd , TBM_GETTHUMBRECT , 0 , reinterpret_cast < LPARAM > ( & rcThumb ) ) ;
// Draw channel (track) - simple dark line
int channelHeight = 4 ;
int channelY = ( rc . bottom + rc . top ) / 2 - channelHeight / 2 ;
RECT rcTrack = { rcChannel . left , channelY , rcChannel . right , channelY + channelHeight } ;
HBRUSH hTrackBrush = CreateSolidBrush ( RGB ( 80 , 80 , 85 ) ) ;
FillRect ( hdc , & rcTrack , hTrackBrush ) ;
DeleteObject ( hTrackBrush ) ;
// Center thumb vertically - at high DPI the thumb rect may not be centered
int thumbHeight = rcThumb . bottom - rcThumb . top ;
int thumbCenterY = ( rc . bottom + rc . top ) / 2 ;
rcThumb . top = thumbCenterY - thumbHeight / 2 ;
rcThumb . bottom = rcThumb . top + thumbHeight ;
// Draw thumb - dark rectangle
HBRUSH hThumbBrush = CreateSolidBrush ( RGB ( 160 , 160 , 165 ) ) ;
FillRect ( hdc , & rcThumb , hThumbBrush ) ;
DeleteObject ( hThumbBrush ) ;
// Draw thumb border
HPEN hPen = CreatePen ( PS_SOLID , 1 , RGB ( 100 , 100 , 105 ) ) ;
HPEN hOldPen = static_cast < HPEN > ( SelectObject ( hdc , hPen ) ) ;
HBRUSH hOldBrush = static_cast < HBRUSH > ( SelectObject ( hdc , GetStockObject ( NULL_BRUSH ) ) ) ;
Rectangle ( hdc , rcThumb . left , rcThumb . top , rcThumb . right , rcThumb . bottom ) ;
SelectObject ( hdc , hOldBrush ) ;
SelectObject ( hdc , hOldPen ) ;
DeleteObject ( hPen ) ;
EndPaint ( hWnd , & ps ) ;
return 0 ;
}
break ;
case WM_ERASEBKGND :
if ( IsDarkModeEnabled ( ) )
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
RECT rc ;
GetClientRect ( hWnd , & rc ) ;
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
return TRUE ;
}
break ;
case WM_NCDESTROY :
RemoveWindowSubclass ( hWnd , SliderSubclassProc , uIdSubclass ) ;
break ;
}
return DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
}
//----------------------------------------------------------------------------
//
// GroupBoxSubclassProc
//
// Subclass procedure for group box controls to handle dark mode painting
//
//----------------------------------------------------------------------------
LRESULT CALLBACK GroupBoxSubclassProc ( HWND hWnd , UINT uMsg , WPARAM wParam , LPARAM lParam , UINT_PTR uIdSubclass , DWORD_PTR dwRefData )
{
switch ( uMsg )
{
case WM_PAINT :
if ( IsDarkModeEnabled ( ) )
{
PAINTSTRUCT ps ;
HDC hdc = BeginPaint ( hWnd , & ps ) ;
RECT rc ;
GetClientRect ( hWnd , & rc ) ;
// Get the text and font
HFONT hFont = reinterpret_cast < HFONT > ( SendMessage ( hWnd , WM_GETFONT , 0 , 0 ) ) ;
HFONT hOldFont = hFont ? static_cast < HFONT > ( SelectObject ( hdc , hFont ) ) : nullptr ;
TCHAR szText [ 256 ] = { 0 } ;
GetWindowText ( hWnd , szText , _countof ( szText ) ) ;
// Measure text
SIZE textSize = { 0 } ;
GetTextExtentPoint32 ( hdc , szText , static_cast < int > ( _tcslen ( szText ) ) , & textSize ) ;
// Text starts at left + 8 pixels
const int textLeft = 8 ;
const int textPadding = 4 ;
int frameTop = textSize . cy / 2 ;
// Only fill the frame border areas, not the interior (to avoid painting over child controls)
// Fill top strip (above frame line)
RECT rcTop = { rc . left , rc . top , rc . right , frameTop + 1 } ;
FillRect ( hdc , & rcTop , GetDarkModeBrush ( ) ) ;
// Fill left edge strip
RECT rcLeft = { rc . left , frameTop , rc . left + 1 , rc . bottom } ;
FillRect ( hdc , & rcLeft , GetDarkModeBrush ( ) ) ;
// Fill right edge strip
RECT rcRight = { rc . right - 1 , frameTop , rc . right , rc . bottom } ;
FillRect ( hdc , & rcRight , GetDarkModeBrush ( ) ) ;
// Fill bottom edge strip
RECT rcBottom = { rc . left , rc . bottom - 1 , rc . right , rc . bottom } ;
FillRect ( hdc , & rcBottom , GetDarkModeBrush ( ) ) ;
// Draw the group box frame (with gap for text)
HPEN hPen = CreatePen ( PS_SOLID , 1 , DarkMode : : BorderColor ) ;
HPEN hOldPen = static_cast < HPEN > ( SelectObject ( hdc , hPen ) ) ;
// Top line - left segment (before text)
MoveToEx ( hdc , rc . left , frameTop , NULL ) ;
LineTo ( hdc , textLeft - textPadding , frameTop ) ;
// Top line - right segment (after text)
MoveToEx ( hdc , textLeft + textSize . cx + textPadding , frameTop , NULL ) ;
LineTo ( hdc , rc . right - 1 , frameTop ) ;
// Right line
LineTo ( hdc , rc . right - 1 , rc . bottom - 1 ) ;
// Bottom line
LineTo ( hdc , rc . left , rc . bottom - 1 ) ;
// Left line
LineTo ( hdc , rc . left , frameTop ) ;
SelectObject ( hdc , hOldPen ) ;
DeleteObject ( hPen ) ;
// Draw text with background
SetBkMode ( hdc , OPAQUE ) ;
SetBkColor ( hdc , DarkMode : : BackgroundColor ) ;
SetTextColor ( hdc , DarkMode : : TextColor ) ;
RECT rcText = { textLeft , 0 , textLeft + textSize . cx , textSize . cy } ;
DrawText ( hdc , szText , - 1 , & rcText , DT_LEFT | DT_SINGLELINE ) ;
if ( hOldFont )
SelectObject ( hdc , hOldFont ) ;
EndPaint ( hWnd , & ps ) ;
return 0 ;
}
break ;
case WM_ERASEBKGND :
// Don't erase background - let parent handle it
if ( IsDarkModeEnabled ( ) )
{
return TRUE ;
}
break ;
case WM_NCDESTROY :
RemoveWindowSubclass ( hWnd , GroupBoxSubclassProc , uIdSubclass ) ;
break ;
}
return DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
}
//----------------------------------------------------------------------------
//
// StaticTextSubclassProc
//
// Subclass procedure for static text controls to handle dark mode painting
//
//----------------------------------------------------------------------------
LRESULT CALLBACK StaticTextSubclassProc ( HWND hWnd , UINT uMsg , WPARAM wParam , LPARAM lParam , UINT_PTR uIdSubclass , DWORD_PTR dwRefData )
{
const int ctrlId = GetDlgCtrlID ( hWnd ) ;
const bool isOptionsHeader = ( ctrlId = = IDC_VERSION | | ctrlId = = IDC_COPYRIGHT ) ;
auto paintStaticText = [ ] ( HWND hWnd , HDC hdc ) - > void
{
RECT rc ;
GetClientRect ( hWnd , & rc ) ;
// Fill background
if ( IsDarkModeEnabled ( ) )
{
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
}
else
{
FillRect ( hdc , & rc , GetSysColorBrush ( COLOR_BTNFACE ) ) ;
}
// Get text
TCHAR text [ 512 ] = { 0 } ;
GetWindowText ( hWnd , text , _countof ( text ) ) ;
// Set up text drawing
SetBkMode ( hdc , TRANSPARENT ) ;
bool isEnabled = IsWindowEnabled ( hWnd ) ;
if ( IsDarkModeEnabled ( ) )
{
SetTextColor ( hdc , isEnabled ? DarkMode : : TextColor : RGB ( 100 , 100 , 100 ) ) ;
}
else
{
SetTextColor ( hdc , isEnabled ? GetSysColor ( COLOR_WINDOWTEXT ) : GetSysColor ( COLOR_GRAYTEXT ) ) ;
}
// Try to get the font from a window property first (for header controls where
// WM_GETFONT may not work reliably), then fall back to WM_GETFONT.
HFONT hFont = static_cast < HFONT > ( GetPropW ( hWnd , L " ZoomIt.HeaderFont " ) ) ;
HFONT hCreatedFont = nullptr ; // Track if we created a font that needs cleanup
// For IDC_VERSION, create a large title font on-demand if the property font doesn't work
const int thisCtrlId = GetDlgCtrlID ( hWnd ) ;
if ( thisCtrlId = = IDC_VERSION )
{
// Create a title font that is proportionally larger than the dialog font
LOGFONT lf { } ;
HFONT hDialogFont = reinterpret_cast < HFONT > ( SendMessage ( GetParent ( hWnd ) , WM_GETFONT , 0 , 0 ) ) ;
if ( hDialogFont )
{
GetObject ( hDialogFont , sizeof ( lf ) , & lf ) ;
}
else
{
GetObject ( GetStockObject ( DEFAULT_GUI_FONT ) , sizeof ( lf ) , & lf ) ;
}
lf . lfWeight = FW_BOLD ;
// Make title 50% larger than dialog font (lfHeight is negative for character height)
lf . lfHeight = MulDiv ( lf . lfHeight , 3 , 2 ) ;
hCreatedFont = CreateFontIndirect ( & lf ) ;
if ( hCreatedFont )
{
hFont = hCreatedFont ;
}
}
if ( ! hFont )
{
hFont = reinterpret_cast < HFONT > ( SendMessage ( hWnd , WM_GETFONT , 0 , 0 ) ) ;
}
HFONT hOldFont = nullptr ;
if ( hFont )
{
hOldFont = static_cast < HFONT > ( SelectObject ( hdc , hFont ) ) ;
}
# if _DEBUG
if ( thisCtrlId = = IDC_VERSION )
{
TEXTMETRIC tm { } ;
GetTextMetrics ( hdc , & tm ) ;
OutputDebug ( L " IDC_VERSION paint: tmHeight=%d selectResult=%p hFont=%p created=%p rc=(%d,%d,%d,%d) \n " ,
tm . tmHeight , hOldFont , hFont , hCreatedFont , rc . left , rc . top , rc . right , rc . bottom ) ;
}
# endif
// Get style to determine alignment and wrapping behavior
LONG style = GetWindowLong ( hWnd , GWL_STYLE ) ;
const LONG staticType = style & SS_TYPEMASK ;
UINT format = 0 ;
if ( style & SS_CENTER )
format | = DT_CENTER ;
else if ( style & SS_RIGHT )
format | = DT_RIGHT ;
else
format | = DT_LEFT ;
if ( style & SS_NOPREFIX )
format | = DT_NOPREFIX ;
bool noWrap = ( staticType = = SS_LEFTNOWORDWRAP ) | | ( staticType = = SS_SIMPLE ) ;
if ( GetDlgCtrlID ( hWnd ) = = IDC_VERSION )
{
// The header title is intended to be a single line.
noWrap = true ;
}
if ( noWrap )
{
// Single-line labels should match the classic static control behavior.
format | = DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS ;
}
else
{
// Multi-line/static text (LTEXT) should wrap like the default control.
format | = DT_WORDBREAK | DT_EDITCONTROL ;
}
DrawText ( hdc , text , - 1 , & rc , format ) ;
if ( hOldFont )
{
SelectObject ( hdc , hOldFont ) ;
}
// Clean up any font we created on-demand
if ( hCreatedFont )
{
DeleteObject ( hCreatedFont ) ;
}
} ;
if ( IsDarkModeEnabled ( ) | | isOptionsHeader )
{
switch ( uMsg )
{
case WM_ERASEBKGND :
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
RECT rc ;
GetClientRect ( hWnd , & rc ) ;
if ( IsDarkModeEnabled ( ) )
{
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
}
else
{
FillRect ( hdc , & rc , GetSysColorBrush ( COLOR_BTNFACE ) ) ;
}
return TRUE ;
}
case WM_PAINT :
{
PAINTSTRUCT ps ;
HDC hdc = BeginPaint ( hWnd , & ps ) ;
paintStaticText ( hWnd , hdc ) ;
EndPaint ( hWnd , & ps ) ;
return 0 ;
}
case WM_PRINTCLIENT :
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
paintStaticText ( hWnd , hdc ) ;
return 0 ;
}
case WM_SETTEXT :
{
// Let the default handle the text change, then repaint
DefWindowProc ( hWnd , uMsg , wParam , lParam ) ;
InvalidateRect ( hWnd , nullptr , TRUE ) ;
return TRUE ;
}
case WM_ENABLE :
{
// Let base window proc handle enable state change, but avoid any subclass chain
// that might trigger themed drawing
LRESULT result = DefWindowProc ( hWnd , uMsg , wParam , lParam ) ;
// Force immediate repaint with our custom painting
InvalidateRect ( hWnd , nullptr , TRUE ) ;
UpdateWindow ( hWnd ) ;
return result ;
}
case WM_NCDESTROY :
# if _DEBUG
RemovePropW ( hWnd , L " ZoomIt.VersionFontLogged " ) ;
# endif
RemoveWindowSubclass ( hWnd , StaticTextSubclassProc , uIdSubclass ) ;
break ;
}
}
else
{
if ( uMsg = = WM_NCDESTROY )
{
# if _DEBUG
RemovePropW ( hWnd , L " ZoomIt.VersionFontLogged " ) ;
# endif
RemoveWindowSubclass ( hWnd , StaticTextSubclassProc , uIdSubclass ) ;
}
}
return DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
}
//----------------------------------------------------------------------------
//
// TabControlSubclassProc
//
// Subclass procedure for tab control to handle dark mode background
//
//----------------------------------------------------------------------------
LRESULT CALLBACK TabControlSubclassProc ( HWND hWnd , UINT uMsg , WPARAM wParam , LPARAM lParam , UINT_PTR uIdSubclass , DWORD_PTR dwRefData )
{
switch ( uMsg )
{
case WM_ERASEBKGND :
if ( IsDarkModeEnabled ( ) )
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
RECT rc ;
GetClientRect ( hWnd , & rc ) ;
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
return TRUE ;
}
break ;
case WM_PAINT :
if ( IsDarkModeEnabled ( ) )
{
PAINTSTRUCT ps ;
HDC hdc = BeginPaint ( hWnd , & ps ) ;
RECT rcClient ;
GetClientRect ( hWnd , & rcClient ) ;
// Fill entire background with dark color
FillRect ( hdc , & rcClient , GetDarkModeBrush ( ) ) ;
// Get the display area (content area below tabs)
RECT rcDisplay = rcClient ;
TabCtrl_AdjustRect ( hWnd , FALSE , & rcDisplay ) ;
// Debug output
TCHAR dbg [ 256 ] ;
_stprintf_s ( dbg , _T ( " TabCtrl: client=(%d,%d,%d,%d) display=(%d,%d,%d,%d) \n " ) ,
rcClient . left , rcClient . top , rcClient . right , rcClient . bottom ,
rcDisplay . left , rcDisplay . top , rcDisplay . right , rcDisplay . bottom ) ;
OutputDebugString ( dbg ) ;
// Draw grey border around the display area
HPEN hPen = CreatePen ( PS_SOLID , 1 , DarkMode : : BorderColor ) ;
HPEN hOldPen = static_cast < HPEN > ( SelectObject ( hdc , hPen ) ) ;
HBRUSH hOldBrush = static_cast < HBRUSH > ( SelectObject ( hdc , GetStockObject ( NULL_BRUSH ) ) ) ;
// Draw border at the edges of the display area (inset by 1 to be visible)
int left = rcDisplay . left ;
int top = rcDisplay . top - 1 ;
int right = ( rcDisplay . right < rcClient . right ) ? rcDisplay . right : rcClient . right - 1 ;
int bottom = ( rcDisplay . bottom < rcClient . bottom ) ? rcDisplay . bottom : rcClient . bottom - 1 ;
_stprintf_s ( dbg , _T ( " TabCtrl border: left=%d top=%d right=%d bottom=%d \n " ) , left , top , right , bottom ) ;
OutputDebugString ( dbg ) ;
// Top line
MoveToEx ( hdc , left , top , NULL ) ;
LineTo ( hdc , right , top ) ;
// Right line
MoveToEx ( hdc , right - 1 , top , NULL ) ;
LineTo ( hdc , right - 1 , bottom ) ;
// Bottom line
MoveToEx ( hdc , left , bottom - 1 , NULL ) ;
LineTo ( hdc , right , bottom - 1 ) ;
// Left line
MoveToEx ( hdc , left , top , NULL ) ;
LineTo ( hdc , left , bottom ) ;
// Draw each tab
int tabCount = TabCtrl_GetItemCount ( hWnd ) ;
int selectedTab = TabCtrl_GetCurSel ( hWnd ) ;
// Get the font from the tab control
HFONT hFont = reinterpret_cast < HFONT > ( SendMessage ( hWnd , WM_GETFONT , 0 , 0 ) ) ;
HFONT hOldFont = hFont ? static_cast < HFONT > ( SelectObject ( hdc , hFont ) ) : nullptr ;
SetBkMode ( hdc , TRANSPARENT ) ;
for ( int i = 0 ; i < tabCount ; i + + )
{
RECT rcTab ;
TabCtrl_GetItemRect ( hWnd , i , & rcTab ) ;
bool isSelected = ( i = = selectedTab ) ;
// Fill tab background
FillRect ( hdc , & rcTab , GetDarkModeBrush ( ) ) ;
// Draw grey border around tab (left, top, right)
MoveToEx ( hdc , rcTab . left , rcTab . bottom - 1 , NULL ) ;
LineTo ( hdc , rcTab . left , rcTab . top ) ;
LineTo ( hdc , rcTab . right - 1 , rcTab . top ) ;
LineTo ( hdc , rcTab . right - 1 , rcTab . bottom ) ;
// For selected tab, erase the bottom border to merge with content
if ( isSelected )
{
HPEN hBgPen = CreatePen ( PS_SOLID , 1 , DarkMode : : BackgroundColor ) ;
SelectObject ( hdc , hBgPen ) ;
MoveToEx ( hdc , rcTab . left + 1 , rcTab . bottom - 1 , NULL ) ;
LineTo ( hdc , rcTab . right - 1 , rcTab . bottom - 1 ) ;
SelectObject ( hdc , hPen ) ;
DeleteObject ( hBgPen ) ;
}
// Get tab text
TCITEM tci = { } ;
tci . mask = TCIF_TEXT ;
TCHAR szText [ 128 ] = { 0 } ;
tci . pszText = szText ;
tci . cchTextMax = _countof ( szText ) ;
TabCtrl_GetItem ( hWnd , i , & tci ) ;
// Draw text
SetTextColor ( hdc , isSelected ? DarkMode : : TextColor : DarkMode : : DisabledTextColor ) ;
RECT rcText = rcTab ;
rcText . top + = 4 ;
DrawText ( hdc , szText , - 1 , & rcText , DT_CENTER | DT_VCENTER | DT_SINGLELINE ) ;
// Draw underline for selected tab
if ( isSelected )
{
RECT rcUnderline = rcTab ;
rcUnderline . top = rcUnderline . bottom - 2 ;
rcUnderline . left + = 1 ;
rcUnderline . right - = 1 ;
HBRUSH hAccent = CreateSolidBrush ( DarkMode : : AccentColor ) ;
FillRect ( hdc , & rcUnderline , hAccent ) ;
DeleteObject ( hAccent ) ;
}
}
if ( hOldFont )
SelectObject ( hdc , hOldFont ) ;
SelectObject ( hdc , hOldBrush ) ;
SelectObject ( hdc , hOldPen ) ;
DeleteObject ( hPen ) ;
EndPaint ( hWnd , & ps ) ;
return 0 ;
}
break ;
case WM_NCDESTROY :
RemoveWindowSubclass ( hWnd , TabControlSubclassProc , uIdSubclass ) ;
break ;
}
return DefSubclassProc ( hWnd , uMsg , wParam , lParam ) ;
}
//----------------------------------------------------------------------------
//
// OptionsProc
//
//----------------------------------------------------------------------------
INT_PTR CALLBACK OptionsProc ( HWND hDlg , UINT message ,
WPARAM wParam , LPARAM lParam )
{
constexpr UINT WM_APPLY_HEADER_FONTS = WM_APP + 42 ;
static HFONT hFontBold = nullptr ;
static HFONT hFontVersion = nullptr ;
PNMLINK notify = nullptr ;
static int curTabSel = 0 ;
static HWND hTabCtrl ;
static HWND hOpacity ;
static HWND hToggleKey ;
static UINT currentDpi = DPI_BASELINE ;
static RECT stableWindowRect { } ;
static bool stableWindowRectValid = false ;
TCHAR text [ 32 ] ;
2026-03-26 13:21:43 +01:00
DWORD newToggleKey , newTimeout , newToggleMod , newBreakToggleKey , newDemoTypeToggleKey , newRecordToggleKey , newSnipToggleKey , newSnipPanoramaToggleKey , newSnipOcrToggleKey ;
DWORD newDrawToggleKey , newDrawToggleMod , newBreakToggleMod , newDemoTypeToggleMod , newRecordToggleMod , newSnipToggleMod , newSnipPanoramaToggleMod , newSnipOcrToggleMod ;
2026-02-03 13:05:31 -08:00
DWORD newLiveZoomToggleKey , newLiveZoomToggleMod ;
static std : : vector < std : : pair < std : : wstring , std : : wstring > > microphones ;
auto CleanupFonts = [ & ] ( )
{
if ( hFontBold )
{
DeleteObject ( hFontBold ) ;
hFontBold = nullptr ;
}
if ( hFontVersion )
{
DeleteObject ( hFontVersion ) ;
hFontVersion = nullptr ;
}
} ;
auto UpdateVersionFont = [ & ] ( )
{
if ( hFontVersion )
{
DeleteObject ( hFontVersion ) ;
hFontVersion = nullptr ;
}
HWND hVersion = GetDlgItem ( hDlg , IDC_VERSION ) ;
if ( ! hVersion )
{
return ;
}
// Prefer the control's current font (it may already be DPI-scaled).
HFONT hBaseFont = reinterpret_cast < HFONT > ( SendMessage ( hVersion , WM_GETFONT , 0 , 0 ) ) ;
if ( ! hBaseFont )
{
hBaseFont = reinterpret_cast < HFONT > ( SendMessage ( hDlg , WM_GETFONT , 0 , 0 ) ) ;
}
if ( ! hBaseFont )
{
hBaseFont = static_cast < HFONT > ( GetStockObject ( DEFAULT_GUI_FONT ) ) ;
}
LOGFONT lf { } ;
if ( ! GetObject ( hBaseFont , sizeof ( lf ) , & lf ) )
{
return ;
}
// Make the header version text title-sized using an explicit point size,
// scaled by the current DPI.
const UINT dpi = GetDpiForWindowHelper ( hDlg ) ;
constexpr int kTitlePointSize = 22 ;
lf . lfWeight = FW_BOLD ;
lf . lfHeight = - MulDiv ( kTitlePointSize , static_cast < int > ( dpi ) , 72 ) ;
hFontVersion = CreateFontIndirect ( & lf ) ;
if ( hFontVersion )
{
SendMessage ( hVersion , WM_SETFONT , reinterpret_cast < WPARAM > ( hFontVersion ) , TRUE ) ;
// Also store in a property so our subclass paint can reliably retrieve it.
SetPropW ( hVersion , L " ZoomIt.HeaderFont " , reinterpret_cast < HANDLE > ( hFontVersion ) ) ;
# if _DEBUG
HFONT checkFont = static_cast < HFONT > ( GetPropW ( hVersion , L " ZoomIt.HeaderFont " ) ) ;
OutputDebug ( L " SetPropW HeaderFont: hwnd=%p font=%p verify=%p \n " , hVersion , hFontVersion , checkFont ) ;
# endif
}
# if _DEBUG
OutputDebug ( L " UpdateVersionFont: dpi=%u titlePt=%d lfHeight=%d font=%p \n " ,
dpi , kTitlePointSize , lf . lfHeight , hFontVersion ) ;
{
HFONT currentFont = reinterpret_cast < HFONT > ( SendMessage ( hVersion , WM_GETFONT , 0 , 0 ) ) ;
LOGFONT currentLf { } ;
if ( currentFont & & GetObject ( currentFont , sizeof ( currentLf ) , & currentLf ) )
{
OutputDebug ( L " IDC_VERSION WM_GETFONT after set: font=%p lfHeight=%d lfWeight=%d \n " ,
currentFont , currentLf . lfHeight , currentLf . lfWeight ) ;
}
else
{
OutputDebug ( L " IDC_VERSION WM_GETFONT after set: font=%p (no logfont) \n " , currentFont ) ;
}
}
# endif
// Resize the version control to fit the new font, and reflow the lines below if needed.
RECT rcVersion { } ;
GetWindowRect ( hVersion , & rcVersion ) ;
MapWindowPoints ( nullptr , hDlg , reinterpret_cast < LPPOINT > ( & rcVersion ) , 2 ) ;
const int oldVersionHeight = rcVersion . bottom - rcVersion . top ;
TCHAR versionText [ 128 ] = { } ;
GetWindowText ( hVersion , versionText , _countof ( versionText ) ) ;
RECT rcCalc { 0 , 0 , 0 , 0 } ;
HDC hdc = GetDC ( hVersion ) ;
if ( hdc )
{
HFONT oldFont = static_cast < HFONT > ( SelectObject ( hdc , hFontVersion ? hFontVersion : hBaseFont ) ) ;
DrawText ( hdc , versionText , - 1 , & rcCalc , DT_CALCRECT | DT_SINGLELINE | DT_LEFT | DT_VCENTER ) ;
SelectObject ( hdc , oldFont ) ;
ReleaseDC ( hVersion , hdc ) ;
}
// Keep within dialog client width.
RECT rcClient { } ;
GetClientRect ( hDlg , & rcClient ) ;
const int maxWidth = max ( 0 , rcClient . right - rcVersion . left - ScaleForDpi ( 8 , GetDpiForWindowHelper ( hDlg ) ) ) ;
const int desiredWidth = min ( maxWidth , ( rcCalc . right - rcCalc . left ) + ScaleForDpi ( 6 , GetDpiForWindowHelper ( hDlg ) ) ) ;
const int desiredHeight = ( rcCalc . bottom - rcCalc . top ) + ScaleForDpi ( 2 , GetDpiForWindowHelper ( hDlg ) ) ;
const int newVersionHeight = max ( oldVersionHeight , desiredHeight ) ;
SetWindowPos ( hVersion , nullptr ,
rcVersion . left , rcVersion . top ,
max ( 1 , desiredWidth ) , newVersionHeight ,
SWP_NOZORDER | SWP_NOACTIVATE ) ;
# if _DEBUG
{
RECT rcAfter { } ;
GetClientRect ( hVersion , & rcAfter ) ;
OutputDebug ( L " UpdateVersionFont resize: desired=(%d,%d) oldH=%d newH=%d actual=(%d,%d) \n " ,
desiredWidth , desiredHeight , oldVersionHeight , newVersionHeight ,
rcAfter . right - rcAfter . left , rcAfter . bottom - rcAfter . top ) ;
}
# endif
InvalidateRect ( hVersion , nullptr , TRUE ) ;
const int deltaY = newVersionHeight - oldVersionHeight ;
if ( deltaY > 0 )
{
const int headerIdsToShift [ ] = { IDC_COPYRIGHT , IDC_LINK } ;
for ( int i = 0 ; i < _countof ( headerIdsToShift ) ; i + + )
{
HWND hCtrl = GetDlgItem ( hDlg , headerIdsToShift [ i ] ) ;
if ( ! hCtrl )
{
continue ;
}
RECT rc { } ;
GetWindowRect ( hCtrl , & rc ) ;
MapWindowPoints ( nullptr , hDlg , reinterpret_cast < LPPOINT > ( & rc ) , 2 ) ;
SetWindowPos ( hCtrl , nullptr ,
rc . left , rc . top + deltaY ,
0 , 0 ,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE ) ;
}
}
} ;
switch ( message ) {
case WM_INITDIALOG :
{
if ( hWndOptions ) {
BringWindowToTop ( hWndOptions ) ;
SetFocus ( hWndOptions ) ;
SetForegroundWindow ( hWndOptions ) ;
EndDialog ( hDlg , 0 ) ;
return FALSE ;
}
hWndOptions = hDlg ;
// Set the dialog icon
{
HICON hIcon = LoadIcon ( g_hInstance , L " APPICON " ) ;
if ( hIcon )
{
SendMessage ( hDlg , WM_SETICON , ICON_BIG , reinterpret_cast < LPARAM > ( hIcon ) ) ;
SendMessage ( hDlg , WM_SETICON , ICON_SMALL , reinterpret_cast < LPARAM > ( hIcon ) ) ;
}
}
SetForegroundWindow ( hDlg ) ;
SetActiveWindow ( hDlg ) ;
// Do not force-show the dialog here. DialogBox will show it after WM_INITDIALOG
// returns, and showing early causes visible layout churn while we add tabs, scale,
// and center the window.
# if 1
// set version info
TCHAR filePath [ MAX_PATH ] ;
const TCHAR * verString ;
GetModuleFileName ( NULL , filePath , _countof ( filePath ) ) ;
DWORD zero = 0 ;
DWORD infoSize = GetFileVersionInfoSize ( filePath , & zero ) ;
void * versionInfo = malloc ( infoSize ) ;
GetFileVersionInfo ( filePath , 0 , infoSize , versionInfo ) ;
verString = GetVersionString ( static_cast < VERSION_INFO * > ( versionInfo ) , _T ( " FileVersion " ) ) ;
SetDlgItemText ( hDlg , IDC_VERSION , ( std : : wstring ( L " ZoomIt v " ) + verString ) . c_str ( ) ) ;
2025-01-16 20:52:24 +00:00
verString = GetVersionString ( static_cast < VERSION_INFO * > ( versionInfo ) , _T ( " LegalCopyright " ) ) ;
SetDlgItemText ( hDlg , IDC_COPYRIGHT , verString ) ;
free ( versionInfo ) ;
# endif
// Add tabs
hTabCtrl = GetDlgItem ( hDlg , IDC_TAB ) ;
2026-02-03 13:05:31 -08:00
// Set owner-draw style for tab control when in dark mode
if ( IsDarkModeEnabled ( ) )
{
LONG_PTR style = GetWindowLongPtr ( hTabCtrl , GWL_STYLE ) ;
SetWindowLongPtr ( hTabCtrl , GWL_STYLE , style | TCS_OWNERDRAWFIXED ) ;
// Subclass the tab control for dark mode background painting
SetWindowSubclass ( hTabCtrl , TabControlSubclassProc , 1 , 0 ) ;
}
OptionsAddTabs ( hDlg , hTabCtrl ) ;
2025-01-16 20:52:24 +00:00
// Configure options
2025-11-10 11:57:13 -08:00
SendMessage ( GetDlgItem ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_HOTKEY ) , HKM_SETRULES ,
static_cast < WPARAM > ( HKCOMB_NONE ) , // invalid key combinations
MAKELPARAM ( HOTKEYF_ALT , 0 ) ) ; // add ALT to invalid entries
2025-01-16 20:52:24 +00:00
if ( g_ToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_HOTKEY ) , HKM_SETHOTKEY , g_ToggleKey , 0 ) ;
if ( pMagInitialize ) {
if ( g_LiveZoomToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ LIVE_PAGE ] . hPage , IDC_LIVE_HOTKEY ) , HKM_SETHOTKEY , g_LiveZoomToggleKey , 0 ) ;
} else {
EnableWindow ( GetDlgItem ( g_OptionsTabs [ LIVE_PAGE ] . hPage , IDC_LIVE_HOTKEY ) , FALSE ) ;
EnableWindow ( GetDlgItem ( g_OptionsTabs [ LIVE_PAGE ] . hPage , IDC_ZOOM_LEVEL ) , FALSE ) ;
EnableWindow ( GetDlgItem ( g_OptionsTabs [ LIVE_PAGE ] . hPage , IDC_ZOOM_SPIN ) , FALSE ) ;
}
if ( g_DrawToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ DRAW_PAGE ] . hPage , IDC_DRAW_HOTKEY ) , HKM_SETHOTKEY , g_DrawToggleKey , 0 ) ;
if ( g_BreakToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_BREAK_HOTKEY ) , HKM_SETHOTKEY , g_BreakToggleKey , 0 ) ;
if ( g_DemoTypeToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_HOTKEY ) , HKM_SETHOTKEY , g_DemoTypeToggleKey , 0 ) ;
if ( g_RecordToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_HOTKEY ) , HKM_SETHOTKEY , g_RecordToggleKey , 0 ) ;
if ( g_SnipToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ SNIP_PAGE ] . hPage , IDC_SNIP_HOTKEY ) , HKM_SETHOTKEY , g_SnipToggleKey , 0 ) ;
2026-03-26 13:21:43 +01:00
if ( g_SnipPanoramaToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ PANORAMA_PAGE ] . hPage , IDC_SNIP_PANORAMA_HOTKEY ) , HKM_SETHOTKEY , g_SnipPanoramaToggleKey , 0 ) ;
if ( g_SnipOcrToggleKey ) SendMessage ( GetDlgItem ( g_OptionsTabs [ SNIP_PAGE ] . hPage , IDC_SNIP_OCR_HOTKEY ) , HKM_SETHOTKEY , g_SnipOcrToggleKey , 0 ) ;
2025-11-10 11:57:13 -08:00
CheckDlgButton ( hDlg , IDC_SHOW_TRAY_ICON ,
2025-01-16 20:52:24 +00:00
g_ShowTrayIcon ? BST_CHECKED : BST_UNCHECKED ) ;
2025-11-10 11:57:13 -08:00
CheckDlgButton ( hDlg , IDC_AUTOSTART ,
2025-01-16 20:52:24 +00:00
IsAutostartConfigured ( ) ? BST_CHECKED : BST_UNCHECKED ) ;
2025-11-10 11:57:13 -08:00
CheckDlgButton ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_ANIMATE_ZOOM ,
2025-01-16 20:52:24 +00:00
g_AnimateZoom ? BST_CHECKED : BST_UNCHECKED ) ;
2025-11-10 11:57:13 -08:00
CheckDlgButton ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_SMOOTH_IMAGE ,
2025-10-07 11:20:00 -07:00
g_SmoothImage ? BST_CHECKED : BST_UNCHECKED ) ;
2025-01-16 20:52:24 +00:00
SendMessage ( GetDlgItem ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_ZOOM_SLIDER ) , TBM_SETRANGE , false , MAKELONG ( 0 , _countof ( g_ZoomLevels ) - 1 ) ) ;
SendMessage ( GetDlgItem ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_ZOOM_SLIDER ) , TBM_SETPOS , true , g_SliderZoomLevel ) ;
_stprintf ( text , L " %d " , g_PenWidth ) ;
SetDlgItemText ( g_OptionsTabs [ DRAW_PAGE ] . hPage , IDC_PEN_WIDTH , text ) ;
SendMessage ( GetDlgItem ( g_OptionsTabs [ DRAW_PAGE ] . hPage , IDC_PEN_WIDTH ) , EM_LIMITTEXT , 1 , 0 ) ;
2025-11-10 11:57:13 -08:00
SendMessage ( GetDlgItem ( g_OptionsTabs [ DRAW_PAGE ] . hPage , IDC_SPIN ) , UDM_SETRANGE , 0L ,
2025-01-16 20:52:24 +00:00
MAKELPARAM ( 19 , 1 ) ) ;
_stprintf ( text , L " %d " , g_BreakTimeout ) ;
SetDlgItemText ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_TIMER , text ) ;
SendMessage ( GetDlgItem ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_TIMER ) , EM_LIMITTEXT , 2 , 0 ) ;
2025-11-10 11:57:13 -08:00
SendMessage ( GetDlgItem ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_SPIN_TIMER ) , UDM_SETRANGE , 0L ,
2025-01-16 20:52:24 +00:00
MAKELPARAM ( 99 , 1 ) ) ;
CheckDlgButton ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_CHECK_SHOW_EXPIRED ,
g_ShowExpiredTime ? BST_CHECKED : BST_UNCHECKED ) ;
2026-03-26 13:21:43 +01:00
CheckDlgButton ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_CHECK_LOCK_WORKSTATION ,
g_BreakLockWorkstation ? BST_CHECKED : BST_UNCHECKED ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
CheckDlgButton ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_CAPTURE_SYSTEM_AUDIO ,
g_CaptureSystemAudio ? BST_CHECKED : BST_UNCHECKED ) ;
2025-11-10 11:57:13 -08:00
CheckDlgButton ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_CAPTURE_AUDIO ,
2025-01-16 20:52:24 +00:00
g_CaptureAudio ? BST_CHECKED : BST_UNCHECKED ) ;
2026-02-05 01:05:33 +01:00
CheckDlgButton ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_MIC_MONO_MIX ,
g_MicMonoMix ? BST_CHECKED : BST_UNCHECKED ) ;
2025-11-14 17:36:31 -08:00
//
// The framerate drop down list is not used in the current version (might be added in the future)
//
/*for (int i = 0; i < _countof(g_FramerateOptions); i++) {
2025-01-16 20:52:24 +00:00
_stprintf ( text , L " %d " , g_FramerateOptions [ i ] ) ;
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_FRAME_RATE ) , static_cast < UINT > ( CB_ADDSTRING ) ,
static_cast < WPARAM > ( 0 ) , reinterpret_cast < LPARAM > ( text ) ) ;
if ( g_RecordFrameRate = = g_FramerateOptions [ i ] ) {
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_FRAME_RATE ) , CB_SETCURSEL , static_cast < WPARAM > ( i ) , static_cast < LPARAM > ( 0 ) ) ;
}
2025-11-14 17:36:31 -08:00
} */
2025-11-10 11:57:13 -08:00
// Add the recording format to the combo box and set the current selection
size_t selection = 0 ;
const wchar_t * currentFormatString = ( g_RecordingFormat = = RecordingFormat : : GIF ) ? L " GIF " : L " MP4 " ;
for ( size_t i = 0 ; i < ( sizeof ( g_RecordingFormats ) / sizeof ( g_RecordingFormats [ 0 ] ) ) ; i + + )
{
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_FORMAT ) , static_cast < UINT > ( CB_ADDSTRING ) , static_cast < WPARAM > ( 0 ) , reinterpret_cast < LPARAM > ( g_RecordingFormats [ i ] ) ) ;
if ( selection = = 0 & & wcscmp ( g_RecordingFormats [ i ] , currentFormatString ) = = 0 )
{
selection = i ;
}
}
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_FORMAT ) , CB_SETCURSEL , static_cast < WPARAM > ( selection ) , static_cast < LPARAM > ( 0 ) ) ;
2025-01-16 20:52:24 +00:00
for ( unsigned int i = 1 ; i < 11 ; i + + ) {
2026-02-03 13:05:31 -08:00
_stprintf ( text , L " %2.1f " , ( static_cast < double > ( i ) ) / 10 ) ;
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_SCALING ) , static_cast < UINT > ( CB_ADDSTRING ) ,
static_cast < WPARAM > ( 0 ) , reinterpret_cast < LPARAM > ( text ) ) ;
if ( g_RecordingFormat = = RecordingFormat : : GIF & & i * 10 = = g_RecordScalingGIF ) {
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_SCALING ) , CB_SETCURSEL , static_cast < WPARAM > ( i ) - 1 , static_cast < LPARAM > ( 0 ) ) ;
}
if ( g_RecordingFormat = = RecordingFormat : : MP4 & & i * 10 = = g_RecordScalingMP4 ) {
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_SCALING ) , CB_SETCURSEL , static_cast < WPARAM > ( i ) - 1 , static_cast < LPARAM > ( 0 ) ) ;
}
}
// Get the current set of microphones
microphones . clear ( ) ;
concurrency : : create_task ( [ ] {
auto devices = winrt : : DeviceInformation : : FindAllAsync ( winrt : : DeviceClass : : AudioCapture ) . get ( ) ;
for ( auto device : devices )
{
microphones . emplace_back ( device . Id ( ) . c_str ( ) , device . Name ( ) . c_str ( ) ) ;
}
} ) . get ( ) ;
// Add the microphone devices to the combo box and set the current selection
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_MICROPHONE ) , static_cast < UINT > ( CB_ADDSTRING ) , static_cast < WPARAM > ( 0 ) , reinterpret_cast < LPARAM > ( L " Default " ) ) ;
selection = 0 ;
for ( size_t i = 0 ; i < microphones . size ( ) ; i + + )
{
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_MICROPHONE ) , static_cast < UINT > ( CB_ADDSTRING ) , static_cast < WPARAM > ( 0 ) , reinterpret_cast < LPARAM > ( microphones [ i ] . second . c_str ( ) ) ) ;
if ( selection = = 0 & & wcscmp ( microphones [ i ] . first . c_str ( ) , g_MicrophoneDeviceId ) = = 0 )
{
selection = i + 1 ;
}
}
SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_MICROPHONE ) , CB_SETCURSEL , static_cast < WPARAM > ( selection ) , static_cast < LPARAM > ( 0 ) ) ;
// Set initial state of audio controls based on recording format (GIF has no audio)
bool isGifSelected = ( g_RecordingFormat = = RecordingFormat : : GIF ) ;
EnableWindow ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_CAPTURE_SYSTEM_AUDIO ) , ! isGifSelected ) ;
EnableWindow ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_CAPTURE_AUDIO ) , ! isGifSelected ) ;
EnableWindow ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_MICROPHONE_LABEL ) , ! isGifSelected ) ;
EnableWindow ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_MICROPHONE ) , ! isGifSelected ) ;
if ( GetFileAttributes ( g_DemoTypeFile ) = = - 1 )
{
memset ( g_DemoTypeFile , 0 , sizeof ( g_DemoTypeFile ) ) ;
}
else
{
SetDlgItemText ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_FILE , g_DemoTypeFile ) ;
}
SendMessage ( GetDlgItem ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_SPEED_SLIDER ) , TBM_SETRANGE , false , MAKELONG ( MAX_TYPING_SPEED , MIN_TYPING_SPEED ) ) ;
SendMessage ( GetDlgItem ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_SPEED_SLIDER ) , TBM_SETPOS , true , g_DemoTypeSpeedSlider ) ;
CheckDlgButton ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_USER_DRIVEN , g_DemoTypeUserDriven ? BST_CHECKED : BST_UNCHECKED ) ;
// Apply DPI scaling to the main dialog and to controls inside tab pages.
// Note: Scaling the main dialog only scales its direct children (including the
// tab page windows), but NOT the controls contained within the tab pages.
// So we scale each tab page's child controls separately.
currentDpi = GetDpiForWindowHelper ( hDlg ) ;
if ( currentDpi ! = DPI_BASELINE )
{
ScaleDialogForDpi ( hDlg , currentDpi , DPI_BASELINE ) ;
for ( int i = 0 ; i < sizeof ( g_OptionsTabs ) / sizeof ( g_OptionsTabs [ 0 ] ) ; i + + )
{
if ( g_OptionsTabs [ i ] . hPage )
{
ScaleChildControlsForDpi ( g_OptionsTabs [ i ] . hPage , currentDpi , DPI_BASELINE ) ;
}
}
}
// Always reposition tab pages to fit the tab control (whether scaled or not)
RepositionTabPages ( hTabCtrl ) ;
// Initialize DPI-aware fonts after scaling so text sizing is correct.
InitializeFonts ( hDlg , & hFontBold ) ;
UpdateDrawTabHeaderFont ( ) ;
UpdateVersionFont ( ) ;
// Always render the header labels using our static text subclass (even in light mode)
// so the larger title font is honored.
if ( HWND hVersion = GetDlgItem ( hDlg , IDC_VERSION ) )
{
SetWindowSubclass ( hVersion , StaticTextSubclassProc , 55 , 0 ) ;
}
if ( HWND hCopyright = GetDlgItem ( hDlg , IDC_COPYRIGHT ) )
{
SetWindowSubclass ( hCopyright , StaticTextSubclassProc , 56 , 0 ) ;
}
// Apply dark mode to the dialog and all tab pages
ApplyDarkModeToDialog ( hDlg ) ;
for ( int i = 0 ; i < sizeof ( g_OptionsTabs ) / sizeof ( g_OptionsTabs [ 0 ] ) ; i + + )
{
if ( g_OptionsTabs [ i ] . hPage )
{
ApplyDarkModeToDialog ( g_OptionsTabs [ i ] . hPage ) ;
}
}
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
// Center dialog on screen, clamping to fit if it's too large for the work area
{
RECT rcDlg ;
GetWindowRect ( hDlg , & rcDlg ) ;
int dlgWidth = rcDlg . right - rcDlg . left ;
int dlgHeight = rcDlg . bottom - rcDlg . top ;
// Get the monitor where the cursor is
POINT pt ;
GetCursorPos ( & pt ) ;
HMONITOR hMon = MonitorFromPoint ( pt , MONITOR_DEFAULTTONEAREST ) ;
MONITORINFO mi = { sizeof ( mi ) } ;
GetMonitorInfo ( hMon , & mi ) ;
// Calculate available work area size
const int workWidth = mi . rcWork . right - mi . rcWork . left ;
const int workHeight = mi . rcWork . bottom - mi . rcWork . top ;
// Clamp dialog size to fit within work area (with a small margin)
constexpr int kMargin = 8 ;
if ( dlgWidth > workWidth - kMargin * 2 )
{
dlgWidth = workWidth - kMargin * 2 ;
}
if ( dlgHeight > workHeight - kMargin * 2 )
{
dlgHeight = workHeight - kMargin * 2 ;
}
// Apply clamped size if it changed
if ( dlgWidth ! = ( rcDlg . right - rcDlg . left ) | | dlgHeight ! = ( rcDlg . bottom - rcDlg . top ) )
{
SetWindowPos ( hDlg , nullptr , 0 , 0 , dlgWidth , dlgHeight , SWP_NOMOVE | SWP_NOZORDER ) ;
}
int x = mi . rcWork . left + ( workWidth - dlgWidth ) / 2 ;
int y = mi . rcWork . top + ( workHeight - dlgHeight ) / 2 ;
SetWindowPos ( hDlg , nullptr , x , y , 0 , 0 , SWP_NOSIZE | SWP_NOZORDER ) ;
}
// Capture a stable window size so per-monitor DPI changes won't resize/reflow the dialog.
GetWindowRect ( hDlg , & stableWindowRect ) ;
stableWindowRectValid = true ;
PostMessage ( hDlg , WM_USER , 0 , 0 ) ;
// Reapply header fonts once the dialog has finished any late initialization.
PostMessage ( hDlg , WM_APPLY_HEADER_FONTS , 0 , 0 ) ;
// Set focus to the tab control instead of the first hotkey control
SetFocus ( hTabCtrl ) ;
return FALSE ;
}
case WM_APPLY_HEADER_FONTS :
InitializeFonts ( hDlg , & hFontBold ) ;
UpdateDrawTabHeaderFont ( ) ;
UpdateVersionFont ( ) ;
return TRUE ;
case WM_USER + 100 :
BringWindowToTop ( hDlg ) ;
SetFocus ( hDlg ) ;
SetForegroundWindow ( hDlg ) ;
return TRUE ;
case WM_DPICHANGED :
{
// Requirement: keep the Options dialog stable while it is open.
// Windows may already have resized the window by the time this arrives,
// so explicitly restore the previous size (but allow the suggested top-left).
RECT * suggested = reinterpret_cast < RECT * > ( lParam ) ;
if ( stableWindowRectValid & & suggested )
{
const int stableW = stableWindowRect . right - stableWindowRect . left ;
const int stableH = stableWindowRect . bottom - stableWindowRect . top ;
SetWindowPos ( hDlg , nullptr ,
suggested - > left ,
suggested - > top ,
stableW ,
stableH ,
SWP_NOZORDER | SWP_NOACTIVATE ) ;
}
return TRUE ;
}
case WM_ERASEBKGND :
if ( IsDarkModeEnabled ( ) )
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
RECT rc ;
GetClientRect ( hDlg , & rc ) ;
FillRect ( hdc , & rc , GetDarkModeBrush ( ) ) ;
return TRUE ;
}
break ;
case WM_CTLCOLORDLG :
case WM_CTLCOLORSTATIC :
case WM_CTLCOLORBTN :
case WM_CTLCOLOREDIT :
case WM_CTLCOLORLISTBOX :
{
HDC hdc = reinterpret_cast < HDC > ( wParam ) ;
HWND hCtrl = reinterpret_cast < HWND > ( lParam ) ;
2025-11-10 11:57:13 -08:00
2026-02-03 13:05:31 -08:00
// Always force the Options header title to use the large version font.
// Note: We must also return a brush in light mode
// dialog proc may ignore our HDC changes.
if ( message = = WM_CTLCOLORSTATIC & & hCtrl = = GetDlgItem ( hDlg , IDC_VERSION ) & & hFontVersion )
{
SetBkMode ( hdc , TRANSPARENT ) ;
SelectObject ( hdc , hFontVersion ) ;
2025-11-10 11:57:13 -08:00
2026-02-03 13:05:31 -08:00
# if _DEBUG
OutputDebug ( L " WM_CTLCOLORSTATIC IDC_VERSION: dark=%d font=%p \n " , IsDarkModeEnabled ( ) ? 1 : 0 , hFontVersion ) ;
# endif
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
if ( ! IsDarkModeEnabled ( ) )
{
// Light mode: explicitly return the dialog background brush.
return reinterpret_cast < INT_PTR > ( GetSysColorBrush ( COLOR_BTNFACE ) ) ;
2025-01-16 20:52:24 +00:00
}
}
2026-02-03 13:05:31 -08:00
// Handle dark mode colors
HBRUSH hBrush = HandleDarkModeCtlColor ( hdc , hCtrl , message ) ;
if ( hBrush )
{
// Ensure the header version text uses the title font in dark mode.
if ( message = = WM_CTLCOLORSTATIC & & hCtrl = = GetDlgItem ( hDlg , IDC_VERSION ) & & hFontVersion )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
SelectObject ( hdc , hFontVersion ) ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
// For bold title controls, also set the bold font
if ( message = = WM_CTLCOLORSTATIC & &
( hCtrl = = GetDlgItem ( hDlg , IDC_TITLE ) | |
hCtrl = = GetDlgItem ( hDlg , IDC_DRAWING ) | |
hCtrl = = GetDlgItem ( hDlg , IDC_ZOOM ) | |
hCtrl = = GetDlgItem ( hDlg , IDC_BREAK ) | |
hCtrl = = GetDlgItem ( hDlg , IDC_TYPE ) ) )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
SelectObject ( hdc , hFontBold ) ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
return reinterpret_cast < INT_PTR > ( hBrush ) ;
2025-01-16 20:52:24 +00:00
}
2025-11-10 11:57:13 -08:00
2026-02-03 13:05:31 -08:00
// Light mode handling for bold title controls
if ( message = = WM_CTLCOLORSTATIC & &
( hCtrl = = GetDlgItem ( hDlg , IDC_TITLE ) | |
hCtrl = = GetDlgItem ( hDlg , IDC_DRAWING ) | |
hCtrl = = GetDlgItem ( hDlg , IDC_ZOOM ) | |
hCtrl = = GetDlgItem ( hDlg , IDC_BREAK ) | |
hCtrl = = GetDlgItem ( hDlg , IDC_TYPE ) ) )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
SetBkMode ( hdc , TRANSPARENT ) ;
SelectObject ( hdc , hFontBold ) ;
return reinterpret_cast < INT_PTR > ( GetSysColorBrush ( COLOR_BTNFACE ) ) ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
break ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
case WM_SETTINGCHANGE :
// Handle theme change (dark/light mode toggle)
if ( lParam & & ( wcscmp ( reinterpret_cast < LPCWSTR > ( lParam ) , L " ImmersiveColorSet " ) = = 0 ) )
{
RefreshDarkModeState ( ) ;
ApplyDarkModeToDialog ( hDlg ) ;
for ( int i = 0 ; i < sizeof ( g_OptionsTabs ) / sizeof ( g_OptionsTabs [ 0 ] ) ; i + + )
{
if ( g_OptionsTabs [ i ] . hPage )
{
ApplyDarkModeToDialog ( g_OptionsTabs [ i ] . hPage ) ;
}
}
InvalidateRect ( hDlg , nullptr , TRUE ) ;
for ( int i = 0 ; i < sizeof ( g_OptionsTabs ) / sizeof ( g_OptionsTabs [ 0 ] ) ; i + + )
{
if ( g_OptionsTabs [ i ] . hPage )
{
InvalidateRect ( g_OptionsTabs [ i ] . hPage , nullptr , TRUE ) ;
}
}
}
2025-01-16 20:52:24 +00:00
break ;
2026-02-03 13:05:31 -08:00
case WM_DRAWITEM :
{
// Handle owner-draw for tab control in dark mode
DRAWITEMSTRUCT * pDIS = reinterpret_cast < DRAWITEMSTRUCT * > ( lParam ) ;
if ( pDIS - > CtlID = = IDC_TAB & & IsDarkModeEnabled ( ) )
{
// Fill tab background
HBRUSH hBrush = GetDarkModeBrush ( ) ;
FillRect ( pDIS - > hDC , & pDIS - > rcItem , hBrush ) ;
// Get tab text
TCITEM tci = { } ;
tci . mask = TCIF_TEXT ;
TCHAR szText [ 128 ] = { 0 } ;
tci . pszText = szText ;
tci . cchTextMax = _countof ( szText ) ;
TabCtrl_GetItem ( hTabCtrl , pDIS - > itemID , & tci ) ;
// Draw text
SetBkMode ( pDIS - > hDC , TRANSPARENT ) ;
bool isSelected = ( pDIS - > itemState & ODS_SELECTED ) ! = 0 ;
SetTextColor ( pDIS - > hDC , isSelected ? DarkMode : : TextColor : DarkMode : : DisabledTextColor ) ;
// Draw underline for selected tab
if ( isSelected )
{
RECT rcUnderline = pDIS - > rcItem ;
rcUnderline . top = rcUnderline . bottom - 2 ;
HBRUSH hAccent = CreateSolidBrush ( DarkMode : : AccentColor ) ;
FillRect ( pDIS - > hDC , & rcUnderline , hAccent ) ;
DeleteObject ( hAccent ) ;
}
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
RECT rcText = pDIS - > rcItem ;
rcText . top + = 4 ;
DrawText ( pDIS - > hDC , szText , - 1 , & rcText , DT_CENTER | DT_VCENTER | DT_SINGLELINE ) ;
return TRUE ;
2025-01-16 20:52:24 +00:00
}
break ;
2026-02-03 13:05:31 -08:00
}
2025-01-16 20:52:24 +00:00
case WM_NOTIFY :
notify = reinterpret_cast < PNMLINK > ( lParam ) ;
if ( notify - > hdr . idFrom = = IDC_LINK )
{
switch ( notify - > hdr . code )
{
case NM_CLICK :
case NM_RETURN :
ShellExecute ( hDlg , _T ( " open " ) , notify - > item . szUrl , NULL , NULL , SW_SHOWNORMAL ) ;
break ;
}
}
else switch ( notify - > hdr . code )
{
case TCN_SELCHANGE :
ShowWindow ( g_OptionsTabs [ curTabSel ] . hPage , SW_HIDE ) ;
curTabSel = TabCtrl_GetCurSel ( hTabCtrl ) ;
ShowWindow ( g_OptionsTabs [ curTabSel ] . hPage , SW_SHOW ) ;
break ;
}
break ;
case WM_COMMAND :
switch ( LOWORD ( wParam ) ) {
case IDOK :
{
if ( ! ConfigureAutostart ( hDlg , IsDlgButtonChecked ( hDlg , IDC_AUTOSTART ) = = BST_CHECKED ) ) {
break ;
}
g_ShowTrayIcon = IsDlgButtonChecked ( hDlg , IDC_SHOW_TRAY_ICON ) = = BST_CHECKED ;
g_AnimateZoom = IsDlgButtonChecked ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_ANIMATE_ZOOM ) = = BST_CHECKED ;
2025-10-07 11:20:00 -07:00
g_SmoothImage = IsDlgButtonChecked ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_SMOOTH_IMAGE ) = = BST_CHECKED ;
2025-01-16 20:52:24 +00:00
g_DemoTypeUserDriven = IsDlgButtonChecked ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_USER_DRIVEN ) = = BST_CHECKED ;
newToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
newLiveZoomToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ LIVE_PAGE ] . hPage , IDC_LIVE_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
newDrawToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ DRAW_PAGE ] . hPage , IDC_DRAW_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
newBreakToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_BREAK_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
newDemoTypeToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
newRecordToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
newSnipToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ SNIP_PAGE ] . hPage , IDC_SNIP_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
2026-03-26 13:21:43 +01:00
newSnipPanoramaToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ PANORAMA_PAGE ] . hPage , IDC_SNIP_PANORAMA_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
newSnipOcrToggleKey = static_cast < DWORD > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ SNIP_PAGE ] . hPage , IDC_SNIP_OCR_HOTKEY ) , HKM_GETHOTKEY , 0 , 0 ) ) ;
2025-01-16 20:52:24 +00:00
newToggleMod = GetKeyMod ( newToggleKey ) ;
newLiveZoomToggleMod = GetKeyMod ( newLiveZoomToggleKey ) ;
newDrawToggleMod = GetKeyMod ( newDrawToggleKey ) ;
newBreakToggleMod = GetKeyMod ( newBreakToggleKey ) ;
newDemoTypeToggleMod = GetKeyMod ( newDemoTypeToggleKey ) ;
newRecordToggleMod = GetKeyMod ( newRecordToggleKey ) ;
newSnipToggleMod = GetKeyMod ( newSnipToggleKey ) ;
2026-03-26 13:21:43 +01:00
newSnipPanoramaToggleMod = GetKeyMod ( newSnipPanoramaToggleKey ) ;
newSnipOcrToggleMod = GetKeyMod ( newSnipOcrToggleKey ) ;
2025-01-16 20:52:24 +00:00
g_SliderZoomLevel = static_cast < int > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ ZOOM_PAGE ] . hPage , IDC_ZOOM_SLIDER ) , TBM_GETPOS , 0 , 0 ) ) ;
g_DemoTypeSpeedSlider = static_cast < int > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ DEMOTYPE_PAGE ] . hPage , IDC_DEMOTYPE_SPEED_SLIDER ) , TBM_GETPOS , 0 , 0 ) ) ;
g_ShowExpiredTime = IsDlgButtonChecked ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_CHECK_SHOW_EXPIRED ) = = BST_CHECKED ;
2026-03-26 13:21:43 +01:00
g_BreakLockWorkstation = IsDlgButtonChecked ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_CHECK_LOCK_WORKSTATION ) = = BST_CHECKED ;
2026-02-03 13:05:31 -08:00
g_CaptureSystemAudio = IsDlgButtonChecked ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_CAPTURE_SYSTEM_AUDIO ) = = BST_CHECKED ;
2025-01-16 20:52:24 +00:00
g_CaptureAudio = IsDlgButtonChecked ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_CAPTURE_AUDIO ) = = BST_CHECKED ;
2026-02-05 01:05:33 +01:00
g_MicMonoMix = IsDlgButtonChecked ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_MIC_MONO_MIX ) = = BST_CHECKED ;
2025-01-16 20:52:24 +00:00
GetDlgItemText ( g_OptionsTabs [ BREAK_PAGE ] . hPage , IDC_TIMER , text , 3 ) ;
text [ 2 ] = 0 ;
newTimeout = _tstoi ( text ) ;
2025-11-10 11:57:13 -08:00
g_RecordingFormat = static_cast < RecordingFormat > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_FORMAT ) , static_cast < UINT > ( CB_GETCURSEL ) , static_cast < WPARAM > ( 0 ) , static_cast < LPARAM > ( 0 ) ) ) ;
2025-11-14 17:36:31 -08:00
g_RecordFrameRate = ( g_RecordingFormat = = RecordingFormat : : GIF ) ? RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE : RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE ;
2025-01-16 20:52:24 +00:00
g_RecordScaling = static_cast < int > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_RECORD_SCALING ) , static_cast < UINT > ( CB_GETCURSEL ) , static_cast < WPARAM > ( 0 ) , static_cast < LPARAM > ( 0 ) ) * 10 + 10 ) ;
// Get the selected microphone
int index = static_cast < int > ( SendMessage ( GetDlgItem ( g_OptionsTabs [ RECORD_PAGE ] . hPage , IDC_MICROPHONE ) , static_cast < UINT > ( CB_GETCURSEL ) , static_cast < WPARAM > ( 0 ) , static_cast < LPARAM > ( 0 ) ) ) ;
_tcscpy ( g_MicrophoneDeviceId , index = = 0 ? L " " : microphones [ static_cast < size_t > ( index ) - 1 ] . first . c_str ( ) ) ;
if ( newToggleKey & & ! RegisterHotKey ( GetParent ( hDlg ) , ZOOM_HOTKEY , newToggleMod , newToggleKey & 0xFF ) ) {
MessageBox ( hDlg , L " The specified zoom toggle hotkey is already in use. \n Select a different zoom toggle hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
2025-11-10 11:57:13 -08:00
} else if ( newLiveZoomToggleKey & &
2025-01-16 20:52:24 +00:00
( ! RegisterHotKey ( GetParent ( hDlg ) , LIVE_HOTKEY , newLiveZoomToggleMod , newLiveZoomToggleKey & 0xFF ) | |
! RegisterHotKey ( GetParent ( hDlg ) , LIVE_DRAW_HOTKEY , ( newLiveZoomToggleMod ^ MOD_SHIFT ) , newLiveZoomToggleKey & 0xFF ) ) ) {
MessageBox ( hDlg , L " The specified live-zoom toggle hotkey is already in use. \n Select a different zoom toggle hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
} else if ( newDrawToggleKey & & ! RegisterHotKey ( GetParent ( hDlg ) , DRAW_HOTKEY , newDrawToggleMod , newDrawToggleKey & 0xFF ) ) {
MessageBox ( hDlg , L " The specified draw w/out zoom hotkey is already in use. \n Select a different draw w/out zoom hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
} else if ( newBreakToggleKey & & ! RegisterHotKey ( GetParent ( hDlg ) , BREAK_HOTKEY , newBreakToggleMod , newBreakToggleKey & 0xFF ) ) {
MessageBox ( hDlg , L " The specified break timer hotkey is already in use. \n Select a different break timer hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
2025-11-10 11:57:13 -08:00
} else if ( newDemoTypeToggleKey & &
2025-01-16 20:52:24 +00:00
( ! RegisterHotKey ( GetParent ( hDlg ) , DEMOTYPE_HOTKEY , newDemoTypeToggleMod , newDemoTypeToggleKey & 0xFF ) | |
! RegisterHotKey ( GetParent ( hDlg ) , DEMOTYPE_RESET_HOTKEY , ( newDemoTypeToggleMod ^ MOD_SHIFT ) , newDemoTypeToggleKey & 0xFF ) ) ) {
MessageBox ( hDlg , L " The specified live-type hotkey is already in use. \n Select a different live-type hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
}
2025-11-10 11:57:13 -08:00
else if ( newSnipToggleKey & &
2025-01-16 20:52:24 +00:00
( ! RegisterHotKey ( GetParent ( hDlg ) , SNIP_HOTKEY , newSnipToggleMod , newSnipToggleKey & 0xFF ) | |
! RegisterHotKey ( GetParent ( hDlg ) , SNIP_SAVE_HOTKEY , ( newSnipToggleMod ^ MOD_SHIFT ) , newSnipToggleKey & 0xFF ) ) ) {
MessageBox ( hDlg , L " The specified snip hotkey is already in use. \n Select a different snip hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
2026-03-26 13:21:43 +01:00
}
else if ( newSnipPanoramaToggleKey & &
( newSnipPanoramaToggleKey ! = newSnipToggleKey | | newSnipPanoramaToggleMod ! = newSnipToggleMod ) & &
( ! RegisterHotKey ( GetParent ( hDlg ) , SNIP_PANORAMA_HOTKEY , newSnipPanoramaToggleMod | MOD_NOREPEAT , newSnipPanoramaToggleKey & 0xFF ) | |
! RegisterHotKey ( GetParent ( hDlg ) , SNIP_PANORAMA_SAVE_HOTKEY , ( newSnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT , newSnipPanoramaToggleKey & 0xFF ) ) ) {
MessageBox ( hDlg , L " The specified panorama snip hotkey is already in use. \n Select a different panorama snip hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
}
else if ( newSnipOcrToggleKey & &
! RegisterHotKey ( GetParent ( hDlg ) , SNIP_OCR_HOTKEY , newSnipOcrToggleMod , newSnipOcrToggleKey & 0xFF ) ) {
MessageBox ( hDlg , L " The specified snip OCR hotkey is already in use. \n Select a different snip OCR hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
2025-11-10 11:57:13 -08:00
}
else if ( newRecordToggleKey & &
2025-01-16 20:52:24 +00:00
( ! RegisterHotKey ( GetParent ( hDlg ) , RECORD_HOTKEY , newRecordToggleMod | MOD_NOREPEAT , newRecordToggleKey & 0xFF ) | |
! RegisterHotKey ( GetParent ( hDlg ) , RECORD_CROP_HOTKEY , ( newRecordToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT , newRecordToggleKey & 0xFF ) | |
! RegisterHotKey ( GetParent ( hDlg ) , RECORD_WINDOW_HOTKEY , ( newRecordToggleMod ^ MOD_ALT ) | MOD_NOREPEAT , newRecordToggleKey & 0xFF ) ) ) {
MessageBox ( hDlg , L " The specified record hotkey is already in use. \n Select a different record hotkey. " ,
APPNAME , MB_ICONERROR ) ;
UnregisterAllHotkeys ( GetParent ( hDlg ) ) ;
break ;
} else {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
g_BreakTimeout = newTimeout ;
g_ToggleKey = newToggleKey ;
g_LiveZoomToggleKey = newLiveZoomToggleKey ;
g_ToggleMod = newToggleMod ;
g_DrawToggleKey = newDrawToggleKey ;
g_DrawToggleMod = newDrawToggleMod ;
g_BreakToggleKey = newBreakToggleKey ;
g_BreakToggleMod = newBreakToggleMod ;
g_DemoTypeToggleKey = newDemoTypeToggleKey ;
g_DemoTypeToggleMod = newDemoTypeToggleMod ;
g_RecordToggleKey = newRecordToggleKey ;
g_RecordToggleMod = newRecordToggleMod ;
g_SnipToggleKey = newSnipToggleKey ;
g_SnipToggleMod = newSnipToggleMod ;
2026-03-26 13:21:43 +01:00
g_SnipPanoramaToggleKey = newSnipPanoramaToggleKey ;
g_SnipPanoramaToggleMod = newSnipPanoramaToggleMod ;
g_SnipOcrToggleKey = newSnipOcrToggleKey ;
g_SnipOcrToggleMod = newSnipOcrToggleMod ;
2025-01-16 20:52:24 +00:00
reg . WriteRegSettings ( RegSettings ) ;
EnableDisableTrayIcon ( GetParent ( hDlg ) , g_ShowTrayIcon ) ;
hWndOptions = NULL ;
2026-02-03 13:05:31 -08:00
CleanupFonts ( ) ;
2025-01-16 20:52:24 +00:00
EndDialog ( hDlg , 0 ) ;
2025-11-10 11:57:13 -08:00
return TRUE ;
2025-01-16 20:52:24 +00:00
}
break ;
}
case IDCANCEL :
RegisterAllHotkeys ( GetParent ( hDlg ) ) ;
hWndOptions = NULL ;
2026-02-03 13:05:31 -08:00
CleanupFonts ( ) ;
2025-01-16 20:52:24 +00:00
EndDialog ( hDlg , 0 ) ;
return TRUE ;
}
2025-11-10 11:57:13 -08:00
break ;
2025-01-16 20:52:24 +00:00
case WM_CLOSE :
hWndOptions = NULL ;
RegisterAllHotkeys ( GetParent ( hDlg ) ) ;
2026-02-03 13:05:31 -08:00
CleanupFonts ( ) ;
2025-01-16 20:52:24 +00:00
EndDialog ( hDlg , 0 ) ;
return TRUE ;
default :
break ;
}
return FALSE ;
}
//----------------------------------------------------------------------------
//
// DeleteDrawUndoList
//
//----------------------------------------------------------------------------
void DeleteDrawUndoList ( P_DRAW_UNDO * DrawUndoList )
{
P_DRAW_UNDO nextUndo ;
nextUndo = * DrawUndoList ;
while ( nextUndo ) {
* DrawUndoList = nextUndo - > Next ;
DeleteObject ( nextUndo - > hBitmap ) ;
DeleteDC ( nextUndo - > hDc ) ;
free ( nextUndo ) ;
nextUndo = * DrawUndoList ;
}
* DrawUndoList = NULL ;
}
//----------------------------------------------------------------------------
//
// PopDrawUndo
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
BOOLEAN PopDrawUndo ( HDC hDc , P_DRAW_UNDO * DrawUndoList ,
2025-01-16 20:52:24 +00:00
int width , int height )
{
P_DRAW_UNDO nextUndo ;
nextUndo = * DrawUndoList ;
if ( nextUndo ) {
2025-11-10 11:57:13 -08:00
BitBlt ( hDc , 0 , 0 , width , height ,
2025-01-16 20:52:24 +00:00
nextUndo - > hDc , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
* DrawUndoList = nextUndo - > Next ;
DeleteObject ( nextUndo - > hBitmap ) ;
DeleteDC ( nextUndo - > hDc ) ;
free ( nextUndo ) ;
return TRUE ;
} else {
Beep ( 700 , 200 ) ;
return FALSE ;
}
}
//----------------------------------------------------------------------------
//
// DeleteOldestUndo
//
//----------------------------------------------------------------------------
void DeleteOldestUndo ( P_DRAW_UNDO * DrawUndoList )
{
P_DRAW_UNDO nextUndo , freeUndo = NULL , prevUndo = NULL ;
nextUndo = * DrawUndoList ;
freeUndo = nextUndo ;
do {
prevUndo = freeUndo ;
freeUndo = nextUndo ;
nextUndo = nextUndo - > Next ;
} while ( nextUndo ) ;
if ( freeUndo ) {
DeleteObject ( freeUndo - > hBitmap ) ;
DeleteDC ( freeUndo - > hDc ) ;
free ( freeUndo ) ;
if ( prevUndo ! = * DrawUndoList ) prevUndo - > Next = NULL ;
else * DrawUndoList = NULL ;
}
}
//----------------------------------------------------------------------------
//
// GetOldestUndo
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
P_DRAW_UNDO GetOldestUndo ( P_DRAW_UNDO DrawUndoList )
{
P_DRAW_UNDO nextUndo , oldestUndo = NULL ;
nextUndo = DrawUndoList ;
oldestUndo = nextUndo ;
do {
oldestUndo = nextUndo ;
nextUndo = nextUndo - > Next ;
} while ( nextUndo ) ;
return oldestUndo ;
}
//----------------------------------------------------------------------------
//
// PushDrawUndo
//
//----------------------------------------------------------------------------
void PushDrawUndo ( HDC hDc , P_DRAW_UNDO * DrawUndoList , int width , int height )
{
P_DRAW_UNDO nextUndo , newUndo ;
int i = 0 ;
HBITMAP hUndoBitmap ;
OutputDebug ( L " PushDrawUndo \n " ) ;
// Don't store more than 8 undo's (XP gets really upset when we
// exhaust heap with them)
nextUndo = * DrawUndoList ;
do {
i + + ;
if ( i = = MAX_UNDO_HISTORY ) {
DeleteOldestUndo ( DrawUndoList ) ;
break ;
}
if ( nextUndo ) nextUndo = nextUndo - > Next ;
} while ( nextUndo ) ;
hUndoBitmap = CreateCompatibleBitmap ( hDc , width , height ) ;
if ( ! hUndoBitmap & & * DrawUndoList ) {
// delete the oldest and try again
DeleteOldestUndo ( DrawUndoList ) ;
hUndoBitmap = CreateCompatibleBitmap ( hDc , width , height ) ;
}
if ( hUndoBitmap ) {
newUndo = static_cast < P_DRAW_UNDO > ( malloc ( sizeof ( DRAW_UNDO ) ) ) ;
if ( newUndo ! = NULL )
{
newUndo - > hDc = CreateCompatibleDC ( hDc ) ;
newUndo - > hBitmap = hUndoBitmap ;
SelectObject ( newUndo - > hDc , newUndo - > hBitmap ) ;
BitBlt ( newUndo - > hDc , 0 , 0 , width , height , hDc , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
newUndo - > Next = * DrawUndoList ;
* DrawUndoList = newUndo ;
}
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// DeleteTypedText
//
//----------------------------------------------------------------------------
void DeleteTypedText ( P_TYPED_KEY * TypedKeyList )
{
P_TYPED_KEY nextKey ;
while ( * TypedKeyList ) {
nextKey = ( * TypedKeyList ) - > Next ;
free ( * TypedKeyList ) ;
* TypedKeyList = nextKey ;
}
}
//----------------------------------------------------------------------------
//
// BlankScreenArea
//
//----------------------------------------------------------------------------
void BlankScreenArea ( HDC hDc , PRECT Rc , int BlankMode )
{
if ( BlankMode = = ' K ' ) {
HBRUSH hBrush = CreateSolidBrush ( RGB ( 0 , 0 , 0 ) ) ;
FillRect ( hDc , Rc , hBrush ) ;
DeleteObject ( static_cast < HGDIOBJ > ( hBrush ) ) ;
} else {
FillRect ( hDc , Rc , GetSysColorBrush ( COLOR_WINDOW ) ) ;
}
}
//----------------------------------------------------------------------------
//
// ClearTypingCursor
//
//----------------------------------------------------------------------------
void ClearTypingCursor ( HDC hdcScreenCompat , HDC hdcScreenCursorCompat , RECT rc ,
int BlankMode )
{
if ( false ) { // BlankMode ) {
BlankScreenArea ( hdcScreenCompat , & rc , BlankMode ) ;
} else {
2025-11-10 11:57:13 -08:00
BitBlt ( hdcScreenCompat , rc . left , rc . top , rc . right - rc . left ,
rc . bottom - rc . top , hdcScreenCursorCompat , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
2025-01-16 20:52:24 +00:00
}
}
//----------------------------------------------------------------------------
//
// DrawTypingCursor
//
//----------------------------------------------------------------------------
void DrawTypingCursor ( HWND hWnd , POINT * textPt , HDC hdcScreenCompat ,
HDC hdcScreenCursorCompat , RECT * rc , bool centerUnderSystemCursor = false )
{
// Draw the typing cursor
rc - > left = textPt - > x ;
rc - > top = textPt - > y ;
TCHAR vKey = ' | ' ;
DrawText ( hdcScreenCompat , static_cast < PTCHAR > ( & vKey ) , 1 , rc , DT_CALCRECT ) ;
// LiveDraw uses a layered window which means mouse messages pass through
// to lower windows unless the system cursor is above a painted area.
// Centering the typing cursor directly under the system cursor allows
// us to capture the mouse wheel input required to change font size.
if ( centerUnderSystemCursor )
{
const LONG halfWidth = static_cast < LONG > ( ( rc - > right - rc - > left ) / 2 ) ;
const LONG halfHeight = static_cast < LONG > ( ( rc - > bottom - rc - > top ) / 2 ) ;
rc - > left - = halfWidth ;
rc - > right - = halfWidth ;
rc - > top - = halfHeight ;
rc - > bottom - = halfHeight ;
textPt - > x = rc - > left ;
textPt - > y = rc - > top ;
}
BitBlt ( hdcScreenCursorCompat , 0 , 0 , rc - > right - rc - > left , rc - > bottom - rc - > top ,
hdcScreenCompat , rc - > left , rc - > top , SRCCOPY | CAPTUREBLT ) ;
DrawText ( hdcScreenCompat , static_cast < PTCHAR > ( & vKey ) , 1 , rc , DT_LEFT ) ;
InvalidateRect ( hWnd , NULL , TRUE ) ;
}
//----------------------------------------------------------------------------
//
// BoundMouse
//
//----------------------------------------------------------------------------
RECT BoundMouse ( float zoomLevel , MONITORINFO * monInfo , int width , int height ,
POINT * cursorPos )
{
RECT rc ;
int x , y ;
GetZoomedTopLeftCoordinates ( zoomLevel , cursorPos , & x , width , & y , height ) ;
2025-11-10 11:57:13 -08:00
rc . left = monInfo - > rcMonitor . left + x ;
2025-01-16 20:52:24 +00:00
rc . right = rc . left + static_cast < int > ( width / zoomLevel ) ;
rc . top = monInfo - > rcMonitor . top + y ;
rc . bottom = rc . top + static_cast < int > ( height / zoomLevel ) ;
OutputDebug ( L " x: %d y: %d width: %d height: %d zoomLevel: %g \n " ,
cursorPos - > x , cursorPos - > y , width , height , zoomLevel ) ;
OutputDebug ( L " left: %d top: %d right: %d bottom: %d \n " ,
rc . left , rc . top , rc . right , rc . bottom ) ;
OutputDebug ( L " mon.left: %d mon.top: %d mon.right: %d mon.bottom: %d \n " ,
monInfo - > rcMonitor . left , monInfo - > rcMonitor . top , monInfo - > rcMonitor . right , monInfo - > rcMonitor . bottom ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
ClipCursor ( & rc ) ;
return rc ;
}
//----------------------------------------------------------------------------
//
// DrawArrow
//
//----------------------------------------------------------------------------
void DrawArrow ( HDC hdc , int x1 , int y1 , int x2 , int y2 , double length , double width ,
bool UseGdiplus )
{
// get normalized dx/dy
double dx = static_cast < double > ( x2 ) - x1 ;
double dy = static_cast < double > ( y2 ) - y1 ;
double bodyLen = sqrt ( dx * dx + dy * dy ) ;
if ( bodyLen ) {
dx / = bodyLen ;
dy / = bodyLen ;
} else {
dx = 1 ;
dy = 0 ;
}
// get midpoint of base
int xMid = x2 - static_cast < int > ( length * dx + 0.5 ) ;
int yMid = y2 - static_cast < int > ( length * dy + 0.5 ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// get left wing
int xLeft = xMid - static_cast < int > ( dy * width + 0.5 ) ;
int yLeft = yMid + static_cast < int > ( dx * width + 0.5 ) ;
// get right wing
int xRight = xMid + static_cast < int > ( dy * width + 0.5 ) ;
int yRight = yMid - static_cast < int > ( dx * width + 0.5 ) ;
// Bring in midpoint to make a nicer arrow
xMid = x2 - static_cast < int > ( length / 2 * dx + 0.5 ) ;
yMid = y2 - static_cast < int > ( length / 2 * dy + 0.5 ) ;
if ( UseGdiplus ) {
Gdiplus : : Graphics dstGraphics ( hdc ) ;
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : Pen pen ( color , static_cast < Gdiplus : : REAL > ( g_PenWidth ) ) ;
pen . SetLineCap ( Gdiplus : : LineCapRound , Gdiplus : : LineCapRound , Gdiplus : : DashCapRound ) ;
#if 0
Gdiplus : : PointF pts [ ] = {
{ ( Gdiplus : : REAL ) x1 , ( Gdiplus : : REAL ) y1 } ,
{ ( Gdiplus : : REAL ) xMid , ( Gdiplus : : REAL ) yMid } ,
{ ( Gdiplus : : REAL ) xLeft , ( Gdiplus : : REAL ) yLeft } ,
{ ( Gdiplus : : REAL ) x2 , ( Gdiplus : : REAL ) y2 } ,
{ ( Gdiplus : : REAL ) xRight , ( Gdiplus : : REAL ) yRight } ,
{ ( Gdiplus : : REAL ) xMid , ( Gdiplus : : REAL ) yMid }
} ;
dstGraphics . DrawPolygon ( & pen , pts , _countof ( pts ) ) ;
# else
Gdiplus : : GraphicsPath path ;
path . StartFigure ( ) ;
path . AddLine ( static_cast < INT > ( x1 ) , static_cast < INT > ( y1 ) , static_cast < INT > ( x2 ) , static_cast < INT > ( y2 ) ) ;
path . AddLine ( static_cast < INT > ( x2 ) , static_cast < INT > ( y2 ) , static_cast < INT > ( xMid ) , static_cast < INT > ( yMid ) ) ;
path . AddLine ( static_cast < INT > ( xMid ) , static_cast < INT > ( yMid ) , static_cast < INT > ( xLeft ) , static_cast < INT > ( yLeft ) ) ;
path . AddLine ( static_cast < INT > ( xLeft ) , static_cast < INT > ( yLeft ) , static_cast < INT > ( x2 ) , static_cast < INT > ( y2 ) ) ;
path . AddLine ( static_cast < INT > ( x2 ) , static_cast < INT > ( y2 ) , static_cast < INT > ( xRight ) , static_cast < INT > ( yRight ) ) ;
path . AddLine ( static_cast < INT > ( xRight ) , static_cast < INT > ( yRight ) , static_cast < INT > ( xMid ) , static_cast < INT > ( yMid ) ) ;
pen . SetLineJoin ( Gdiplus : : LineJoinRound ) ;
dstGraphics . DrawPath ( & pen , & path ) ;
# endif
}
else {
POINT pts [ ] = {
x1 , y1 ,
xMid , yMid ,
xLeft , yLeft ,
x2 , y2 ,
xRight , yRight ,
xMid , yMid
} ;
// draw arrow head filled with current color
HBRUSH hBrush = CreateSolidBrush ( g_PenColor ) ;
HBRUSH hOldBrush = SelectBrush ( hdc , hBrush ) ;
Polygon ( hdc , pts , sizeof ( pts ) / sizeof ( pts [ 0 ] ) ) ;
DeleteObject ( hBrush ) ;
SelectObject ( hdc , hOldBrush ) ;
}
}
//----------------------------------------------------------------------------
//
// DrawShape
//
//----------------------------------------------------------------------------
VOID DrawShape ( DWORD Shape , HDC hDc , RECT * Rect , bool UseGdiPlus = false )
{
bool isBlur = false ;
Gdiplus : : Graphics dstGraphics ( hDc ) ;
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : Pen pen ( color , static_cast < Gdiplus : : REAL > ( g_PenWidth ) ) ;
pen . SetLineCap ( Gdiplus : : LineCapRound , Gdiplus : : LineCapRound , Gdiplus : : DashCapRound ) ;
// Check for highlighting or blur
Gdiplus : : Brush * pBrush = NULL ;
if ( PEN_COLOR_HIGHLIGHT ( g_PenColor ) ) {
// Use half the alpha for higher contrast
DWORD newColor = g_PenColor & 0xFFFFFF | ( ( g_AlphaBlend / 2 ) < < 24 ) ;
pBrush = new Gdiplus : : SolidBrush ( ColorFromColorRef ( newColor ) ) ;
if ( UseGdiPlus & & Shape ! = DRAW_LINE & & Shape ! = DRAW_ARROW )
InflateRect ( Rect , g_PenWidth / 2 , g_PenWidth / 2 ) ;
}
else if ( ( g_PenColor & 0xFFFFFF ) = = COLOR_BLUR ) {
if ( UseGdiPlus & & Shape ! = DRAW_LINE & & Shape ! = DRAW_ARROW )
InflateRect ( Rect , g_PenWidth / 2 , g_PenWidth / 2 ) ;
isBlur = true ;
}
2025-10-07 11:20:00 -07:00
OutputDebug ( L " Draw shape: highlight: %d pbrush: %d \n " , PEN_COLOR_HIGHLIGHT ( g_PenColor ) , pBrush ! = NULL ) ;
2025-01-16 20:52:24 +00:00
switch ( Shape ) {
case DRAW_RECTANGLE :
if ( UseGdiPlus )
if ( pBrush )
2025-11-10 11:57:13 -08:00
DrawHighlightedShape ( DRAW_RECTANGLE , hDc , pBrush , NULL ,
2025-01-16 20:52:24 +00:00
static_cast < int > ( Rect - > left - 1 ) , static_cast < int > ( Rect - > top - 1 ) ,
static_cast < int > ( Rect - > right ) , static_cast < int > ( Rect - > bottom ) ) ;
else if ( isBlur )
DrawBlurredShape ( DRAW_RECTANGLE , & pen , hDc , & dstGraphics ,
static_cast < int > ( Rect - > left - 1 ) , static_cast < int > ( Rect - > top - 1 ) ,
static_cast < int > ( Rect - > right ) , static_cast < int > ( Rect - > bottom ) ) ;
else
dstGraphics . DrawRectangle ( & pen ,
Gdiplus : : Rect : : Rect ( Rect - > left - 1 , Rect - > top - 1 ,
Rect - > right - Rect - > left , Rect - > bottom - Rect - > top ) ) ;
else
Rectangle ( hDc , Rect - > left , Rect - > top ,
Rect - > right , Rect - > bottom ) ;
break ;
case DRAW_ELLIPSE :
if ( UseGdiPlus )
if ( pBrush )
DrawHighlightedShape ( DRAW_ELLIPSE , hDc , pBrush , NULL ,
static_cast < int > ( Rect - > left - 1 ) , static_cast < int > ( Rect - > top - 1 ) ,
static_cast < int > ( Rect - > right ) , static_cast < int > ( Rect - > bottom ) ) ;
else if ( isBlur )
DrawBlurredShape ( DRAW_ELLIPSE , & pen , hDc , & dstGraphics ,
static_cast < int > ( Rect - > left - 1 ) , static_cast < int > ( Rect - > top - 1 ) ,
static_cast < int > ( Rect - > right ) , static_cast < int > ( Rect - > bottom ) ) ;
else
dstGraphics . DrawEllipse ( & pen ,
Gdiplus : : Rect : : Rect ( Rect - > left - 1 , Rect - > top - 1 ,
Rect - > right - Rect - > left , Rect - > bottom - Rect - > top ) ) ;
else
Ellipse ( hDc , Rect - > left , Rect - > top ,
Rect - > right , Rect - > bottom ) ;
break ;
case DRAW_LINE :
if ( UseGdiPlus )
if ( pBrush )
DrawHighlightedShape ( DRAW_LINE , hDc , NULL , & pen ,
static_cast < int > ( Rect - > left ) , static_cast < int > ( Rect - > top ) ,
static_cast < int > ( Rect - > right ) , static_cast < int > ( Rect - > bottom ) ) ;
else if ( isBlur )
DrawBlurredShape ( DRAW_LINE , & pen , hDc , & dstGraphics ,
static_cast < int > ( Rect - > left ) , static_cast < int > ( Rect - > top ) ,
static_cast < int > ( Rect - > right ) , static_cast < int > ( Rect - > bottom ) ) ;
else
dstGraphics . DrawLine ( & pen ,
static_cast < INT > ( Rect - > left - 1 ) , static_cast < INT > ( Rect - > top - 1 ) ,
static_cast < INT > ( Rect - > right ) , static_cast < INT > ( Rect - > bottom ) ) ;
else {
MoveToEx ( hDc , Rect - > left , Rect - > top , NULL ) ;
LineTo ( hDc , Rect - > right + 1 , Rect - > bottom + 1 ) ;
}
break ;
case DRAW_ARROW :
DrawArrow ( hDc , Rect - > right + 1 , Rect - > bottom + 1 ,
Rect - > left , Rect - > top ,
static_cast < double > ( g_PenWidth ) * 2.5 , static_cast < double > ( g_PenWidth ) * 1.5 , UseGdiPlus ) ;
break ;
}
if ( pBrush ) delete pBrush ;
}
//----------------------------------------------------------------------------
//
// SendPenMessage
//
// Inserts the pen message marker.
//
//----------------------------------------------------------------------------
VOID SendPenMessage ( HWND hWnd , UINT Message , LPARAM lParam )
{
WPARAM wParam = 0 ;
//
// Get key states
//
if ( GetKeyState ( VK_LCONTROL ) < 0 ) {
wParam | = MK_CONTROL ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
if ( GetKeyState ( VK_LSHIFT ) < 0 | | GetKeyState ( VK_RSHIFT ) < 0 ) {
wParam | = MK_SHIFT ;
}
SetMessageExtraInfo ( static_cast < LPARAM > ( MI_WP_SIGNATURE ) ) ;
SendMessage ( hWnd , Message , wParam , lParam ) ;
}
//----------------------------------------------------------------------------
//
// ScalePenPosition
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
// Maps pen input to mouse input coordinates based on zoom level. Returns
// 0 if pen is active but we didn't send this message to ourselves (pen
2025-11-10 11:57:13 -08:00
// signature will be missing).
2025-01-16 20:52:24 +00:00
//
//----------------------------------------------------------------------------
LPARAM ScalePenPosition ( float zoomLevel , MONITORINFO * monInfo , RECT boundRc ,
UINT message , LPARAM lParam )
{
RECT rc ;
WORD x , y ;
LPARAM extraInfo ;
extraInfo = GetMessageExtraInfo ( ) ;
2025-11-10 11:57:13 -08:00
if ( g_PenDown ) {
2025-01-16 20:52:24 +00:00
// ignore messages we didn't tag as pen
if ( extraInfo = = MI_WP_SIGNATURE ) {
OutputDebug ( L " Tablet Pen message \n " ) ;
// tablet input: don't bound the cursor
ClipCursor ( NULL ) ;
x = LOWORD ( lParam ) ;
y = HIWORD ( lParam ) ;
x = static_cast < WORD > ( ( x - static_cast < WORD > ( monInfo - > rcMonitor . left ) ) / zoomLevel ) + static_cast < WORD > ( boundRc . left - monInfo - > rcMonitor . left ) ;
y = static_cast < WORD > ( ( y - static_cast < WORD > ( monInfo - > rcMonitor . top ) ) / zoomLevel ) + static_cast < WORD > ( boundRc . top - monInfo - > rcMonitor . top ) ;
lParam = MAKELPARAM ( x , y ) ;
}
else {
OutputDebug ( L " Ignore pen message we didn't send \n " ) ;
lParam = 0 ;
}
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else {
if ( ! GetClipCursor ( & rc ) ) {
ClipCursor ( & boundRc ) ;
}
OutputDebug ( L " Mouse message \n " ) ;
}
return lParam ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
//
// DrawHighlightedCursor
//
//----------------------------------------------------------------------------
BOOLEAN DrawHighlightedCursor ( float ZoomLevel , int Width , int Height )
{
DWORD zoomWidth = static_cast < DWORD > ( static_cast < float > ( Width ) / ZoomLevel ) ;
DWORD zoomHeight = static_cast < DWORD > ( static_cast < float > ( Height ) / ZoomLevel ) ;
if ( g_PenWidth < 5 & & zoomWidth > g_PenWidth * 100 & & zoomHeight > g_PenWidth * 100 ) {
return TRUE ;
} else {
return FALSE ;
}
}
//----------------------------------------------------------------------------
//
// InvalidateCursorMoveArea
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
void InvalidateCursorMoveArea ( HWND hWnd , float zoomLevel , int width , int height ,
2025-01-16 20:52:24 +00:00
POINT currentPt , POINT prevPt , POINT cursorPos )
{
int x , y ;
RECT rc ;
2025-10-07 11:20:00 -07:00
int invWidth = g_PenWidth + CURSOR_SAVE_MARGIN ;
2025-01-16 20:52:24 +00:00
if ( DrawHighlightedCursor ( zoomLevel , width , height ) ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
invWidth = g_PenWidth * 3 + 1 ;
}
GetZoomedTopLeftCoordinates ( zoomLevel , & cursorPos , & x , width , & y , height ) ;
rc . left = static_cast < int > ( max ( 0 , ( int ) ( ( min ( prevPt . x , currentPt . x ) - invWidth - x ) * zoomLevel ) ) ) ;
rc . right = static_cast < int > ( ( max ( prevPt . x , currentPt . x ) + invWidth - x ) * zoomLevel ) ;
rc . top = static_cast < int > ( max ( 0 , ( int ) ( ( min ( prevPt . y , currentPt . y ) - invWidth - y ) * zoomLevel ) ) ) ;
rc . bottom = static_cast < int > ( ( max ( prevPt . y , currentPt . y ) + invWidth - y ) * zoomLevel ) ;
InvalidateRect ( hWnd , & rc , FALSE ) ;
OutputDebug ( L " INVALIDATE: (%d, %d) - (%d, %d) \n " , rc . left , rc . top , rc . right , rc . bottom ) ;
}
//----------------------------------------------------------------------------
//
// SavCursorArea
//
//----------------------------------------------------------------------------
void SaveCursorArea ( HDC hDcTarget , HDC hDcSource , POINT pt )
{
OutputDebug ( L " SaveCursorArea \n " ) ;
2025-10-07 11:20:00 -07:00
int penWidth = g_PenWidth + CURSOR_SAVE_MARGIN ;
2025-01-16 20:52:24 +00:00
BitBlt ( hDcTarget , 0 , 0 , penWidth + CURSOR_ARM_LENGTH * 2 , penWidth + CURSOR_ARM_LENGTH * 2 ,
hDcSource , static_cast < INT > ( pt . x - penWidth / 2 ) - CURSOR_ARM_LENGTH ,
static_cast < INT > ( pt . y - penWidth / 2 ) - CURSOR_ARM_LENGTH , SRCCOPY | CAPTUREBLT ) ;
}
//----------------------------------------------------------------------------
//
// RestoreCursorArea
//
//----------------------------------------------------------------------------
void RestoreCursorArea ( HDC hDcTarget , HDC hDcSource , POINT pt )
{
OutputDebug ( L " RestoreCursorArea \n " ) ;
2025-10-07 11:20:00 -07:00
int penWidth = g_PenWidth + CURSOR_SAVE_MARGIN ;
2025-01-16 20:52:24 +00:00
BitBlt ( hDcTarget , static_cast < INT > ( pt . x - penWidth / 2 ) - CURSOR_ARM_LENGTH ,
static_cast < INT > ( pt . y - penWidth / 2 ) - CURSOR_ARM_LENGTH , penWidth + CURSOR_ARM_LENGTH * 2 ,
penWidth + CURSOR_ARM_LENGTH * 2 , hDcSource , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
}
//----------------------------------------------------------------------------
//
// DrawCursor
//
//----------------------------------------------------------------------------
void DrawCursor ( HDC hDcTarget , POINT pt , float ZoomLevel , int Width , int Height )
{
RECT rc ;
if ( g_DrawPointer ) {
Gdiplus : : Graphics dstGraphics ( hDcTarget ) ;
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : Pen pen ( color , static_cast < Gdiplus : : REAL > ( g_PenWidth ) ) ;
rc . left = pt . x - CURSOR_ARM_LENGTH ;
rc . right = pt . x + CURSOR_ARM_LENGTH ;
rc . top = pt . y - CURSOR_ARM_LENGTH ;
rc . bottom = pt . y + CURSOR_ARM_LENGTH ;
Gdiplus : : GraphicsPath path ;
path . StartFigure ( ) ;
path . AddLine ( static_cast < INT > ( rc . left ) - 1 , static_cast < INT > ( rc . top ) - 1 , static_cast < INT > ( rc . right ) , static_cast < INT > ( rc . bottom ) ) ;
path . AddLine ( static_cast < INT > ( rc . left ) - 2 , static_cast < INT > ( rc . top ) - 1 , rc . left + ( rc . right - rc . left ) / 2 , rc . top - 1 ) ;
path . AddLine ( static_cast < INT > ( rc . left ) - 1 , static_cast < INT > ( rc . top ) - 2 , rc . left - 1 , rc . top + ( rc . bottom - rc . top ) / 2 ) ;
path . AddLine ( static_cast < INT > ( rc . left ) - 1 , static_cast < INT > ( rc . top ) - 2 , rc . left - 1 , rc . top + ( rc . bottom - rc . top ) / 2 ) ;
path . AddLine ( static_cast < INT > ( rc . left + ( rc . right - rc . left ) / 2 ) , rc . top - 1 , rc . left - 1 , rc . top + ( rc . bottom - rc . top ) / 2 ) ;
pen . SetLineJoin ( Gdiplus : : LineJoinRound ) ;
dstGraphics . DrawPath ( & pen , & path ) ;
OutputDebug ( L " DrawPointer: %d %d %d %d \n " , rc . left , rc . top , rc . right , rc . bottom ) ;
} else if ( DrawHighlightedCursor ( ZoomLevel , Width , Height ) ) {
OutputDebug ( L " DrawHighlightedCursor: %d %d %d %d \n " , pt . x , pt . y , g_PenWidth , g_PenWidth ) ;
Gdiplus : : Graphics dstGraphics ( hDcTarget ) ;
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : Pen pen ( color , static_cast < Gdiplus : : REAL > ( g_PenWidth ) ) ;
Gdiplus : : GraphicsPath path ;
path . StartFigure ( ) ;
pen . SetLineJoin ( Gdiplus : : LineJoinRound ) ;
path . AddLine ( static_cast < INT > ( pt . x - CURSOR_ARM_LENGTH ) , pt . y , pt . x + CURSOR_ARM_LENGTH , pt . y ) ;
path . CloseFigure ( ) ;
path . StartFigure ( ) ;
pen . SetLineJoin ( Gdiplus : : LineJoinRound ) ;
path . AddLine ( static_cast < INT > ( pt . x ) , pt . y - CURSOR_ARM_LENGTH , pt . x , pt . y + CURSOR_ARM_LENGTH ) ;
path . CloseFigure ( ) ;
dstGraphics . DrawPath ( & pen , & path ) ;
} else {
Gdiplus : : Graphics dstGraphics ( hDcTarget ) ;
{
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : SolidBrush solidBrush ( color ) ;
dstGraphics . FillEllipse ( & solidBrush , static_cast < INT > ( pt . x - g_PenWidth / 2 ) , static_cast < INT > ( pt . y - g_PenWidth / 2 ) ,
static_cast < INT > ( g_PenWidth ) , static_cast < INT > ( g_PenWidth ) ) ;
}
}
}
//----------------------------------------------------------------------------
//
// ResizePen
//
//----------------------------------------------------------------------------
void ResizePen ( HWND hWnd , HDC hdcScreenCompat , HDC hdcScreenCursorCompat , POINT prevPt ,
2025-11-10 11:57:13 -08:00
BOOLEAN g_Tracing , BOOLEAN * g_Drawing , float g_LiveZoomLevel ,
2025-01-16 20:52:24 +00:00
BOOLEAN isUser , int newWidth )
{
if ( ! g_Tracing ) {
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
}
OutputDebug ( L " RESIZE_PEN-PRE: penWidth: %d " , g_PenWidth ) ;
int prevWidth = g_PenWidth ;
if ( g_ZoomOnLiveZoom )
{
if ( isUser )
{
// Amplify user delta proportional to LiveZoomLevel
newWidth = g_PenWidth + static_cast < int > ( ( newWidth - static_cast < int > ( g_PenWidth ) ) * g_LiveZoomLevel ) ;
}
g_PenWidth = min ( max ( newWidth , MIN_PEN_WIDTH ) ,
min ( static_cast < int > ( MAX_PEN_WIDTH * g_LiveZoomLevel ) , MAX_LIVE_PEN_WIDTH ) ) ;
g_RootPenWidth = static_cast < int > ( g_PenWidth / g_LiveZoomLevel ) ;
}
else
{
g_PenWidth = min ( max ( newWidth , MIN_PEN_WIDTH ) , MAX_PEN_WIDTH ) ;
g_RootPenWidth = g_PenWidth ;
}
if ( prevWidth = = static_cast < int > ( g_PenWidth ) ) {
// No change
return ;
}
OutputDebug ( L " newWidth: %d \n RESIZE_PEN-POST: penWidth: %d \n " , newWidth , g_PenWidth ) ;
reg . WriteRegSettings ( RegSettings ) ;
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
* g_Drawing = FALSE ;
EnableDisableStickyKeys ( TRUE ) ;
SendMessage ( hWnd , WM_LBUTTONDOWN , - 1 , MAKELPARAM ( prevPt . x , prevPt . y ) ) ;
}
//----------------------------------------------------------------------------
//
// IsPenInverted
//
//----------------------------------------------------------------------------
bool IsPenInverted ( WPARAM wParam )
{
POINTER_INPUT_TYPE pointerType ;
POINTER_PEN_INFO penInfo ;
return
pGetPointerType ( GET_POINTERID_WPARAM ( wParam ) , & pointerType ) & & ( pointerType = = PT_PEN ) & &
pGetPointerPenInfo ( GET_POINTERID_WPARAM ( wParam ) , & penInfo ) & & ( penInfo . penFlags & PEN_FLAG_INVERTED ) ;
}
//----------------------------------------------------------------------------
//
// CaptureScreenshotAsync
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
// Captures the specified screen using the capture APIs
//
//----------------------------------------------------------------------------
2026-02-11 22:31:46 +08:00
wil : : task < winrt : : com_ptr < ID3D11Texture2D > > CaptureScreenshotAsync ( winrt : : IDirect3DDevice const & device , winrt : : GraphicsCaptureItem const & item , winrt : : DirectXPixelFormat const & pixelFormat )
2025-01-16 20:52:24 +00:00
{
auto d3dDevice = GetDXGIInterfaceFromObject < ID3D11Device > ( device ) ;
winrt : : com_ptr < ID3D11DeviceContext > d3dContext ;
d3dDevice - > GetImmediateContext ( d3dContext . put ( ) ) ;
2025-11-10 11:57:13 -08:00
// Creating our frame pool with CreateFreeThreaded means that we
2025-01-16 20:52:24 +00:00
// will be called back from the frame pool's internal worker thread
// instead of the thread we are currently on. It also disables the
// DispatcherQueue requirement.
auto framePool = winrt : : Direct3D11CaptureFramePool : : CreateFreeThreaded (
device ,
pixelFormat ,
1 ,
item . Size ( ) ) ;
auto session = framePool . CreateCaptureSession ( item ) ;
wil : : shared_event captureEvent ( wil : : EventOptions : : ManualReset ) ;
winrt : : Direct3D11CaptureFrame frame { nullptr } ;
framePool . FrameArrived ( [ & frame , captureEvent ] ( auto & framePool , auto & )
{
frame = framePool . TryGetNextFrame ( ) ;
// Complete the operation
captureEvent . SetEvent ( ) ;
} ) ;
session . IsCursorCaptureEnabled ( false ) ;
session . StartCapture ( ) ;
co_await winrt : : resume_on_signal ( captureEvent . get ( ) ) ;
// End the capture
session . Close ( ) ;
framePool . Close ( ) ;
auto texture = GetDXGIInterfaceFromObject < ID3D11Texture2D > ( frame . Surface ( ) ) ;
2026-02-11 22:31:46 +08:00
co_return util : : CopyD3DTexture ( d3dDevice , texture , true ) ;
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// CaptureScreenshot
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
// Captures the specified screen using the capture APIs
//
//----------------------------------------------------------------------------
winrt : : com_ptr < ID3D11Texture2D > CaptureScreenshot ( winrt : : DirectXPixelFormat const & pixelFormat )
{
auto d3dDevice = util : : CreateD3D11Device ( ) ;
auto dxgiDevice = d3dDevice . as < IDXGIDevice > ( ) ;
auto device = CreateDirect3DDevice ( dxgiDevice . get ( ) ) ;
// Get the active MONITOR capture device
HMONITOR hMon = NULL ;
POINT cursorPos = { 0 , 0 } ;
if ( pMonitorFromPoint ) {
GetCursorPos ( & cursorPos ) ;
hMon = pMonitorFromPoint ( cursorPos , MONITOR_DEFAULTTONEAREST ) ;
}
auto item = util : : CreateCaptureItemForMonitor ( hMon ) ;
2026-02-11 22:31:46 +08:00
return CaptureScreenshotAsync ( device , item , pixelFormat ) . get ( ) ;
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// CopyD3DTexture
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
inline auto CopyD3DTexture ( winrt : : com_ptr < ID3D11Device > const & device ,
2025-01-16 20:52:24 +00:00
winrt : : com_ptr < ID3D11Texture2D > const & texture , bool asStagingTexture )
{
winrt : : com_ptr < ID3D11DeviceContext > context ;
device - > GetImmediateContext ( context . put ( ) ) ;
D3D11_TEXTURE2D_DESC desc = { } ;
texture - > GetDesc ( & desc ) ;
// Clear flags that we don't need
desc . Usage = asStagingTexture ? D3D11_USAGE_STAGING : D3D11_USAGE_DEFAULT ;
desc . BindFlags = asStagingTexture ? 0 : D3D11_BIND_SHADER_RESOURCE ;
desc . CPUAccessFlags = asStagingTexture ? D3D11_CPU_ACCESS_READ : 0 ;
desc . MiscFlags = 0 ;
// Create and fill the texture copy
winrt : : com_ptr < ID3D11Texture2D > textureCopy ;
winrt : : check_hresult ( device - > CreateTexture2D ( & desc , nullptr , textureCopy . put ( ) ) ) ;
context - > CopyResource ( textureCopy . get ( ) , texture . get ( ) ) ;
return textureCopy ;
}
//----------------------------------------------------------------------------
//
// PrepareStagingTexture
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
inline auto PrepareStagingTexture ( winrt : : com_ptr < ID3D11Device > const & device ,
2025-01-16 20:52:24 +00:00
winrt : : com_ptr < ID3D11Texture2D > const & texture )
{
// If our texture is already set up for staging, then use it.
D3D11_TEXTURE2D_DESC desc = { } ;
texture - > GetDesc ( & desc ) ;
if ( desc . Usage = = D3D11_USAGE_STAGING & & desc . CPUAccessFlags & D3D11_CPU_ACCESS_READ )
{
return texture ;
}
return CopyD3DTexture ( device , texture , true ) ;
}
//----------------------------------------------------------------------------
//
// GetBytesPerPixel
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
inline size_t
GetBytesPerPixel ( DXGI_FORMAT pixelFormat )
{
switch ( pixelFormat )
{
case DXGI_FORMAT_R32G32B32A32_TYPELESS :
case DXGI_FORMAT_R32G32B32A32_FLOAT :
case DXGI_FORMAT_R32G32B32A32_UINT :
case DXGI_FORMAT_R32G32B32A32_SINT :
return 16 ;
case DXGI_FORMAT_R32G32B32_TYPELESS :
case DXGI_FORMAT_R32G32B32_FLOAT :
case DXGI_FORMAT_R32G32B32_UINT :
case DXGI_FORMAT_R32G32B32_SINT :
return 12 ;
case DXGI_FORMAT_R16G16B16A16_TYPELESS :
case DXGI_FORMAT_R16G16B16A16_FLOAT :
case DXGI_FORMAT_R16G16B16A16_UNORM :
case DXGI_FORMAT_R16G16B16A16_UINT :
case DXGI_FORMAT_R16G16B16A16_SNORM :
case DXGI_FORMAT_R16G16B16A16_SINT :
case DXGI_FORMAT_R32G32_TYPELESS :
case DXGI_FORMAT_R32G32_FLOAT :
case DXGI_FORMAT_R32G32_UINT :
case DXGI_FORMAT_R32G32_SINT :
case DXGI_FORMAT_R32G8X24_TYPELESS :
return 8 ;
case DXGI_FORMAT_D32_FLOAT_S8X24_UINT :
case DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS :
case DXGI_FORMAT_X32_TYPELESS_G8X24_UINT :
case DXGI_FORMAT_R10G10B10A2_TYPELESS :
case DXGI_FORMAT_R10G10B10A2_UNORM :
case DXGI_FORMAT_R10G10B10A2_UINT :
case DXGI_FORMAT_R11G11B10_FLOAT :
case DXGI_FORMAT_R8G8B8A8_TYPELESS :
case DXGI_FORMAT_R8G8B8A8_UNORM :
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB :
case DXGI_FORMAT_R8G8B8A8_UINT :
case DXGI_FORMAT_R8G8B8A8_SNORM :
case DXGI_FORMAT_R8G8B8A8_SINT :
case DXGI_FORMAT_R16G16_TYPELESS :
case DXGI_FORMAT_R16G16_FLOAT :
case DXGI_FORMAT_UNKNOWN :
case DXGI_FORMAT_R16G16_UINT :
case DXGI_FORMAT_R16G16_SNORM :
case DXGI_FORMAT_R16G16_SINT :
case DXGI_FORMAT_R32_TYPELESS :
case DXGI_FORMAT_D32_FLOAT :
case DXGI_FORMAT_R32_FLOAT :
case DXGI_FORMAT_R32_UINT :
case DXGI_FORMAT_R32_SINT :
case DXGI_FORMAT_R24G8_TYPELESS :
case DXGI_FORMAT_D24_UNORM_S8_UINT :
case DXGI_FORMAT_R24_UNORM_X8_TYPELESS :
case DXGI_FORMAT_X24_TYPELESS_G8_UINT :
case DXGI_FORMAT_R8G8_B8G8_UNORM :
case DXGI_FORMAT_G8R8_G8B8_UNORM :
case DXGI_FORMAT_B8G8R8A8_UNORM :
case DXGI_FORMAT_B8G8R8X8_UNORM :
case DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM :
case DXGI_FORMAT_B8G8R8A8_TYPELESS :
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB :
case DXGI_FORMAT_B8G8R8X8_TYPELESS :
case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB :
return 4 ;
case DXGI_FORMAT_R8G8_TYPELESS :
case DXGI_FORMAT_R8G8_UNORM :
case DXGI_FORMAT_R8G8_UINT :
case DXGI_FORMAT_R8G8_SNORM :
case DXGI_FORMAT_R8G8_SINT :
case DXGI_FORMAT_R16_TYPELESS :
case DXGI_FORMAT_R16_FLOAT :
case DXGI_FORMAT_D16_UNORM :
case DXGI_FORMAT_R16_UNORM :
case DXGI_FORMAT_R16_UINT :
case DXGI_FORMAT_R16_SNORM :
case DXGI_FORMAT_R16_SINT :
case DXGI_FORMAT_B5G6R5_UNORM :
case DXGI_FORMAT_B5G5R5A1_UNORM :
case DXGI_FORMAT_B4G4R4A4_UNORM :
return 2 ;
case DXGI_FORMAT_R8_TYPELESS :
case DXGI_FORMAT_R8_UNORM :
case DXGI_FORMAT_R8_UINT :
case DXGI_FORMAT_R8_SNORM :
case DXGI_FORMAT_R8_SINT :
case DXGI_FORMAT_A8_UNORM :
return 1 ;
default :
throw winrt : : hresult_invalid_argument ( L " Invalid pixel format! " ) ;
}
}
//----------------------------------------------------------------------------
//
// CopyBytesFromTexture
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
inline auto CopyBytesFromTexture ( winrt : : com_ptr < ID3D11Texture2D > const & texture , uint32_t subresource = 0 )
{
winrt : : com_ptr < ID3D11Device > device ;
texture - > GetDevice ( device . put ( ) ) ;
winrt : : com_ptr < ID3D11DeviceContext > context ;
device - > GetImmediateContext ( context . put ( ) ) ;
auto stagingTexture = PrepareStagingTexture ( device , texture ) ;
D3D11_TEXTURE2D_DESC desc = { } ;
stagingTexture - > GetDesc ( & desc ) ;
auto bytesPerPixel = GetBytesPerPixel ( desc . Format ) ;
// Copy the bits
D3D11_MAPPED_SUBRESOURCE mapped = { } ;
winrt : : check_hresult ( context - > Map ( stagingTexture . get ( ) , subresource , D3D11_MAP_READ , 0 , & mapped ) ) ;
auto bytesStride = static_cast < size_t > ( desc . Width ) * bytesPerPixel ;
std : : vector < byte > bytes ( bytesStride * static_cast < size_t > ( desc . Height ) , 0 ) ;
auto source = static_cast < byte * > ( mapped . pData ) ;
auto dest = bytes . data ( ) ;
for ( auto i = 0 ; i < static_cast < int > ( desc . Height ) ; i + + )
{
memcpy ( dest , source , bytesStride ) ;
source + = mapped . RowPitch ;
dest + = bytesStride ;
}
context - > Unmap ( stagingTexture . get ( ) , 0 ) ;
return bytes ;
}
//----------------------------------------------------------------------------
//
// StopRecording
//
//----------------------------------------------------------------------------
void StopRecording ( )
{
2026-02-03 13:05:31 -08:00
OutputDebugStringW ( L " [Recording] StopRecording called \n " ) ;
2025-01-16 20:52:24 +00:00
if ( g_RecordToggle = = TRUE ) {
2026-02-03 13:05:31 -08:00
OutputDebugStringW ( L " [Recording] g_RecordToggle was TRUE, stopping... \n " ) ;
2025-01-16 20:52:24 +00:00
g_SelectRectangle . Stop ( ) ;
if ( g_RecordingSession ! = nullptr ) {
2026-02-03 13:05:31 -08:00
OutputDebugStringW ( L " [Recording] Closing VideoRecordingSession \n " ) ;
2025-01-16 20:52:24 +00:00
g_RecordingSession - > Close ( ) ;
2026-02-03 13:05:31 -08:00
// NOTE: Do NOT null the session here - let the coroutine finish first
2025-01-16 20:52:24 +00:00
}
2025-11-10 11:57:13 -08:00
if ( g_GifRecordingSession ! = nullptr ) {
2026-02-03 13:05:31 -08:00
OutputDebugStringW ( L " [Recording] Closing GifRecordingSession \n " ) ;
2025-11-10 11:57:13 -08:00
g_GifRecordingSession - > Close ( ) ;
2026-02-03 13:05:31 -08:00
// NOTE: Do NOT null the session here - let the coroutine finish first
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
g_RecordToggle = FALSE ;
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( g_hWndLiveZoom ! = NULL & & g_LiveZoomLevelOne ) {
if ( IsWindowVisible ( g_hWndLiveZoom ) ) {
ShowWindow ( g_hWndLiveZoom , SW_HIDE ) ;
DestroyWindow ( g_hWndLiveZoom ) ;
g_LiveZoomLevelOne = false ;
}
}
# endif
}
}
//----------------------------------------------------------------------------
//
2026-02-03 13:05:31 -08:00
// GetUniqueFilename
2025-01-16 20:52:24 +00:00
//
2026-02-03 13:05:31 -08:00
// Returns a unique filename by checking for existing files and adding (1), (2), etc.
// suffixes as needed. Uses the folder from lastSavePath if available
2025-01-16 20:52:24 +00:00
//
//----------------------------------------------------------------------------
2026-03-26 13:21:43 +01:00
std : : wstring GetUniqueFilename ( const std : : wstring & lastSavePath , const wchar_t * defaultFilename , REFKNOWNFOLDERID defaultFolderId )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
// Get the folder where the file will be saved
std : : filesystem : : path saveFolder ;
if ( ! lastSavePath . empty ( ) )
2025-11-14 17:36:31 -08:00
{
2026-02-03 13:05:31 -08:00
// Use folder from last save location
saveFolder = std : : filesystem : : path ( lastSavePath ) . parent_path ( ) ;
2025-11-14 17:36:31 -08:00
}
2026-02-03 13:05:31 -08:00
if ( saveFolder . empty ( ) )
2025-11-14 17:36:31 -08:00
{
2026-02-03 13:05:31 -08:00
// Default to specified known folder
wil : : unique_cotaskmem_string folderPath ;
if ( SUCCEEDED ( SHGetKnownFolderPath ( defaultFolderId , KF_FLAG_DEFAULT , nullptr , folderPath . put ( ) ) ) )
{
saveFolder = folderPath . get ( ) ;
}
2025-11-14 17:36:31 -08:00
}
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// Build base name and extension
std : : filesystem : : path defaultPath = defaultFilename ;
auto base = defaultPath . stem ( ) . wstring ( ) ;
auto ext = defaultPath . extension ( ) . wstring ( ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// Check for existing files and find unique name
std : : wstring candidateName = base + ext ;
std : : filesystem : : path checkPath = saveFolder / candidateName ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
int index = 1 ;
std : : error_code ec ;
while ( std : : filesystem : : exists ( checkPath , ec ) )
{
candidateName = base + L " ( " + std : : to_wstring ( index ) + L " ) " + ext ;
checkPath = saveFolder / candidateName ;
index + + ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
return candidateName ;
}
//----------------------------------------------------------------------------
//
// GetUniqueRecordingFilename
//
// Gets a unique file name for recording saves, using the " (N)" suffix
// approach so that the user can hit OK without worrying about overwriting
// if they are making multiple recordings in one session or don't want to
// always see an overwrite dialog or stop to clean up files.
//
//----------------------------------------------------------------------------
auto GetUniqueRecordingFilename ( )
{
const wchar_t * defaultFile = ( g_RecordingFormat = = RecordingFormat : : GIF )
? DEFAULT_GIF_RECORDING_FILE
: DEFAULT_RECORDING_FILE ;
return GetUniqueFilename ( g_RecordingSaveLocation , defaultFile , FOLDERID_Videos ) ;
}
[ZoomIt] Add datetime suffix to screenshots (#43172)
This pull request introduces an improvement to how screenshot filenames
are generated in `ZoomIt`. The main change is to ensure that each
screenshot is saved with a unique, timestamped filename, which helps
prevent accidental overwrites and keeps files sorted chronologically.
**Enhancement to screenshot filename generation:**
* Added a new implementation for `GetUniqueScreenshotFilename()` in
`Zoomit.cpp` to generate screenshot filenames using the current date and
time, following the format `"ZoomIt YYYY-MM-DD HHMMSS.png"`. This
reduces the risk of overwriting existing files and improves file
organization.<!-- Enter a brief description/summary of your PR here.
What does it fix/what does it change/how was it tested (even manually,
if necessary)? -->
## Summary of the Pull Request
This PR:
Includes an update to create unique screenshot filenames, saving the
user from having to manually type a new filename each time they save a
new screenshot in the same location.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43158
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
## Unique filename issue
The PR fixes the user's complaint about the screenshots always being
called "zoomit" by adding a `GetUniqueScreenshotFilename()` method which
creates a unique filename based on the current date and time, e.g.
`ZoomIt 2025-11-01 004723.png`. This is consistent with other tools like
the Windows Snipping Tool and provides files which sort correctly when
ordered by name, which is not the case for files with simple numeric
suffixes.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Tested on a local build.
2026-02-24 04:53:22 +00:00
//----------------------------------------------------------------------------
//
// GetUniqueScreenshotFilename
//
// Gets a unique file name for screenshot saves, using the current date and
// time as a suffix. This reduces the chance that the user could overwrite an
// existing file if they are saving multiple captures in the same folder, and
// also ensures that ordering is correct when sorted by name.
//
//----------------------------------------------------------------------------
2026-02-03 13:05:31 -08:00
auto GetUniqueScreenshotFilename ( )
{
[ZoomIt] Add datetime suffix to screenshots (#43172)
This pull request introduces an improvement to how screenshot filenames
are generated in `ZoomIt`. The main change is to ensure that each
screenshot is saved with a unique, timestamped filename, which helps
prevent accidental overwrites and keeps files sorted chronologically.
**Enhancement to screenshot filename generation:**
* Added a new implementation for `GetUniqueScreenshotFilename()` in
`Zoomit.cpp` to generate screenshot filenames using the current date and
time, following the format `"ZoomIt YYYY-MM-DD HHMMSS.png"`. This
reduces the risk of overwriting existing files and improves file
organization.<!-- Enter a brief description/summary of your PR here.
What does it fix/what does it change/how was it tested (even manually,
if necessary)? -->
## Summary of the Pull Request
This PR:
Includes an update to create unique screenshot filenames, saving the
user from having to manually type a new filename each time they save a
new screenshot in the same location.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43158
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
## Unique filename issue
The PR fixes the user's complaint about the screenshots always being
called "zoomit" by adding a `GetUniqueScreenshotFilename()` method which
creates a unique filename based on the current date and time, e.g.
`ZoomIt 2025-11-01 004723.png`. This is consistent with other tools like
the Windows Snipping Tool and provides files which sort correctly when
ordered by name, which is not the case for files with simple numeric
suffixes.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Tested on a local build.
2026-02-24 04:53:22 +00:00
SYSTEMTIME lt ;
GetLocalTime ( & lt ) ;
// Format: "ZoomIt YYYY-MM-DD HHMMSS.png"
wchar_t buffer [ MAX_PATH ] ;
swprintf_s ( buffer , L " %s %04d-%02d-%02d %02d%02d%02d.png " ,
APPNAME ,
lt . wYear , lt . wMonth , lt . wDay ,
lt . wHour , lt . wMinute , lt . wSecond ) ;
return std : : wstring ( buffer ) ;
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// StartRecordingAsync
2025-11-10 11:57:13 -08:00
//
2025-01-16 20:52:24 +00:00
// Starts the screen recording.
//
//----------------------------------------------------------------------------
winrt : : fire_and_forget StartRecordingAsync ( HWND hWnd , LPRECT rcCrop , HWND hWndRecord ) try
{
2026-02-03 13:05:31 -08:00
// Capture the UI thread context so we can resume on it for the save dialog
winrt : : apartment_context uiThread ;
2025-01-16 20:52:24 +00:00
auto tempFolderPath = std : : filesystem : : temp_directory_path ( ) . wstring ( ) ;
auto tempFolder = co_await winrt : : StorageFolder : : GetFolderFromPathAsync ( tempFolderPath ) ;
auto appFolder = co_await tempFolder . CreateFolderAsync ( L " ZoomIt " , winrt : : CreationCollisionOption : : OpenIfExists ) ;
2025-11-10 11:57:13 -08:00
// Choose temp file extension based on format
const wchar_t * tempFileName = ( g_RecordingFormat = = RecordingFormat : : GIF ) ? L " zoomit.gif " : L " zoomit.mp4 " ;
auto file = co_await appFolder . CreateFileAsync ( tempFileName , winrt : : CreationCollisionOption : : ReplaceExisting ) ;
2025-01-16 20:52:24 +00:00
// Get the device
auto d3dDevice = util : : CreateD3D11Device ( ) ;
auto dxgiDevice = d3dDevice . as < IDXGIDevice > ( ) ;
g_RecordDevice = CreateDirect3DDevice ( dxgiDevice . get ( ) ) ;
// Get the active MONITOR capture device
HMONITOR hMon = NULL ;
2025-11-10 11:57:13 -08:00
POINT cursorPos = { 0 , 0 } ;
2025-01-16 20:52:24 +00:00
if ( pMonitorFromPoint ) {
GetCursorPos ( & cursorPos ) ;
hMon = pMonitorFromPoint ( cursorPos , MONITOR_DEFAULTTONEAREST ) ;
}
winrt : : Windows : : Graphics : : Capture : : GraphicsCaptureItem item { nullptr } ;
2025-11-10 11:57:13 -08:00
if ( hWndRecord )
2025-01-16 20:52:24 +00:00
item = util : : CreateCaptureItemForWindow ( hWndRecord ) ;
else
item = util : : CreateCaptureItemForMonitor ( hMon ) ;
auto stream = co_await file . OpenAsync ( winrt : : FileAccessMode : : ReadWrite ) ;
2025-11-10 11:57:13 -08:00
// Create the appropriate recording session based on format
2025-11-14 17:36:31 -08:00
OutputDebugStringW ( ( L " Starting recording session. Framerate: " + std : : to_wstring ( g_RecordFrameRate ) + L " scaling: " + std : : to_wstring ( g_RecordScaling ) + L " Format: " + ( g_RecordingFormat = = RecordingFormat : : GIF ? L " GIF " : L " MP4 " ) + L " \n " ) . c_str ( ) ) ;
2026-02-03 13:05:31 -08:00
bool recordingStarted = false ;
HRESULT captureStatus = S_OK ;
2025-11-10 11:57:13 -08:00
if ( g_RecordingFormat = = RecordingFormat : : GIF )
{
g_GifRecordingSession = GifRecordingSession : : Create (
g_RecordDevice ,
item ,
* rcCrop ,
g_RecordFrameRate ,
stream ) ;
2026-02-03 13:05:31 -08:00
recordingStarted = ( g_GifRecordingSession ! = nullptr ) ;
2025-11-10 11:57:13 -08:00
if ( g_hWndLiveZoom ! = NULL )
g_GifRecordingSession - > EnableCursorCapture ( false ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
if ( recordingStarted )
{
try
{
co_await g_GifRecordingSession - > StartAsync ( ) ;
}
catch ( const winrt : : hresult_error & error )
{
captureStatus = error . code ( ) ;
OutputDebugStringW ( ( L " Recording session failed: " + error . message ( ) + L " \n " ) . c_str ( ) ) ;
}
}
// If no frames were captured, behave as if the hotkey was never pressed.
if ( recordingStarted & & g_GifRecordingSession & & ! g_GifRecordingSession - > HasCapturedFrames ( ) )
{
if ( stream )
{
stream . Close ( ) ;
stream = nullptr ;
}
try { co_await file . DeleteAsync ( ) ; } catch ( . . . ) { }
g_RecordingSession = nullptr ;
g_GifRecordingSession = nullptr ;
co_return ;
}
2025-11-10 11:57:13 -08:00
}
else
{
g_RecordingSession = VideoRecordingSession : : Create (
g_RecordDevice ,
item ,
* rcCrop ,
g_RecordFrameRate ,
g_CaptureAudio ,
2026-02-03 13:05:31 -08:00
g_CaptureSystemAudio ,
2026-02-05 01:05:33 +01:00
g_MicMonoMix ,
2025-11-10 11:57:13 -08:00
stream ) ;
2026-02-03 13:05:31 -08:00
recordingStarted = ( g_RecordingSession ! = nullptr ) ;
2025-11-10 11:57:13 -08:00
if ( g_hWndLiveZoom ! = NULL )
g_RecordingSession - > EnableCursorCapture ( false ) ;
2026-02-03 13:05:31 -08:00
if ( recordingStarted )
{
try
{
co_await g_RecordingSession - > StartAsync ( ) ;
}
catch ( const winrt : : hresult_error & error )
{
captureStatus = error . code ( ) ;
OutputDebugStringW ( ( L " Recording session failed: " + error . message ( ) + L " \n " ) . c_str ( ) ) ;
}
}
// If no frames were captured, behave as if the hotkey was never pressed.
if ( recordingStarted & & g_RecordingSession & & ! g_RecordingSession - > HasCapturedVideoFrames ( ) )
{
if ( stream )
{
stream . Close ( ) ;
stream = nullptr ;
}
try { co_await file . DeleteAsync ( ) ; } catch ( . . . ) { }
g_RecordingSession = nullptr ;
g_GifRecordingSession = nullptr ;
co_return ;
}
}
// If we never created a session, bail and clean up the temp file silently
if ( ! recordingStarted ) {
if ( stream ) {
stream . Close ( ) ;
stream = nullptr ;
}
try { co_await file . DeleteAsync ( ) ; } catch ( . . . ) { }
co_return ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// Recording completed (closed via hotkey or item close). Proceed to save/trim workflow.
OutputDebugStringW ( L " [Recording] StartAsync completed, entering save workflow \n " ) ;
Zoomit: Fix a issue that after trim, the video can't be saved and we can't start a new recording session (#46034)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
The bug was caused by a resource lifetime issue between the recording
phase and the save/trim phase. After a recording stopped,
StartRecordingAsync moved directly into the save workflow while it was
still holding the temporary recording stream and the active recording
session objects, later file operations in the save flow could fail
against that same file. Once that happened, ZoomIt could end up stuck in
a bad state where the first save did not complete cleanly and subsequent
recording attempts would no longer start until ZoomIt was restarted.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
As title
- [ ] Closes: #46006
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Validated locally recording works fine when trimmed
2026-03-10 23:41:15 +08:00
// Release the writer stream and session objects before trim/save. Keeping the temp file
// open here can cause trimming and later MoveAndReplaceAsync calls to fail on the same file.
if ( stream )
{
stream . Close ( ) ;
stream = nullptr ;
}
g_RecordingSession = nullptr ;
g_GifRecordingSession = nullptr ;
2026-02-03 13:05:31 -08:00
// Resume on the UI thread for the save dialog
co_await uiThread ;
OutputDebugStringW ( L " [Recording] Resumed on UI thread \n " ) ;
{
2025-01-16 20:52:24 +00:00
g_bSaveInProgress = true ;
SendMessage ( g_hWndMain , WM_USER_SAVE_CURSOR , 0 , 0 ) ;
winrt : : StorageFile destFile = nullptr ;
HRESULT hr = S_OK ;
try {
2026-02-03 13:05:31 -08:00
// Show trim dialog option and save dialog
std : : wstring trimmedFilePath ;
auto suggestedName = GetUniqueRecordingFilename ( ) ;
auto finalPath = VideoRecordingSession : : ShowSaveDialogWithTrim (
hWnd ,
suggestedName ,
std : : wstring { file . Path ( ) } ,
trimmedFilePath
) ;
2025-11-10 11:57:13 -08:00
2026-02-03 13:05:31 -08:00
if ( ! finalPath . empty ( ) )
2025-11-10 11:57:13 -08:00
{
2026-02-03 13:05:31 -08:00
auto path = std : : filesystem : : path ( finalPath ) ;
winrt : : StorageFolder folder { co_await winrt : : StorageFolder : : GetFolderFromPathAsync ( path . parent_path ( ) . c_str ( ) ) } ;
destFile = co_await folder . CreateFileAsync ( path . filename ( ) . c_str ( ) , winrt : : CreationCollisionOption : : ReplaceExisting ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// If user trimmed, use the trimmed file
winrt : : StorageFile sourceFile = file ;
if ( ! trimmedFilePath . empty ( ) )
2025-11-14 17:36:31 -08:00
{
2026-02-03 13:05:31 -08:00
sourceFile = co_await winrt : : StorageFile : : GetFileFromPathAsync ( trimmedFilePath ) ;
2025-11-14 17:36:31 -08:00
}
2026-02-03 13:05:31 -08:00
// Move the chosen source into the user-selected destination
co_await sourceFile . MoveAndReplaceAsync ( destFile ) ;
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// If we moved a trimmed copy, clean up the original temp capture file
if ( sourceFile ! = file )
{
try { co_await file . DeleteAsync ( ) ; } catch ( . . . ) { }
2025-11-14 17:36:31 -08:00
}
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// Use finalPath directly - destFile.Path() may be stale after MoveAndReplaceAsync
g_RecordingSaveLocation = finalPath ;
// Update the registry buffer and save to persist across app restarts
wcsncpy_s ( g_RecordingSaveLocationBuffer , g_RecordingSaveLocation . c_str ( ) , _TRUNCATE ) ;
reg . WriteRegSettings ( RegSettings ) ;
SaveToClipboard ( g_RecordingSaveLocation . c_str ( ) , hWnd ) ;
}
else
{
// User cancelled
hr = HRESULT_FROM_WIN32 ( ERROR_CANCELLED ) ;
}
//auto saveDialog = wil::CoCreateInstance<IFileSaveDialog>( CLSID_FileSaveDialog );
//FILEOPENDIALOGOPTIONS options;
//if( SUCCEEDED( saveDialog->GetOptions( &options ) ) )
// saveDialog->SetOptions( options | FOS_FORCEFILESYSTEM );
//wil::com_ptr<IShellItem> videosItem;
//if( SUCCEEDED ( SHGetKnownFolderItem( FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr, IID_IShellItem, (void**) videosItem.put() ) ) )
// saveDialog->SetDefaultFolder( videosItem.get() );
//// Set file type based on the recording format
//if (g_RecordingFormat == RecordingFormat::GIF)
//{
// saveDialog->SetDefaultExtension( L".gif" );
// COMDLG_FILTERSPEC fileTypes[] = {
// { L"GIF Animation", L"*.gif" }
// };
// saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
//}
//else
//{
// saveDialog->SetDefaultExtension( L".mp4" );
// COMDLG_FILTERSPEC fileTypes[] = {
// { L"MP4 Video", L"*.mp4" }
// };
// saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
//}
//// Peek the folder Windows has chosen to display
//static std::filesystem::path lastSaveFolder;
//wil::unique_cotaskmem_string chosenFolderPath;
//wil::com_ptr<IShellItem> currentSelectedFolder;
//bool bFolderChanged = false;
//if (SUCCEEDED(saveDialog->GetFolder(currentSelectedFolder.put())))
//{
// if (SUCCEEDED(currentSelectedFolder->GetDisplayName(SIGDN_FILESYSPATH, chosenFolderPath.put())))
// {
// if (lastSaveFolder != chosenFolderPath.get())
// {
// lastSaveFolder = chosenFolderPath.get() ? chosenFolderPath.get() : std::filesystem::path{};
// bFolderChanged = true;
// }
// }
//}
//if( (g_RecordingFormat == RecordingFormat::GIF && g_RecordingSaveLocationGIF.size() == 0) || (g_RecordingFormat == RecordingFormat::MP4 && g_RecordingSaveLocation.size() == 0) || (bFolderChanged)) {
// wil::com_ptr<IShellItem> shellItem;
// wil::unique_cotaskmem_string folderPath;
// if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put()))) {
// if (g_RecordingFormat == RecordingFormat::GIF) {
// g_RecordingSaveLocationGIF = folderPath.get();
// std::filesystem::path currentPath{ g_RecordingSaveLocationGIF };
// g_RecordingSaveLocationGIF = currentPath / DEFAULT_GIF_RECORDING_FILE;
// }
// else {
// g_RecordingSaveLocation = folderPath.get();
// if (g_RecordingFormat == RecordingFormat::MP4) {
// std::filesystem::path currentPath{ g_RecordingSaveLocation };
// g_RecordingSaveLocation = currentPath / DEFAULT_RECORDING_FILE;
// }
// }
// }
//}
//// Always use appropriate default filename based on current format
//auto suggestedName = GetUniqueRecordingFilename();
//saveDialog->SetFileName( suggestedName.c_str() );
//THROW_IF_FAILED( saveDialog->Show( hWnd ) );
//wil::com_ptr<IShellItem> shellItem;
//THROW_IF_FAILED(saveDialog->GetResult(shellItem.put()));
//wil::unique_cotaskmem_string filePath;
//THROW_IF_FAILED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, filePath.put()));
//auto path = std::filesystem::path( filePath.get() );
//winrt::StorageFolder folder{ co_await winrt::StorageFolder::GetFolderFromPathAsync( path.parent_path().c_str() ) };
//destFile = co_await folder.CreateFileAsync( path.filename().c_str(), winrt::CreationCollisionOption::ReplaceExisting );
2025-01-16 20:52:24 +00:00
}
catch ( const wil : : ResultException & error ) {
2026-02-03 13:05:31 -08:00
OutputDebugStringW ( ( L " [Recording] wil exception: hr=0x " + std : : to_wstring ( error . GetErrorCode ( ) ) + L " \n " ) . c_str ( ) ) ;
2025-01-16 20:52:24 +00:00
hr = error . GetErrorCode ( ) ;
}
2026-02-03 13:05:31 -08:00
catch ( const std : : exception & ex ) {
OutputDebugStringA ( " [Recording] std::exception: " ) ;
OutputDebugStringA ( ex . what ( ) ) ;
OutputDebugStringA ( " \n " ) ;
hr = E_FAIL ;
}
catch ( . . . ) {
OutputDebugStringW ( L " [Recording] Unknown exception in save workflow \n " ) ;
hr = E_FAIL ;
}
2025-01-16 20:52:24 +00:00
if ( destFile = = nullptr ) {
zoomit bug fixes (#41773)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #41040, #41041, #41043
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Manually tested and ensured that the issues are
resolved.
- [ ] **Localization:** N/A
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** No new binaries added
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
This PR includes code-changes to restore some of the features of ZoomIt
that existed in older versions and lost in later versions, such as when
in Draw mode, after drawing, if an area is snipped, it doesn't clear the
drawing immediately, giving the user an option to cancel snip and update
the drawing. Also, in draw mode, when left mouse is clicked, it results
in a dot, as it was in previous versions. This PR also addresses some
race conditions during Recording that results in error when the
Recording is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually tested and validated the draw and snip modes.
2025-09-14 22:59:48 -07:00
if ( stream ) {
stream . Close ( ) ;
stream = nullptr ;
}
2026-02-03 13:05:31 -08:00
try { co_await file . DeleteAsync ( ) ; } catch ( . . . ) { }
2025-01-16 20:52:24 +00:00
}
g_bSaveInProgress = false ;
SendMessage ( g_hWndMain , WM_USER_RESTORE_CURSOR , 0 , 0 ) ;
if ( hWnd = = g_hWndMain )
RestoreForeground ( ) ;
if ( FAILED ( hr ) )
2026-03-26 13:21:43 +01:00
{
if ( hr ! = HRESULT_FROM_WIN32 ( ERROR_CANCELLED ) )
{
throw winrt : : hresult_error ( hr ) ;
}
}
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
// Ensure globals are reset after the save/cleanup path completes
if ( stream ) {
stream . Close ( ) ;
stream = nullptr ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
g_RecordingSession = nullptr ;
g_GifRecordingSession = nullptr ;
2025-01-16 20:52:24 +00:00
} catch ( const winrt : : hresult_error & error ) {
2026-02-03 13:05:31 -08:00
// Reset the save-in-progress flag so that hotkeys are not blocked after an error or cancellation
g_bSaveInProgress = false ;
2025-01-16 20:52:24 +00:00
PostMessage ( g_hWndMain , WM_USER_STOP_RECORDING , 0 , 0 ) ;
// Suppress the error from canceling the save dialog
if ( error . code ( ) = = HRESULT_FROM_WIN32 ( ERROR_CANCELLED ) )
co_return ;
if ( g_RecordToggle = = FALSE ) {
MessageBox ( g_hWndMain , L " Recording cancelled before started " , APPNAME , MB_OK | MB_ICONERROR | MB_SYSTEMMODAL ) ;
}
else {
ErrorDialogString ( g_hWndMain , L " Error starting recording " , error . message ( ) . c_str ( ) ) ;
}
}
//----------------------------------------------------------------------------
//
// UpdateMonitorInfo
//
//----------------------------------------------------------------------------
void UpdateMonitorInfo ( POINT point , MONITORINFO * monInfo )
{
HMONITOR hMon { } ;
if ( pMonitorFromPoint ! = nullptr )
{
hMon = pMonitorFromPoint ( point , MONITOR_DEFAULTTONEAREST ) ;
}
if ( hMon ! = nullptr )
{
monInfo - > cbSize = sizeof * monInfo ;
pGetMonitorInfo ( hMon , monInfo ) ;
}
else
{
* monInfo = { } ;
HDC hdcScreen = CreateDC ( L " DISPLAY " , nullptr , nullptr , nullptr ) ;
if ( hdcScreen ! = nullptr )
{
monInfo - > rcMonitor . right = GetDeviceCaps ( hdcScreen , HORZRES ) ;
monInfo - > rcMonitor . bottom = GetDeviceCaps ( hdcScreen , VERTRES ) ;
DeleteDC ( hdcScreen ) ;
}
}
}
# ifdef __ZOOMIT_POWERTOYS__
HRESULT OpenPowerToysSettingsApp ( )
{
std : : wstring path = get_module_folderpath ( g_hInstance ) ;
path + = L " \\ PowerToys.exe " ;
std : : wstring openSettings = L " --open-settings=ZoomIt " ;
std : : wstring full_command_path = path + L " " + openSettings ;
STARTUPINFO startupInfo ;
ZeroMemory ( & startupInfo , sizeof ( STARTUPINFO ) ) ;
startupInfo . cb = sizeof ( STARTUPINFO ) ;
startupInfo . wShowWindow = SW_SHOWNORMAL ;
PROCESS_INFORMATION processInformation ;
CreateProcess (
path . c_str ( ) ,
full_command_path . data ( ) ,
NULL ,
NULL ,
TRUE ,
0 ,
NULL ,
NULL ,
& startupInfo ,
& processInformation ) ;
if ( ! CloseHandle ( processInformation . hProcess ) )
{
return HRESULT_FROM_WIN32 ( GetLastError ( ) ) ;
}
if ( ! CloseHandle ( processInformation . hThread ) )
{
return HRESULT_FROM_WIN32 ( GetLastError ( ) ) ;
}
return S_OK ;
}
# endif // __ZOOMIT_POWERTOYS__
//----------------------------------------------------------------------------
//
// ShowMainWindow
//
//----------------------------------------------------------------------------
void ShowMainWindow ( HWND hWnd , const MONITORINFO & monInfo , int width , int height )
{
// Show the window first
SetWindowPos ( hWnd , HWND_TOPMOST , monInfo . rcMonitor . left , monInfo . rcMonitor . top ,
width , height , SWP_SHOWWINDOW | SWP_NOCOPYBITS ) ;
// Now invalidate and update the window
InvalidateRect ( hWnd , NULL , TRUE ) ;
UpdateWindow ( hWnd ) ;
SetForegroundWindow ( hWnd ) ;
SetActiveWindow ( hWnd ) ;
}
//----------------------------------------------------------------------------
//
// MainWndProc
//
//----------------------------------------------------------------------------
LRESULT APIENTRY MainWndProc (
2025-11-10 11:57:13 -08:00
HWND hWnd ,
2025-01-16 20:52:24 +00:00
UINT message ,
2025-11-10 11:57:13 -08:00
WPARAM wParam ,
2025-01-16 20:52:24 +00:00
LPARAM lParam )
{
static int width , height ;
static HDC hdcScreen , hdcScreenCompat , hdcScreenCursorCompat , hdcScreenSaveCompat ;
static HBITMAP hbmpCompat , hbmpDrawingCompat , hbmpCursorCompat ;
static RECT cropRc { } ;
static BITMAP bmp ;
static BOOLEAN g_TimerActive = FALSE ;
static BOOLEAN g_Zoomed = FALSE ;
static TypeModeState g_TypeMode = TypeModeOff ;
static BOOLEAN g_HaveTyped = FALSE ;
static DEVMODE secondaryDevMode ;
static RECT g_LiveZoomSourceRect ;
static float g_LiveZoomLevel ;
static float zoomLevel ;
static float zoomTelescopeStep ;
static float zoomTelescopeTarget ;
static POINT cursorPos ;
static POINT savedCursorPos ;
static RECT cursorRc ;
static RECT boundRc ;
static POINT prevPt ;
static POINT textStartPt ;
static POINT textPt ;
static P_DRAW_UNDO drawUndoList = NULL ;
static P_TYPED_KEY typedKeyList = NULL ;
static BOOLEAN g_HaveDrawn = FALSE ;
static DWORD g_DrawingShape = 0 ;
static DWORD prevPenWidth = g_PenWidth ;
static POINT g_RectangleAnchor ;
static RECT g_rcRectangle ;
static BOOLEAN g_Tracing = FALSE ;
static int g_BlankedScreen = 0 ;
static int g_StraightDirection = 0 ;
static BOOLEAN g_Drawing = FALSE ;
static HWND g_ActiveWindow = NULL ;
static int breakTimeout ;
static HBITMAP g_hBackgroundBmp = NULL ;
static HDC g_hDcBackgroundFile ;
static HPEN hDrawingPen ;
static HFONT hTimerFont ;
static HFONT hNegativeTimerFont ;
static HFONT hTypingFont ;
static MONITORINFO monInfo ;
static MONITORINFO lastMonInfo ;
static HWND hTargetWindow = NULL ;
static RECT rcTargetWindow ;
static BOOLEAN forcePenResize = TRUE ;
static BOOLEAN activeBreakShowDesktop = g_BreakShowDesktop ;
static BOOLEAN activeBreakShowBackgroundFile = g_BreakShowBackgroundFile ;
static TCHAR activeBreakBackgroundFile [ MAX_PATH ] = { 0 } ;
static UINT wmTaskbarCreated ;
#if 0
TITLEBARINFO titleBarInfo ;
WINDOWINFO targetWindowInfo ;
# endif
bool isCaptureSupported = false ;
RECT rc , rc1 ;
2025-11-10 11:57:13 -08:00
PAINTSTRUCT ps ;
2025-01-16 20:52:24 +00:00
TCHAR timerText [ 16 ] ;
TCHAR negativeTimerText [ 16 ] ;
BOOLEAN penInverted ;
BOOLEAN zoomIn ;
HDC hDc ;
HWND hWndRecord ;
int x , y , delta ;
HMENU hPopupMenu ;
static TCHAR filePath [ MAX_PATH ] = { L " zoomit " } ;
NOTIFYICONDATA tNotifyIconData ;
2025-11-11 16:42:59 +01:00
static DWORD64 g_TelescopingZoomLastTick = 0ull ;
2025-01-16 20:52:24 +00:00
const auto drawAllRightJustifiedLines = [ & rc ] ( long lineHeight , bool doPop = false ) {
rc . top = textPt . y - static_cast < LONG > ( g_TextBufferPreviousLines . size ( ) ) * lineHeight ;
for ( const auto & line : g_TextBufferPreviousLines )
{
DrawText ( hdcScreenCompat , line . c_str ( ) , static_cast < int > ( line . length ( ) ) , & rc , DT_CALCRECT ) ;
const auto textWidth = rc . right - rc . left ;
rc . left = textPt . x - textWidth ;
rc . right = textPt . x ;
DrawText ( hdcScreenCompat , line . c_str ( ) , static_cast < int > ( line . length ( ) ) , & rc , DT_LEFT ) ;
rc . top + = lineHeight ;
}
if ( ! g_TextBuffer . empty ( ) )
{
if ( doPop )
{
g_TextBuffer . pop_back ( ) ;
}
DrawText ( hdcScreenCompat , g_TextBuffer . c_str ( ) , static_cast < int > ( g_TextBuffer . length ( ) ) , & rc , DT_CALCRECT ) ;
rc . left = textPt . x - ( rc . right - rc . left ) ;
rc . right = textPt . x ;
DrawText ( hdcScreenCompat , g_TextBuffer . c_str ( ) , static_cast < int > ( g_TextBuffer . length ( ) ) , & rc , DT_LEFT ) ;
}
} ;
2025-11-11 16:42:59 +01:00
const auto doTelescopingZoomTimer = [ hWnd , wParam , lParam , & x , & y ] ( bool invalidate = true ) {
if ( zoomTelescopeStep ! = 0.0f )
{
zoomLevel * = zoomTelescopeStep ;
g_TelescopingZoomLastTick = GetTickCount64 ( ) ;
if ( ( zoomTelescopeStep > 1 & & zoomLevel > = zoomTelescopeTarget ) | |
( zoomTelescopeStep < 1 & & zoomLevel < = zoomTelescopeTarget ) )
{
zoomLevel = zoomTelescopeTarget ;
g_TelescopingZoomLastTick = 0ull ;
KillTimer ( hWnd , wParam ) ;
OutputDebug ( L " SETCURSOR mon_left: %x mon_top: %x x: %d y: %d \n " ,
monInfo . rcMonitor . left ,
monInfo . rcMonitor . top ,
cursorPos . x ,
cursorPos . y ) ;
SetCursorPos ( monInfo . rcMonitor . left + cursorPos . x ,
monInfo . rcMonitor . top + cursorPos . y ) ;
}
}
else
{
// Case where we didn't zoom at all
g_TelescopingZoomLastTick = 0ull ;
KillTimer ( hWnd , wParam ) ;
}
if ( wParam = = 2 & & zoomLevel = = 1 )
{
g_Zoomed = FALSE ;
2026-02-03 13:05:31 -08:00
// Unregister Ctrl+C and Ctrl+S hotkeys when exiting static zoom
UnregisterHotKey ( hWnd , COPY_IMAGE_HOTKEY ) ;
UnregisterHotKey ( hWnd , COPY_CROP_HOTKEY ) ;
UnregisterHotKey ( hWnd , SAVE_IMAGE_HOTKEY ) ;
UnregisterHotKey ( hWnd , SAVE_CROP_HOTKEY ) ;
2025-11-11 16:42:59 +01:00
if ( g_ZoomOnLiveZoom )
{
GetCursorPos ( & cursorPos ) ;
cursorPos = ScalePointInRects ( cursorPos , monInfo . rcMonitor , g_LiveZoomSourceRect ) ;
SetCursorPos ( cursorPos . x , cursorPos . y ) ;
SendMessage ( hWnd , WM_HOTKEY , LIVE_HOTKEY , 0 ) ;
}
else if ( lParam ! = SHALLOW_ZOOM )
{
// Figure out where final unzoomed cursor should be
if ( g_Drawing )
{
cursorPos = prevPt ;
}
OutputDebug ( L " FINAL MOUSE: x: %d y: %d \n " , cursorPos . x , cursorPos . y ) ;
GetZoomedTopLeftCoordinates ( zoomLevel , & cursorPos , & x , width , & y , height ) ;
cursorPos . x = monInfo . rcMonitor . left + x + static_cast < int > ( ( cursorPos . x - x ) * zoomLevel ) ;
cursorPos . y = monInfo . rcMonitor . top + y + static_cast < int > ( ( cursorPos . y - y ) * zoomLevel ) ;
SetCursorPos ( cursorPos . x , cursorPos . y ) ;
}
if ( hTargetWindow )
{
SetWindowPos ( hTargetWindow , HWND_BOTTOM , rcTargetWindow . left , rcTargetWindow . top , rcTargetWindow . right - rcTargetWindow . left , rcTargetWindow . bottom - rcTargetWindow . top , 0 ) ;
hTargetWindow = NULL ;
}
DeleteDrawUndoList ( & drawUndoList ) ;
// Restore live zoom if we came from that mode
if ( g_ZoomOnLiveZoom )
{
SendMessage ( g_hWndLiveZoom , WM_USER_SET_ZOOM , static_cast < WPARAM > ( g_LiveZoomLevel ) , reinterpret_cast < LPARAM > ( & g_LiveZoomSourceRect ) ) ;
g_ZoomOnLiveZoom = FALSE ;
forcePenResize = TRUE ;
}
SetForegroundWindow ( g_ActiveWindow ) ;
ClipCursor ( NULL ) ;
g_HaveDrawn = FALSE ;
g_TypeMode = TypeModeOff ;
g_HaveTyped = FALSE ;
g_Drawing = FALSE ;
EnableDisableStickyKeys ( TRUE ) ;
DeleteObject ( hTypingFont ) ;
DeleteDC ( hdcScreen ) ;
DeleteDC ( hdcScreenCompat ) ;
DeleteDC ( hdcScreenCursorCompat ) ;
DeleteDC ( hdcScreenSaveCompat ) ;
DeleteObject ( hbmpCompat ) ;
DeleteObject ( hbmpCursorCompat ) ;
DeleteObject ( hbmpDrawingCompat ) ;
DeleteObject ( hDrawingPen ) ;
SetFocus ( g_ActiveWindow ) ;
ShowWindow ( hWnd , SW_HIDE ) ;
}
if ( invalidate )
{
InvalidateRect ( hWnd , NULL , FALSE ) ;
}
} ;
2025-01-16 20:52:24 +00:00
switch ( message ) {
case WM_CREATE :
// get default font
2025-11-10 11:57:13 -08:00
GetObject ( GetStockObject ( DEFAULT_GUI_FONT ) , sizeof g_LogFont , & g_LogFont ) ;
2025-01-16 20:52:24 +00:00
g_LogFont . lfWeight = FW_NORMAL ;
hDc = CreateCompatibleDC ( NULL ) ;
g_LogFont . lfHeight = - MulDiv ( 8 , GetDeviceCaps ( hDc , LOGPIXELSY ) , 72 ) ;
DeleteDC ( hDc ) ;
reg . ReadRegSettings ( RegSettings ) ;
2025-11-10 11:57:13 -08:00
2026-03-26 13:21:43 +01:00
// Recover screensaver settings if ZoomIt crashed while the break
// screensaver was active (or was force-killed).
if ( HasOrphanedScreenSaverSettings ( ) )
{
RestoreScreenSaverSettings ( ) ;
}
// Register for session change notifications so we can restore
// screensaver settings after the break timer screensaver is dismissed
// via user authentication (session unlock).
WTSRegisterSessionNotification ( hWnd , NOTIFY_FOR_THIS_SESSION ) ;
2026-02-03 13:05:31 -08:00
// Refresh dark mode state after loading theme override from registry
RefreshDarkModeState ( ) ;
// Initialize save location strings from registry buffers
g_RecordingSaveLocation = g_RecordingSaveLocationBuffer ;
g_ScreenshotSaveLocation = g_ScreenshotSaveLocationBuffer ;
2025-11-10 11:57:13 -08:00
// Set g_RecordScaling based on the current recording format
if ( g_RecordingFormat = = RecordingFormat : : GIF ) {
g_RecordScaling = g_RecordScalingGIF ;
2025-11-14 17:36:31 -08:00
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE ;
2025-11-10 11:57:13 -08:00
} else {
g_RecordScaling = g_RecordScalingMP4 ;
2025-11-14 17:36:31 -08:00
g_RecordFrameRate = RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE ;
2025-11-10 11:57:13 -08:00
}
// to support migrating from
2025-01-16 20:52:24 +00:00
if ( ( g_PenColor > > 24 ) = = 0 ) {
g_PenColor | = 0xFF < < 24 ;
}
g_PenWidth = g_RootPenWidth ;
g_ToggleMod = GetKeyMod ( g_ToggleKey ) ;
g_LiveZoomToggleMod = GetKeyMod ( g_LiveZoomToggleKey ) ;
g_DrawToggleMod = GetKeyMod ( g_DrawToggleKey ) ;
g_BreakToggleMod = GetKeyMod ( g_BreakToggleKey ) ;
g_DemoTypeToggleMod = GetKeyMod ( g_DemoTypeToggleKey ) ;
g_SnipToggleMod = GetKeyMod ( g_SnipToggleKey ) ;
2026-03-26 13:21:43 +01:00
g_SnipPanoramaToggleMod = GetKeyMod ( g_SnipPanoramaToggleKey ) ;
g_SnipOcrToggleMod = GetKeyMod ( g_SnipOcrToggleKey ) ;
2025-01-16 20:52:24 +00:00
g_RecordToggleMod = GetKeyMod ( g_RecordToggleKey ) ;
if ( ! g_OptionsShown & & ! g_StartedByPowerToys ) {
// First run should show options when running as standalone. If not running as standalone,
// options screen won't show and we should register keys instead.
SendMessage ( hWnd , WM_COMMAND , IDC_OPTIONS , 0 ) ;
g_OptionsShown = TRUE ;
reg . WriteRegSettings ( RegSettings ) ;
} else {
BOOL showOptions = FALSE ;
if ( g_ToggleKey & & ! RegisterHotKey ( hWnd , ZOOM_HOTKEY , g_ToggleMod , g_ToggleKey & 0xFF ) ) {
MessageBox ( hWnd , L " The specified zoom toggle hotkey is already in use. \n Select a different zoom toggle hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
2025-11-10 11:57:13 -08:00
} else if ( g_LiveZoomToggleKey & &
2025-01-16 20:52:24 +00:00
( ! RegisterHotKey ( hWnd , LIVE_HOTKEY , g_LiveZoomToggleMod , g_LiveZoomToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , LIVE_DRAW_HOTKEY , ( g_LiveZoomToggleMod ^ MOD_SHIFT ) , g_LiveZoomToggleKey & 0xFF ) ) ) {
MessageBox ( hWnd , L " The specified live-zoom toggle hotkey is already in use. \n Select a different zoom toggle hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
} else if ( g_DrawToggleKey & & ! RegisterHotKey ( hWnd , DRAW_HOTKEY , g_DrawToggleMod , g_DrawToggleKey & 0xFF ) ) {
MessageBox ( hWnd , L " The specified draw w/out zoom hotkey is already in use. \n Select a different draw w/out zoom hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
else if ( g_BreakToggleKey & & ! RegisterHotKey ( hWnd , BREAK_HOTKEY , g_BreakToggleMod , g_BreakToggleKey & 0xFF ) ) {
MessageBox ( hWnd , L " The specified break timer hotkey is already in use. \n Select a different break timer hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
2025-11-10 11:57:13 -08:00
else if ( g_DemoTypeToggleKey & &
2025-01-16 20:52:24 +00:00
( ! RegisterHotKey ( hWnd , DEMOTYPE_HOTKEY , g_DemoTypeToggleMod , g_DemoTypeToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , DEMOTYPE_RESET_HOTKEY , ( g_DemoTypeToggleMod ^ MOD_SHIFT ) , g_DemoTypeToggleKey & 0xFF ) ) ) {
MessageBox ( hWnd , L " The specified live-type hotkey is already in use. \n Select a different live-type hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
2025-11-10 11:57:13 -08:00
else if ( g_SnipToggleKey & &
2025-01-16 20:52:24 +00:00
( ! RegisterHotKey ( hWnd , SNIP_HOTKEY , g_SnipToggleMod , g_SnipToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , SNIP_SAVE_HOTKEY , ( g_SnipToggleMod ^ MOD_SHIFT ) , g_SnipToggleKey & 0xFF ) ) ) {
MessageBox ( hWnd , L " The specified snip hotkey is already in use. \n Select a different snip hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
2026-03-26 13:21:43 +01:00
}
else if ( g_SnipPanoramaToggleKey & &
( g_SnipPanoramaToggleKey ! = g_SnipToggleKey | | g_SnipPanoramaToggleMod ! = g_SnipToggleMod ) & &
( ! RegisterHotKey ( hWnd , SNIP_PANORAMA_HOTKEY , g_SnipPanoramaToggleMod | MOD_NOREPEAT , g_SnipPanoramaToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , SNIP_PANORAMA_SAVE_HOTKEY , ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT , g_SnipPanoramaToggleKey & 0xFF ) ) ) {
MessageBox ( hWnd , L " The specified panorama snip hotkey is already in use. \n Select a different panorama snip hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
else if ( g_SnipOcrToggleKey & &
! RegisterHotKey ( hWnd , SNIP_OCR_HOTKEY , g_SnipOcrToggleMod , g_SnipOcrToggleKey & 0xFF ) ) {
MessageBox ( hWnd , L " The specified snip OCR hotkey is already in use. \n Select a different snip OCR hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
2025-01-16 20:52:24 +00:00
}
2025-11-10 11:57:13 -08:00
else if ( g_RecordToggleKey & &
2025-01-16 20:52:24 +00:00
( ! RegisterHotKey ( hWnd , RECORD_HOTKEY , g_RecordToggleMod | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , RECORD_CROP_HOTKEY , ( g_RecordToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , RECORD_WINDOW_HOTKEY , ( g_RecordToggleMod ^ MOD_ALT ) | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) ) ) {
MessageBox ( hWnd , L " The specified record hotkey is already in use. \n Select a different record hotkey. " ,
APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
if ( showOptions ) {
SendMessage ( hWnd , WM_COMMAND , IDC_OPTIONS , 0 ) ;
}
}
SetThreadPriority ( GetCurrentThread ( ) , THREAD_PRIORITY_TIME_CRITICAL ) ;
wmTaskbarCreated = RegisterWindowMessage ( _T ( " TaskbarCreated " ) ) ;
return TRUE ;
case WM_CLOSE :
// Do not allow users to close the main window, for example with Alt-F4.
return 0 ;
case WM_HOTKEY :
2026-03-26 13:21:43 +01:00
OutputDebug ( L " [Hotkey] WM_HOTKEY id=%ld(%s) lParam=0x%llX \n " ,
static_cast < long > ( wParam ) ,
HotkeyIdToString ( wParam ) ,
static_cast < unsigned long long > ( lParam ) ) ;
LogPanoramaState ( L " WM_HOTKEY entry " , wParam ) ;
if ( g_PanoramaCaptureActive )
{
if ( wParam = = SNIP_PANORAMA_HOTKEY | | wParam = = SNIP_PANORAMA_SAVE_HOTKEY )
{
OutputDebug ( L " [Panorama] Stop hotkey received while capture is active \n " ) ;
g_PanoramaStopRequested = true ;
// If we're still selecting the panorama source area, stop selection
// immediately so the capture path can unwind cleanly.
if ( g_RecordCropping = = TRUE )
{
OutputDebug ( L " [Panorama] Stop requested during crop selection; stopping SelectRectangle \n " ) ;
g_SelectRectangle . Stop ( ) ;
g_RecordCropping = FALSE ;
}
}
// Do not process any other hotkeys while panorama capture is active.
LogPanoramaState ( L " WM_HOTKEY consumed while panorama active " , wParam ) ;
return 0 ;
}
if ( g_RecordCropping = = TRUE )
{
// If the crop overlay has already been torn down, clear stale state.
if ( ! g_SelectRectangle . IsActive ( ) )
{
OutputDebug ( L " [Hotkey] Clearing stale crop state (g_RecordCropping=TRUE but SelectRectangle inactive) \n " ) ;
g_RecordCropping = FALSE ;
}
}
2025-01-16 20:52:24 +00:00
if ( g_RecordCropping = = TRUE )
{
if ( wParam ! = RECORD_CROP_HOTKEY )
{
// Cancel cropping on any hotkey.
2026-03-26 13:21:43 +01:00
OutputDebug ( L " [Hotkey] Cancelling crop due to hotkey id=%ld(%s) \n " ,
static_cast < long > ( wParam ) ,
HotkeyIdToString ( wParam ) ) ;
2025-01-16 20:52:24 +00:00
g_SelectRectangle . Stop ( ) ;
g_RecordCropping = FALSE ;
// Cropping is handled by a blocking call in WM_HOTKEY, so post
// this message to the window for processing after the previous
// WM_HOTKEY message completes processing.
PostMessage ( hWnd , message , wParam , lParam ) ;
}
return 0 ;
}
//
// Magic value that comes from tray context menu
//
if ( lParam = = 1 ) {
//
// Sleep to let context menu dismiss
//
Sleep ( 250 ) ;
}
switch ( wParam ) {
case LIVE_DRAW_HOTKEY :
{
OutputDebug ( L " LIVE_DRAW_HOTKEY \n " ) ;
LONG_PTR exStyle = GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) ;
if ( ( exStyle & WS_EX_LAYERED ) ) {
OutputDebug ( L " LiveDraw reactivate \n " ) ;
// Just focus on the window and re-enter drawing mode
SetFocus ( hWnd ) ;
SetForegroundWindow ( hWnd ) ;
SendMessage ( hWnd , WM_LBUTTONDOWN , 0 , MAKELPARAM ( cursorPos . x , cursorPos . y ) ) ;
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( cursorPos . x , cursorPos . y ) ) ;
if ( IsWindowVisible ( g_hWndLiveZoom ) )
{
SendMessage ( g_hWndLiveZoom , WM_USER_MAGNIFY_CURSOR , FALSE , 0 ) ;
}
break ;
}
else {
OutputDebug ( L " LiveDraw create \n " ) ;
exStyle = GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) ;
SetWindowLongPtr ( hWnd , GWL_EXSTYLE , exStyle | WS_EX_LAYERED ) ;
SetLayeredWindowAttributes ( hWnd , COLORREF ( RGB ( 0 , 0 , 0 ) ) , 0 , LWA_COLORKEY ) ;
pMagSetWindowFilterList ( g_hWndLiveZoomMag , MW_FILTERMODE_EXCLUDE , 0 , nullptr ) ;
}
[[fallthrough]] ;
}
case DRAW_HOTKEY :
//
// Enter drawing mode without zoom
//
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateDraw ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
if ( ! g_Zoomed ) {
OutputDebug ( L " LiveDraw: %d (%d) \n " , wParam , ( wParam = = LIVE_DRAW_HOTKEY ) ) ;
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( IsWindowVisible ( g_hWndLiveZoom ) & & ! g_LiveZoomLevelOne ) {
# else
if ( IsWindowVisible ( g_hWndLiveZoom ) ) {
# endif
OutputDebug ( L " In Live zoom \n " ) ;
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , wParam = = LIVE_DRAW_HOTKEY ? LIVE_DRAW_ZOOM : 0 ) ;
} else {
OutputDebug ( L " Not in Live zoom \n " ) ;
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , wParam = = LIVE_DRAW_HOTKEY ? LIVE_DRAW_ZOOM : 0 ) ;
zoomLevel = zoomTelescopeTarget = 1 ;
SendMessage ( hWnd , WM_LBUTTONDOWN , 0 , MAKELPARAM ( cursorPos . x , cursorPos . y ) ) ;
}
if ( wParam = = LIVE_DRAW_HOTKEY ) {
SetLayeredWindowAttributes ( hWnd , COLORREF ( RGB ( 0 , 0 , 0 ) ) , 0 , LWA_COLORKEY ) ;
SendMessage ( hWnd , WM_KEYDOWN , ' K ' , LIVE_DRAW_ZOOM ) ;
SetTimer ( hWnd , 3 , 10 , NULL ) ;
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( cursorPos . x , cursorPos . y ) ) ;
ShowMainWindow ( hWnd , monInfo , width , height ) ;
if ( ( g_PenColor & 0xFFFFFF ) = = COLOR_BLUR )
{
// Blur is not supported in LiveDraw
g_PenColor = COLOR_RED ;
}
// Highlight is not supported in LiveDraw
g_PenColor | = 0xFF < < 24 ;
}
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
break ;
2026-03-26 13:21:43 +01:00
case SNIP_PANORAMA_HOTKEY :
case SNIP_PANORAMA_SAVE_HOTKEY :
2025-01-16 20:52:24 +00:00
case SNIP_SAVE_HOTKEY :
case SNIP_HOTKEY :
{
2026-02-03 13:05:31 -08:00
OutputDebugStringW ( ( L " [Snip] Hotkey received: " + std : : to_wstring ( LOWORD ( wParam ) ) +
L " (SNIP_SAVE= " + std : : to_wstring ( SNIP_SAVE_HOTKEY ) +
L " SNIP= " + std : : to_wstring ( SNIP_HOTKEY ) + L " ) \n " ) . c_str ( ) ) ;
2026-03-26 13:21:43 +01:00
const bool panoramaRequested = ( LOWORD ( wParam ) = = SNIP_PANORAMA_HOTKEY | | LOWORD ( wParam ) = = SNIP_PANORAMA_SAVE_HOTKEY ) ;
const bool panoramaSaveToFile = ( LOWORD ( wParam ) = = SNIP_PANORAMA_SAVE_HOTKEY ) ;
if ( panoramaRequested )
{
LogPanoramaState ( L " Panorama requested " , wParam ) ;
// Block liveZoom liveDraw snip due to mirroring bug
if ( IsWindowVisible ( g_hWndLiveZoom )
& & ( GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) )
{
OutputDebug ( L " [Panorama] Request ignored because liveDraw overlay is active \n " ) ;
break ;
}
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateSnip ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
if ( g_Drawing )
{
OutputDebug ( L " [Panorama] Exiting drawing mode before capture \n " ) ;
SendMessage ( hWnd , WM_USER_EXIT_MODE , 0 , 0 ) ;
if ( g_Drawing )
{
SendMessage ( hWnd , WM_USER_EXIT_MODE , 0 , 0 ) ;
}
}
if ( g_Zoomed )
{
OutputDebug ( L " [Panorama] Exiting zoom before capture; zoomOnLiveZoom=%d \n " , g_ZoomOnLiveZoom ? 1 : 0 ) ;
if ( g_ZoomOnLiveZoom )
{
ShowCursor ( false ) ;
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
ShowCursor ( true ) ;
}
else
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , SHALLOW_ZOOM ) ;
}
}
g_PanoramaCaptureActive = true ;
g_PanoramaStopRequested = false ;
LogPanoramaState ( L " Panorama capture armed " , wParam ) ;
auto panoramaCaptureCleanup = wil : : scope_exit ( [ hWnd ] {
LogPanoramaState ( L " Panorama cleanup begin " ) ;
g_PanoramaCaptureActive = false ;
g_PanoramaStopRequested = false ;
g_RecordCropping = FALSE ;
g_SelectRectangle . Stop ( ) ;
UNREFERENCED_PARAMETER ( hWnd ) ;
LogPanoramaState ( L " Panorama cleanup end " ) ;
} ) ;
const bool captureSuccess = panoramaSaveToFile
? RunPanoramaCaptureToFile ( hWnd )
: RunPanoramaCaptureToClipboard ( hWnd ) ;
OutputDebug ( L " [Panorama] RunPanoramaCapture%s result=%d \n " ,
panoramaSaveToFile ? L " ToFile " : L " ToClipboard " ,
captureSuccess ? 1 : 0 ) ;
if ( ! captureSuccess )
{
OutputDebugStringW ( panoramaSaveToFile
? L " [Panorama] Failed to save capture to file \n "
: L " [Panorama] Failed to copy capture to clipboard \n " ) ;
}
break ;
}
2025-01-16 20:52:24 +00:00
// Block liveZoom liveDraw snip due to mirroring bug
if ( IsWindowVisible ( g_hWndLiveZoom )
& & ( GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) )
{
break ;
}
bool zoomed = true ;
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateSnip ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
// First, static zoom
if ( ! g_Zoomed )
{
zoomed = false ;
if ( IsWindowVisible ( g_hWndLiveZoom ) & & ! g_LiveZoomLevelOne )
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , SHALLOW_ZOOM ) ;
}
else
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , LIVE_DRAW_ZOOM ) ;
}
zoomLevel = zoomTelescopeTarget = 1 ;
}
else if ( g_Drawing )
{
// Exit drawing mode to hide the drawing cursor
SendMessage ( hWnd , WM_USER_EXIT_MODE , 0 , 0 ) ;
// Exit again if still in drawing mode, which happens from type mode
if ( g_Drawing )
{
SendMessage ( hWnd , WM_USER_EXIT_MODE , 0 , 0 ) ;
}
}
ShowMainWindow ( hWnd , monInfo , width , height ) ;
if ( LOWORD ( wParam ) = = SNIP_SAVE_HOTKEY )
{
2026-02-03 13:05:31 -08:00
// IDC_SAVE_CROP handles cursor hiding internally after region selection
2025-01-16 20:52:24 +00:00
SendMessage ( hWnd , WM_COMMAND , IDC_SAVE_CROP , ( zoomed ? 0 : SHALLOW_ZOOM ) ) ;
}
else
{
SendMessage ( hWnd , WM_COMMAND , IDC_COPY_CROP , ( zoomed ? 0 : SHALLOW_ZOOM ) ) ;
}
// Now if we weren't zoomed, unzoom
if ( ! zoomed )
{
if ( g_ZoomOnLiveZoom )
{
// hiding the cursor allows for a cleaner transition back to the magnified cursor
ShowCursor ( false ) ;
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
ShowCursor ( true ) ;
}
else
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , SHALLOW_ZOOM ) ;
}
}
// exit zoom
if ( g_Zoomed )
{
// If from liveDraw, extra care is needed to destruct
if ( GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED )
{
OutputDebug ( L " Exiting liveDraw after snip \n " ) ;
SendMessage ( hWnd , WM_KEYDOWN , VK_ESCAPE , 0 ) ;
}
}
break ;
}
2026-03-26 13:21:43 +01:00
case SNIP_OCR_HOTKEY :
{
OutputDebugStringW ( L " [SnipOCR] Hotkey received \n " ) ;
// Block liveZoom liveDraw snip OCR due to mirroring bug
if ( IsWindowVisible ( g_hWndLiveZoom )
& & ( GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) )
{
break ;
}
bool zoomed = true ;
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateSnipOcr ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
// First, static zoom at 1x
if ( ! g_Zoomed )
{
zoomed = false ;
if ( IsWindowVisible ( g_hWndLiveZoom ) & & ! g_LiveZoomLevelOne )
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , SHALLOW_ZOOM ) ;
}
else
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , LIVE_DRAW_ZOOM ) ;
}
zoomLevel = zoomTelescopeTarget = 1 ;
}
else if ( g_Drawing )
{
SendMessage ( hWnd , WM_USER_EXIT_MODE , 0 , 0 ) ;
if ( g_Drawing )
{
SendMessage ( hWnd , WM_USER_EXIT_MODE , 0 , 0 ) ;
}
}
ShowMainWindow ( hWnd , monInfo , width , height ) ;
// Perform OCR on the selected region
SendMessage ( hWnd , WM_COMMAND , IDC_COPY_OCR , ( zoomed ? 0 : SHALLOW_ZOOM ) ) ;
// Now if we weren't zoomed, unzoom
if ( ! zoomed )
{
if ( g_ZoomOnLiveZoom )
{
ShowCursor ( false ) ;
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
ShowCursor ( true ) ;
}
else
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , SHALLOW_ZOOM ) ;
}
}
// exit zoom
if ( g_Zoomed )
{
if ( GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED )
{
OutputDebug ( L " Exiting liveDraw after snip OCR \n " ) ;
SendMessage ( hWnd , WM_KEYDOWN , VK_ESCAPE , 0 ) ;
}
}
break ;
}
2026-02-03 13:05:31 -08:00
case SAVE_IMAGE_HOTKEY :
SendMessage ( hWnd , WM_COMMAND , IDC_SAVE , 0 ) ;
break ;
case SAVE_CROP_HOTKEY :
SendMessage ( hWnd , WM_COMMAND , IDC_SAVE_CROP , 0 ) ;
break ;
case COPY_IMAGE_HOTKEY :
SendMessage ( hWnd , WM_COMMAND , IDC_COPY , 0 ) ;
break ;
case COPY_CROP_HOTKEY :
SendMessage ( hWnd , WM_COMMAND , IDC_COPY_CROP , 0 ) ;
break ;
2025-01-16 20:52:24 +00:00
case BREAK_HOTKEY :
//
// Go to break timer
//
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( ! g_Zoomed & & ( ! IsWindowVisible ( g_hWndLiveZoom ) | | g_LiveZoomLevelOne ) ) {
# else
if ( ! g_Zoomed & & ! IsWindowVisible ( g_hWndLiveZoom ) ) {
# endif
SendMessage ( hWnd , WM_COMMAND , IDC_BREAK , 0 ) ;
}
break ;
case DEMOTYPE_RESET_HOTKEY :
ResetDemoTypeIndex ( ) ;
break ;
case DEMOTYPE_HOTKEY :
{
//
// Live type
//
switch ( StartDemoType ( g_DemoTypeFile , g_DemoTypeSpeedSlider , g_DemoTypeUserDriven ) )
{
case ERROR_LOADING_FILE :
ErrorDialog ( hWnd , L " Error loading DemoType file " , GetLastError ( ) ) ;
break ;
case NO_FILE_SPECIFIED :
MessageBox ( hWnd , L " No DemoType file specified " , APPNAME , MB_OK ) ;
break ;
case FILE_SIZE_OVERFLOW :
{
std : : wstring msg = L " Unsupported DemoType file size ( "
+ std : : to_wstring ( MAX_INPUT_SIZE ) + L " byte limit) " ;
MessageBox ( hWnd , msg . c_str ( ) , APPNAME , MB_OK ) ;
break ;
}
case UNKNOWN_FILE_DATA :
MessageBox ( hWnd , L " Unrecognized DemoType file content " , APPNAME , MB_OK ) ;
break ;
default :
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateDemoType ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
break ;
}
break ;
}
case LIVE_HOTKEY :
//
// Live zoom
//
OutputDebug ( L " *** LIVE_HOTKEY \n " ) ;
// If LiveZoom and LiveDraw are active then exit both
if ( g_Zoomed & & IsWindowVisible ( g_hWndLiveZoom ) & & ( GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) )
{
SendMessage ( hWnd , WM_KEYDOWN , VK_ESCAPE , 0 ) ;
PostMessage ( hWnd , WM_HOTKEY , LIVE_HOTKEY , 0 ) ;
break ;
}
if ( ! g_Zoomed & & ! g_TimerActive & & ( ! g_fullScreenWorkaround | | ! g_RecordToggle ) ) {
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateLiveZoom ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
if ( g_hWndLiveZoom = = NULL ) {
OutputDebug ( L " Create LIVEZOOM \n " ) ;
g_hWndLiveZoom = CreateWindowEx ( WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TRANSPARENT ,
2025-11-10 11:57:13 -08:00
L " MagnifierClass " , L " ZoomIt Live Zoom " ,
2025-01-16 20:52:24 +00:00
WS_POPUP | WS_CLIPSIBLINGS ,
0 , 0 , 0 , 0 , NULL , NULL , g_hInstance , static_cast < PVOID > ( GetForegroundWindow ( ) ) ) ;
pSetLayeredWindowAttributes ( hWnd , 0 , 0 , LWA_ALPHA ) ;
EnableWindow ( g_hWndLiveZoom , FALSE ) ;
pMagSetWindowFilterList ( g_hWndLiveZoomMag , MW_FILTERMODE_EXCLUDE , 1 , & hWnd ) ;
} else {
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( g_LiveZoomLevelOne ) {
OutputDebug ( L " liveZoom level one \n " ) ;
SendMessage ( g_hWndLiveZoom , WM_USER_SET_ZOOM , static_cast < WPARAM > ( g_LiveZoomLevel ) , 0 ) ;
}
else {
# endif
if ( IsWindowVisible ( g_hWndLiveZoom ) ) {
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( g_RecordToggle )
g_LiveZoomLevel = g_ZoomLevels [ g_SliderZoomLevel ] ;
# endif
// Unzoom
2025-11-10 11:57:13 -08:00
SendMessage ( g_hWndLiveZoom , WM_KEYDOWN , VK_ESCAPE , 0 ) ;
2025-01-16 20:52:24 +00:00
} else {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
OutputDebug ( L " Show liveZoom \n " ) ;
ShowWindow ( g_hWndLiveZoom , SW_SHOW ) ;
}
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
}
# endif
}
2025-10-07 11:20:00 -07:00
OutputDebug ( L " LIVEDRAW SMOOTHING: %d \n " , g_SmoothImage ) ;
if ( ! pMagSetLensUseBitmapSmoothing ( g_hWndLiveZoomMag , g_SmoothImage ) )
{
OutputDebug ( L " MagSetLensUseBitmapSmoothing failed: %d \n " , GetLastError ( ) ) ;
}
2025-01-16 20:52:24 +00:00
if ( g_RecordToggle )
{
g_SelectRectangle . UpdateOwner ( g_hWndLiveZoom ) ;
}
}
break ;
case RECORD_HOTKEY :
case RECORD_CROP_HOTKEY :
case RECORD_WINDOW_HOTKEY :
2025-11-10 11:57:13 -08:00
case RECORD_GIF_HOTKEY :
case RECORD_GIF_WINDOW_HOTKEY :
2025-01-16 20:52:24 +00:00
//
// Recording
// This gets entered twice per recording:
// 1. When the hotkey is pressed to start recording
// 2. When the hotkey is pressed to stop recording
//
if ( g_fullScreenWorkaround & & g_hWndLiveZoom ! = NULL & & IsWindowVisible ( g_hWndLiveZoom ) ! = FALSE )
{
break ;
}
if ( g_RecordCropping = = TRUE )
{
break ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
2026-02-03 13:05:31 -08:00
// Ignore recording hotkey when save dialog is open
if ( g_bSaveInProgress )
{
break ;
}
2025-01-16 20:52:24 +00:00
// Start screen recording
try
{
isCaptureSupported = winrt : : GraphicsCaptureSession : : IsSupported ( ) ;
}
catch ( const winrt : : hresult_error & ) { }
if ( ! isCaptureSupported )
{
MessageBox ( hWnd , L " Screen recording requires Windows 10, May 2019 Update or higher. " , APPNAME , MB_OK ) ;
break ;
}
// If shift, then we're cropping
hWndRecord = 0 ;
if ( wParam = = RECORD_CROP_HOTKEY )
{
if ( g_RecordToggle = = TRUE )
{
// Already recording
break ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
g_RecordCropping = TRUE ;
POINT savedPoint { } ;
RECT savedClip = { } ;
// Handle the cursor for live zoom and static zoom modes.
if ( ( g_hWndLiveZoom ! = nullptr ) | | ( g_Zoomed = = TRUE ) )
{
GetCursorPos ( & savedPoint ) ;
UpdateMonitorInfo ( savedPoint , & monInfo ) ;
}
if ( g_hWndLiveZoom ! = nullptr )
{
// Hide the magnified cursor.
SendMessage ( g_hWndLiveZoom , WM_USER_MAGNIFY_CURSOR , FALSE , 0 ) ;
// Show the system cursor where the magnified was.
g_LiveZoomSourceRect = * reinterpret_cast < RECT * > ( SendMessage ( g_hWndLiveZoom , WM_USER_GET_SOURCE_RECT , 0 , 0 ) ) ;
savedPoint = ScalePointInRects ( savedPoint , g_LiveZoomSourceRect , monInfo . rcMonitor ) ;
SetCursorPos ( savedPoint . x , savedPoint . y ) ;
if ( pMagShowSystemCursor ! = nullptr )
{
pMagShowSystemCursor ( TRUE ) ;
}
}
else if ( ( g_Zoomed = = TRUE ) & & ( g_Drawing = = TRUE ) )
{
// Unclip the cursor.
GetClipCursor ( & savedClip ) ;
ClipCursor ( nullptr ) ;
// Scale the cursor position to the zoomed and move it.
auto point = ScalePointInRects ( savedPoint , boundRc , monInfo . rcMonitor ) ;
SetCursorPos ( point . x , point . y ) ;
}
if ( g_Zoomed = = FALSE )
{
SetWindowPos ( hWnd , HWND_TOPMOST , monInfo . rcMonitor . left , monInfo . rcMonitor . top , width , height , SWP_SHOWWINDOW ) ;
}
// This call blocks with a message loop while cropping.
auto canceled = ! g_SelectRectangle . Start ( ( g_hWndLiveZoom ! = nullptr ) ? g_hWndLiveZoom : hWnd ) ;
g_RecordCropping = FALSE ;
// Restore the cursor if applicable.
if ( g_hWndLiveZoom ! = nullptr )
{
// Hide the system cursor.
if ( pMagShowSystemCursor ! = nullptr )
{
pMagShowSystemCursor ( FALSE ) ;
}
// Show the magnified cursor where the system cursor was.
GetCursorPos ( & savedPoint ) ;
savedPoint = ScalePointInRects ( savedPoint , monInfo . rcMonitor , g_LiveZoomSourceRect ) ;
SetCursorPos ( savedPoint . x , savedPoint . y ) ;
SendMessage ( g_hWndLiveZoom , WM_USER_MAGNIFY_CURSOR , TRUE , 0 ) ;
}
else if ( g_Zoomed = = TRUE )
{
SetCursorPos ( savedPoint . x , savedPoint . y ) ;
if ( g_Drawing = = TRUE )
{
ClipCursor ( & savedClip ) ;
}
}
SetForegroundWindow ( hWnd ) ;
if ( g_Zoomed = = FALSE )
{
SetActiveWindow ( hWnd ) ;
ShowWindow ( hWnd , SW_HIDE ) ;
}
if ( canceled )
{
break ;
}
g_SelectRectangle . UpdateOwner ( ( g_hWndLiveZoom ! = nullptr ) ? g_hWndLiveZoom : hWnd ) ;
cropRc = g_SelectRectangle . SelectedRect ( ) ;
}
else
{
cropRc = { } ;
2025-11-10 11:57:13 -08:00
// if we're recording a window, get the window
if ( wParam = = RECORD_WINDOW_HOTKEY | | wParam = = RECORD_GIF_WINDOW_HOTKEY )
2025-01-16 20:52:24 +00:00
{
GetCursorPos ( & cursorPos ) ;
hWndRecord = WindowFromPoint ( cursorPos ) ;
while ( GetParent ( hWndRecord ) ! = NULL )
{
hWndRecord = GetParent ( hWndRecord ) ;
}
if ( hWndRecord = = GetDesktopWindow ( ) ) {
hWndRecord = NULL ;
}
}
}
if ( g_RecordToggle = = FALSE )
{
g_RecordToggle = TRUE ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateRecord ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
StartRecordingAsync ( hWnd , & cropRc , hWndRecord ) ;
}
else
{
StopRecording ( ) ;
}
break ;
case ZOOM_HOTKEY :
//
// Zoom
//
// Don't react to hotkey while options are open or we're
// saving the screen or live zoom is active
//
if ( hWndOptions ) {
break ;
}
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
OutputDebug ( L " ZOOM HOTKEY: %d \n " , lParam ) ;
if ( g_TimerActive ) {
//
// Finished with break timer
//
if ( g_BreakOnSecondary )
{
EnableDisableSecondaryDisplay ( hWnd , FALSE , & secondaryDevMode ) ;
}
if ( lParam ! = SHALLOW_DESTROY )
{
ShowWindow ( hWnd , SW_HIDE ) ;
if ( g_hBackgroundBmp )
{
DeleteObject ( g_hBackgroundBmp ) ;
DeleteDC ( g_hDcBackgroundFile ) ;
g_hBackgroundBmp = NULL ;
}
}
SetFocus ( GetDesktopWindow ( ) ) ;
KillTimer ( hWnd , 0 ) ;
g_TimerActive = FALSE ;
DeleteObject ( hTimerFont ) ;
DeleteObject ( hNegativeTimerFont ) ;
DeleteDC ( hdcScreen ) ;
DeleteDC ( hdcScreenCompat ) ;
DeleteDC ( hdcScreenSaveCompat ) ;
DeleteDC ( hdcScreenCursorCompat ) ;
DeleteObject ( hbmpCompat ) ;
EnableDisableScreenSaver ( TRUE ) ;
EnableDisableOpacity ( hWnd , FALSE ) ;
} else {
SendMessage ( hWnd , WM_USER_TYPING_OFF , 0 , 0 ) ;
if ( ! g_Zoomed ) {
g_Zoomed = TRUE ;
g_DrawingShape = FALSE ;
OutputDebug ( L " Zoom on \n " ) ;
2026-02-03 13:05:31 -08:00
// Register Ctrl+C and Ctrl+S hotkeys only during static zoom
RegisterHotKey ( hWnd , COPY_IMAGE_HOTKEY , MOD_CONTROL | MOD_NOREPEAT , ' C ' ) ;
RegisterHotKey ( hWnd , COPY_CROP_HOTKEY , MOD_CONTROL | MOD_SHIFT | MOD_NOREPEAT , ' C ' ) ;
RegisterHotKey ( hWnd , SAVE_IMAGE_HOTKEY , MOD_CONTROL | MOD_NOREPEAT , ' S ' ) ;
RegisterHotKey ( hWnd , SAVE_CROP_HOTKEY , MOD_CONTROL | MOD_SHIFT | MOD_NOREPEAT , ' S ' ) ;
2025-01-16 20:52:24 +00:00
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateZoom ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
// Hide the cursor before capturing if in live zoom
if ( g_hWndLiveZoom ! = nullptr )
{
OutputDebug ( L " Hide cursor \n " ) ;
SendMessage ( g_hWndLiveZoom , WM_USER_MAGNIFY_CURSOR , FALSE , 0 ) ;
SendMessage ( g_hWndLiveZoom , WM_TIMER , 0 , 0 ) ;
SendMessage ( g_hWndLiveZoom , WM_USER_MAGNIFY_CURSOR , FALSE , 0 ) ;
}
// Get screen DCs
hdcScreen = CreateDC ( L " DISPLAY " , static_cast < PTCHAR > ( NULL ) ,
static_cast < PTCHAR > ( NULL ) , static_cast < CONST DEVMODE * > ( NULL ) ) ;
2025-11-10 11:57:13 -08:00
hdcScreenCompat = CreateCompatibleDC ( hdcScreen ) ;
hdcScreenSaveCompat = CreateCompatibleDC ( hdcScreen ) ;
hdcScreenCursorCompat = CreateCompatibleDC ( hdcScreen ) ;
2025-01-16 20:52:24 +00:00
// Determine what monitor we're on
GetCursorPos ( & cursorPos ) ;
UpdateMonitorInfo ( cursorPos , & monInfo ) ;
width = monInfo . rcMonitor . right - monInfo . rcMonitor . left ;
height = monInfo . rcMonitor . bottom - monInfo . rcMonitor . top ;
OutputDebug ( L " ZOOM x: %d y: %d width: %d height: %d zoomLevel: %g \n " ,
cursorPos . x , cursorPos . y , width , height , zoomLevel ) ;
// Create display bitmap
bmp . bmBitsPixel = static_cast < BYTE > ( GetDeviceCaps ( hdcScreen , BITSPIXEL ) ) ;
bmp . bmPlanes = static_cast < BYTE > ( GetDeviceCaps ( hdcScreen , PLANES ) ) ;
bmp . bmWidth = width ;
bmp . bmHeight = height ;
2025-11-10 11:57:13 -08:00
bmp . bmWidthBytes = ( ( bmp . bmWidth + 15 ) & ~ 15 ) / 8 ;
hbmpCompat = CreateBitmap ( bmp . bmWidth , bmp . bmHeight ,
2025-01-16 20:52:24 +00:00
bmp . bmPlanes , bmp . bmBitsPixel , static_cast < CONST VOID * > ( NULL ) ) ;
2025-11-10 11:57:13 -08:00
SelectObject ( hdcScreenCompat , hbmpCompat ) ;
2025-01-16 20:52:24 +00:00
// Create saved bitmap
2025-11-10 11:57:13 -08:00
hbmpDrawingCompat = CreateBitmap ( bmp . bmWidth , bmp . bmHeight ,
2025-01-16 20:52:24 +00:00
bmp . bmPlanes , bmp . bmBitsPixel , static_cast < CONST VOID * > ( NULL ) ) ;
SelectObject ( hdcScreenSaveCompat , hbmpDrawingCompat ) ;
// Create cursor save bitmap
// (have to accomodate large fonts and LiveZoom pen scaling)
hbmpCursorCompat = CreateBitmap ( MAX_LIVE_PEN_WIDTH + CURSOR_ARM_LENGTH * 2 ,
MAX_LIVE_PEN_WIDTH + CURSOR_ARM_LENGTH * 2 , bmp . bmPlanes ,
bmp . bmBitsPixel , static_cast < CONST VOID * > ( NULL ) ) ;
SelectObject ( hdcScreenCursorCompat , hbmpCursorCompat ) ;
// Create typing font
g_LogFont . lfHeight = height / 15 ;
if ( g_LogFont . lfHeight < 20 )
g_LogFont . lfQuality = NONANTIALIASED_QUALITY ;
else
g_LogFont . lfQuality = ANTIALIASED_QUALITY ;
hTypingFont = CreateFontIndirect ( & g_LogFont ) ;
SelectObject ( hdcScreenCompat , hTypingFont ) ;
SetTextColor ( hdcScreenCompat , g_PenColor & 0xFFFFFF ) ;
SetBkMode ( hdcScreenCompat , TRANSPARENT ) ;
// Use the screen DC unless recording, because it contains the yellow border
HDC hdcSource = hdcScreen ;
if ( g_RecordToggle ) try {
auto capture = CaptureScreenshot ( winrt : : DirectXPixelFormat : : B8G8R8A8UIntNormalized ) ;
auto bytes = CopyBytesFromTexture ( capture ) ;
D3D11_TEXTURE2D_DESC desc ;
capture - > GetDesc ( & desc ) ;
BITMAPINFO bitmapInfo = { } ;
bitmapInfo . bmiHeader . biSize = sizeof bitmapInfo . bmiHeader ;
bitmapInfo . bmiHeader . biWidth = desc . Width ;
bitmapInfo . bmiHeader . biHeight = - static_cast < LONG > ( desc . Height ) ;
bitmapInfo . bmiHeader . biPlanes = 1 ;
bitmapInfo . bmiHeader . biBitCount = 32 ;
bitmapInfo . bmiHeader . biCompression = BI_RGB ;
void * bits ;
auto dib = CreateDIBSection ( NULL , & bitmapInfo , DIB_RGB_COLORS , & bits , nullptr , 0 ) ;
if ( dib ) {
CopyMemory ( bits , bytes . data ( ) , bytes . size ( ) ) ;
auto hdcCapture = CreateCompatibleDC ( hdcScreen ) ;
SelectObject ( hdcCapture , dib ) ;
hdcSource = hdcCapture ;
}
} catch ( const winrt : : hresult_error & ) { } // on any failure, fall back to the screen DC
bool captured = hdcSource ! = hdcScreen ;
// paint the initial bitmap
BitBlt ( hdcScreenCompat , 0 , 0 , bmp . bmWidth , bmp . bmHeight , hdcSource ,
captured ? 0 : monInfo . rcMonitor . left , captured ? 0 : monInfo . rcMonitor . top , SRCCOPY | CAPTUREBLT ) ;
BitBlt ( hdcScreenSaveCompat , 0 , 0 , bmp . bmWidth , bmp . bmHeight , hdcSource ,
captured ? 0 : monInfo . rcMonitor . left , captured ? 0 : monInfo . rcMonitor . top , SRCCOPY | CAPTUREBLT ) ;
if ( captured )
{
OutputDebug ( L " Captured screen \n " ) ;
auto bitmap = GetCurrentObject ( hdcSource , OBJ_BITMAP ) ;
DeleteObject ( bitmap ) ;
DeleteDC ( hdcSource ) ;
}
// Create drawing pen
hDrawingPen = CreatePen ( PS_SOLID , g_PenWidth , g_PenColor & 0xFFFFFF ) ;
g_BlankedScreen = FALSE ;
g_HaveTyped = FALSE ;
g_Drawing = FALSE ;
g_TypeMode = TypeModeOff ;
g_HaveDrawn = FALSE ;
EnableDisableStickyKeys ( TRUE ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// Go full screen
g_ActiveWindow = GetForegroundWindow ( ) ;
OutputDebug ( L " active window: %x \n " , PtrToLong ( g_ActiveWindow ) ) ;
if ( lParam ! = LIVE_DRAW_ZOOM ) {
OutputDebug ( L " Calling ShowMainWindow \n " ) ;
ShowMainWindow ( hWnd , monInfo , width , height ) ;
}
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// Start telescoping zoom. Lparam is non-zero if this
// was a real hotkey and not the message we send ourself to enter
// unzoomed drawing mode.
//
// Are we switching from live zoom to standard zoom?
//
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( IsWindowVisible ( g_hWndLiveZoom ) & & ! g_LiveZoomLevelOne ) {
# else
if ( IsWindowVisible ( g_hWndLiveZoom ) ) {
# endif
// Enter drawing mode
OutputDebug ( L " Enter liveZoom draw \n " ) ;
g_LiveZoomSourceRect = * reinterpret_cast < RECT * > ( SendMessage ( g_hWndLiveZoom , WM_USER_GET_SOURCE_RECT , 0 , 0 ) ) ;
g_LiveZoomLevel = * reinterpret_cast < float * > ( SendMessage ( g_hWndLiveZoom , WM_USER_GET_ZOOM_LEVEL , 0 , 0 ) ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// Set live zoom level to 1 in preparation of us being full screen static
zoomLevel = 1.0 ;
zoomTelescopeTarget = 1.0 ;
if ( lParam ! = LIVE_DRAW_ZOOM ) {
g_ZoomOnLiveZoom = TRUE ;
}
UpdateWindow ( hWnd ) ; // overwrites where cursor erased
if ( lParam ! = SHALLOW_ZOOM )
{
// Put the drawing cursor where the magnified cursor was
OutputDebug ( L " Setting cursor \n " ) ;
if ( lParam ! = LIVE_DRAW_ZOOM )
{
cursorPos = ScalePointInRects ( cursorPos , g_LiveZoomSourceRect , monInfo . rcMonitor ) ;
SetCursorPos ( cursorPos . x , cursorPos . y ) ;
UpdateWindow ( hWnd ) ; // overwrites where cursor erased
SendMessage ( hWnd , WM_LBUTTONDOWN , 0 , MAKELPARAM ( cursorPos . x , cursorPos . y ) ) ;
}
}
else
{
InvalidateRect ( hWnd , NULL , FALSE ) ;
}
UpdateWindow ( hWnd ) ;
if ( g_RecordToggle )
{
g_SelectRectangle . UpdateOwner ( hWnd ) ;
}
if ( lParam ! = LIVE_DRAW_ZOOM ) {
OutputDebug ( L " Calling ShowMainWindow 2 \n " ) ;
ShowWindow ( g_hWndLiveZoom , SW_HIDE ) ;
}
} else if ( lParam ! = 0 & & lParam ! = LIVE_DRAW_ZOOM ) {
zoomTelescopeStep = ZOOM_LEVEL_STEP_IN ;
zoomTelescopeTarget = g_ZoomLevels [ g_SliderZoomLevel ] ;
2025-11-10 11:57:13 -08:00
if ( g_AnimateZoom )
2025-11-11 16:42:59 +01:00
{
2025-11-10 11:57:13 -08:00
zoomLevel = static_cast < float > ( 1.0 ) * zoomTelescopeStep ;
2025-11-11 16:42:59 +01:00
g_TelescopingZoomLastTick = GetTickCount64 ( ) ;
}
2025-01-16 20:52:24 +00:00
else
zoomLevel = zoomTelescopeTarget ;
SetTimer ( hWnd , 1 , ZOOM_LEVEL_STEP_TIME , NULL ) ;
}
} else {
OutputDebug ( L " Zoom off: don't animate=%d \n " , lParam ) ;
// turn off liveDraw
SetLayeredWindowAttributes ( hWnd , 0 , 255 , LWA_ALPHA ) ;
if ( lParam ! = SHALLOW_DESTROY & & ! g_ZoomOnLiveZoom & & g_AnimateZoom & &
g_TelescopeZoomOut & & zoomTelescopeTarget ! = 1 ) {
2025-11-10 11:57:13 -08:00
// Start telescoping zoom.
2025-01-16 20:52:24 +00:00
zoomTelescopeStep = ZOOM_LEVEL_STEP_OUT ;
zoomTelescopeTarget = 1.0 ;
2025-11-11 16:42:59 +01:00
g_TelescopingZoomLastTick = GetTickCount64 ( ) ;
2025-01-16 20:52:24 +00:00
SetTimer ( hWnd , 2 , ZOOM_LEVEL_STEP_TIME , NULL ) ;
} else {
// Simulate timer expiration
zoomTelescopeStep = 0 ;
zoomTelescopeTarget = zoomLevel = 1.0 ;
SendMessage ( hWnd , WM_TIMER , 2 , lParam ) ;
}
}
}
break ;
}
return TRUE ;
case WM_POINTERUPDATE : {
penInverted = IsPenInverted ( wParam ) ;
OutputDebug ( L " WM_POINTERUPDATE: contact: %d button down: %d X: %d Y: %d \n " ,
IS_POINTER_INCONTACT_WPARAM ( wParam ) ,
penInverted ,
GET_X_LPARAM ( lParam ) ,
GET_Y_LPARAM ( lParam ) ) ;
if ( penInverted ! = g_PenInverted ) {
g_PenInverted = penInverted ;
if ( g_PenInverted ) {
if ( PopDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ) {
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
}
}
} else if ( g_PenDown & & ! penInverted ) {
SendPenMessage ( hWnd , WM_MOUSEMOVE , lParam ) ;
}
}
return TRUE ;
case WM_POINTERUP :
OutputDebug ( L " WM_POINTERUP \n " ) ;
penInverted = IsPenInverted ( wParam ) ;
if ( ! penInverted ) {
SendPenMessage ( hWnd , WM_LBUTTONUP , lParam ) ;
SendPenMessage ( hWnd , WM_RBUTTONDOWN , lParam ) ;
g_PenDown = FALSE ;
}
break ;
2025-11-10 11:57:13 -08:00
case WM_POINTERDOWN :
2025-01-16 20:52:24 +00:00
OutputDebug ( L " WM_POINTERDOWN \n " ) ;
penInverted = IsPenInverted ( wParam ) ;
if ( ! penInverted ) {
g_PenDown = TRUE ;
// Enter drawing mode
SendPenMessage ( hWnd , WM_LBUTTONDOWN , lParam ) ;
SendPenMessage ( hWnd , WM_MOUSEMOVE , lParam ) ;
SendPenMessage ( hWnd , WM_LBUTTONUP , lParam ) ;
SendPenMessage ( hWnd , WM_MOUSEMOVE , lParam ) ;
PopDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ;
// Enter tracing mode
SendPenMessage ( hWnd , WM_LBUTTONDOWN , lParam ) ;
}
break ;
case WM_KILLFOCUS :
if ( ( g_RecordCropping = = FALSE ) & & g_Zoomed & & ! g_bSaveInProgress ) {
// Turn off zoom if not in liveDraw
DWORD layeringFlag ;
GetLayeredWindowAttributes ( hWnd , NULL , NULL , & layeringFlag ) ;
if ( ! ( layeringFlag & LWA_COLORKEY ) ) {
PostMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
}
}
break ;
case WM_MOUSEWHEEL :
//
// Zoom or modify break timer
//
2025-11-10 11:57:13 -08:00
if ( GET_WHEEL_DELTA_WPARAM ( wParam ) < 0 )
2025-01-16 20:52:24 +00:00
wParam - = ( WHEEL_DELTA - 1 ) < < 16 ;
2025-11-10 11:57:13 -08:00
else
2025-01-16 20:52:24 +00:00
wParam + = ( WHEEL_DELTA - 1 ) < < 16 ;
delta = GET_WHEEL_DELTA_WPARAM ( wParam ) / WHEEL_DELTA ;
2025-11-10 11:57:13 -08:00
OutputDebug ( L " mousewheel: wParam: %d delta: %d \n " ,
2025-01-16 20:52:24 +00:00
GET_WHEEL_DELTA_WPARAM ( wParam ) , delta ) ;
if ( g_Zoomed ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
if ( g_TypeMode = = TypeModeOff ) {
if ( g_Drawing & & ( LOWORD ( wParam ) & MK_CONTROL ) ) {
ResizePen ( hWnd , hdcScreenCompat , hdcScreenCursorCompat , prevPt ,
g_Tracing , & g_Drawing , g_LiveZoomLevel , TRUE , g_PenWidth + delta ) ;
// Perform static zoom unless in liveDraw
} else if ( ! ( GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) ) {
if ( delta > 0 ) zoomIn = TRUE ;
else {
zoomIn = FALSE ;
delta = - delta ;
}
while ( delta - - ) {
if ( zoomIn ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
if ( zoomTelescopeTarget < ZOOM_LEVEL_MAX ) {
if ( zoomTelescopeTarget < 2 ) {
zoomTelescopeTarget = 2 ;
} else {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// Start telescoping zoom
2025-11-10 11:57:13 -08:00
zoomTelescopeTarget = zoomTelescopeTarget * 2 ;
2025-01-16 20:52:24 +00:00
}
2025-11-10 11:57:13 -08:00
zoomTelescopeStep = ZOOM_LEVEL_STEP_IN ;
if ( g_AnimateZoom )
zoomLevel * = zoomTelescopeStep ;
2025-01-16 20:52:24 +00:00
else
zoomLevel = zoomTelescopeTarget ;
2025-11-10 11:57:13 -08:00
if ( zoomLevel > zoomTelescopeTarget )
2025-01-16 20:52:24 +00:00
zoomLevel = zoomTelescopeTarget ;
else
SetTimer ( hWnd , 1 , ZOOM_LEVEL_STEP_TIME , NULL ) ;
}
} else if ( zoomTelescopeTarget > ZOOM_LEVEL_MIN ) {
// Let them more gradually zoom out from 2x to 1x
if ( zoomTelescopeTarget < = 2 ) {
2025-11-10 11:57:13 -08:00
zoomTelescopeTarget * = .75 ;
if ( zoomTelescopeTarget < ZOOM_LEVEL_MIN )
2025-01-16 20:52:24 +00:00
zoomTelescopeTarget = ZOOM_LEVEL_MIN ;
} else {
2025-11-10 11:57:13 -08:00
zoomTelescopeTarget = zoomTelescopeTarget / 2 ;
2025-01-16 20:52:24 +00:00
}
2025-11-10 11:57:13 -08:00
zoomTelescopeStep = ZOOM_LEVEL_STEP_OUT ;
if ( g_AnimateZoom )
zoomLevel * = zoomTelescopeStep ;
2025-01-16 20:52:24 +00:00
else
zoomLevel = zoomTelescopeTarget ;
if ( zoomLevel < zoomTelescopeTarget )
{
zoomLevel = zoomTelescopeTarget ;
// Force update on final step out
InvalidateRect ( hWnd , NULL , FALSE ) ;
}
else
{
SetTimer ( hWnd , 1 , ZOOM_LEVEL_STEP_TIME , NULL ) ;
}
}
}
if ( zoomLevel ! = zoomTelescopeTarget ) {
if ( g_Drawing ) {
if ( ! g_Tracing ) {
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
}
2025-11-10 11:57:13 -08:00
//SetCursorPos( monInfo.rcMonitor.left + cursorPos.x,
2025-01-16 20:52:24 +00:00
// monInfo.rcMonitor.top + cursorPos.y );
}
InvalidateRect ( hWnd , NULL , FALSE ) ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
}
} else {
// Resize the text font
if ( ( delta > 0 & & g_FontScale > - 20 ) | | ( delta < 0 & & g_FontScale < 50 ) ) {
ClearTypingCursor ( hdcScreenCompat , hdcScreenCursorCompat , cursorRc , g_BlankedScreen ) ;
g_FontScale - = delta ;
if ( g_FontScale = = 0 ) g_FontScale = 1 ;
// Set lParam to 0 as part of message to keyup hander
DeleteObject ( hTypingFont ) ;
g_LogFont . lfHeight = max ( ( int ) ( height / zoomLevel ) / g_FontScale , 12 ) ;
2025-11-10 11:57:13 -08:00
if ( g_LogFont . lfHeight < 20 )
2025-01-16 20:52:24 +00:00
g_LogFont . lfQuality = NONANTIALIASED_QUALITY ;
else
g_LogFont . lfQuality = ANTIALIASED_QUALITY ;
hTypingFont = CreateFontIndirect ( & g_LogFont ) ;
SelectObject ( hdcScreenCompat , hTypingFont ) ;
DrawTypingCursor ( hWnd , & textPt , hdcScreenCompat , hdcScreenCursorCompat , & cursorRc ) ;
}
}
} else if ( g_TimerActive & & ( breakTimeout > 0 | | delta ) ) {
if ( delta ) {
if ( breakTimeout < 0 ) breakTimeout = 0 ;
if ( breakTimeout % 60 ) {
breakTimeout + = ( 60 - breakTimeout % 60 ) ;
delta - - ;
}
breakTimeout + = delta * 60 ;
} else {
if ( breakTimeout % 60 ) {
breakTimeout - = breakTimeout % 60 ;
delta - - ;
}
breakTimeout - = delta * 60 ;
}
if ( breakTimeout < 0 ) breakTimeout = 0 ;
KillTimer ( hWnd , 0 ) ;
SetTimer ( hWnd , 0 , 1000 , NULL ) ;
InvalidateRect ( hWnd , NULL , TRUE ) ;
}
if ( zoomLevel ! = 1 & & g_Drawing ) {
// Constrain the mouse to the visible region
boundRc = BoundMouse ( zoomTelescopeTarget , & monInfo , width , height , & cursorPos ) ;
} else {
ClipCursor ( NULL ) ;
}
return TRUE ;
case WM_IME_CHAR :
case WM_CHAR :
[ZoomIt] Fix ampersand typing bug and debug assertion failure (#43679)
## Summary of the Pull Request
This PR fixes two typing-related issues in ZoomIt:
1. Ampersands could be typed even when Type Mode or Draw Mode were not
engaged
2. On Debug builds, typing a non-alphanumeric character in Type Mode
would crash ZoomIt with a CRT assertion failure
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43676
- [x] Closes: #43677
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
## Detailed Description of the Pull Request / Additional comments
### Assertion failure on Debug builds
**Root Cause**
This occurred because of a combination of a type-coercion issue and the
use of `isprint()` in `WM_KEYDOWN`, which operates on virtual key codes,
not characters.
This is the code with the fault:
```cpp
if( (g_TypeMode != TypeModeOff) && g_HaveTyped && static_cast<char>(wParam) != VK_UP && static_cast<char>(wParam) != VK_DOWN &&
(isprint( static_cast<char>(wParam)) ||
wParam == VK_RETURN || wParam == VK_DELETE || wParam == VK_BACK )) {
```
There are a few issues here:
1. There is no need for the `VK_UP` / `VK_DOWN` check. The block only
executes if `VK_RETURN`, `VK_DELETE` or `VK_BACK` are pressed, which
cannot be `VK_UP` or `VK_DOWN` by definition. This should be removed.
2. Casting the `wParam` to `char` means casting an unsigned int value to
a signed char. This works for alphanumeric characters, as the VK_-codes
correspond to their char counterparts. But it fails for values with
their high bit set, e.g. a hyphen:
- The virtual key code for the hyphen key is `VK_OEM_MINUS`, or `0xBD`
- `0xBD` (10111101) becomes `-67` when cast to a char
- In Debug builds, a call to `isprint()` includes a range check to
ensure the value is in the range 1 to 255. A negative value trips this
assertion.
3. The casts are not needed.
**Fix**
Remove both the `isprint()` call (the WM_CHAR handler has an
`iswprint()` check) and remove the check against `VK_UP` and `VK_DOWN`.
### Ampersand issue
This is a simple operator precedence issue with this statement:
```cpp
if ((g_TypeMode != TypeModeOff) && iswprint(static_cast<TCHAR>(wParam)) ||
(static_cast<TCHAR>(wParam) == L'&'))
```
The intention is to continue if one of the Type Modes is engaged (either
left-to-right or right-to-left) and either the typed character is
printable or (a special-case) the ampersand (presumably for legacy
issues when `DT_NOPREFIX` was not present on all draw text calls).
Unfortunately, the parentheses are placed incorrectly, resulting in the
expression actually being:
`if (Type Mode is active AND a printable character was pressed) OR
(Ampersand was pressed)`
(Meaning the code will always execute if ampersand is pressed regardless
of the mode.)
**Fix**
Correcting the placement of the parentheses fixes the issue.
Note: I think `DT_NOPREFIX` exists on all `DrawText()` calls which
render characters, so we could potentially remove the ampersand check
entirely in the future, assuming that was the original issue which
required the special casing.
## Validation Steps Performed
- Ensure ampersand does not result in the character appearing and/or
glitches occurring where the cursor is when Type Mode or Draw Mode are
not active.
- Ensure ampersands may still be typed as normal in Type Mode.
- Confirm that non-alphanumeric characters can be typed without issue in
Type Mode on both Debug and Release builds.
- Test draw operations in combination with text notes.
- Test backspace, return and delete keys in Type Mode.
- Test that Type Mode engages repeatedly and can be exited.
2026-02-13 11:29:38 +00:00
if ( ( g_TypeMode ! = TypeModeOff ) & &
( iswprint ( static_cast < TCHAR > ( wParam ) ) | | ( static_cast < TCHAR > ( wParam ) = = L ' & ' ) ) ) {
2025-01-16 20:52:24 +00:00
g_HaveTyped = TRUE ;
TCHAR vKey = static_cast < TCHAR > ( wParam ) ;
g_HaveDrawn = TRUE ;
// Clear typing cursor
rc . left = textPt . x ;
rc . top = textPt . y ;
ClearTypingCursor ( hdcScreenCompat , hdcScreenCursorCompat , cursorRc , g_BlankedScreen ) ;
if ( g_TypeMode = = TypeModeRightJustify ) {
if ( ! g_TextBuffer . empty ( ) | | ! g_TextBufferPreviousLines . empty ( ) ) {
PopDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ; //***
}
PushDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ;
// Restore previous lines.
wParam = ' X ' ;
DrawText ( hdcScreenCompat , reinterpret_cast < PTCHAR > ( & wParam ) , 1 , & rc , DT_CALCRECT ) ;
const auto lineHeight = rc . bottom - rc . top ;
rc . top - = static_cast < LONG > ( g_TextBufferPreviousLines . size ( ) ) * lineHeight ;
// Draw the current character on the current line.
g_TextBuffer + = vKey ;
drawAllRightJustifiedLines ( lineHeight ) ;
}
else {
DrawText ( hdcScreenCompat , & vKey , 1 , & rc , DT_CALCRECT | DT_NOPREFIX ) ;
DrawText ( hdcScreenCompat , & vKey , 1 , & rc , DT_LEFT | DT_NOPREFIX ) ;
textPt . x + = rc . right - rc . left ;
}
InvalidateRect ( hWnd , NULL , TRUE ) ;
// Save the key for undo
P_TYPED_KEY newKey = static_cast < P_TYPED_KEY > ( malloc ( sizeof ( TYPED_KEY ) ) ) ;
newKey - > rc = rc ;
newKey - > Next = typedKeyList ;
typedKeyList = newKey ;
// Draw the typing cursor
DrawTypingCursor ( hWnd , & textPt , hdcScreenCompat , hdcScreenCursorCompat , & cursorRc ) ;
return FALSE ;
}
break ;
case WM_KEYUP :
if ( wParam = = ' T ' & & ( g_TypeMode = = TypeModeOff ) ) {
// lParam is 0 when we're resizing the font and so don't have a cursor that
// we need to restore
if ( ! g_Drawing & & lParam = = 0 ) {
OutputDebug ( L " Entering typing mode and resetting cursor position \n " ) ;
SendMessage ( hWnd , WM_LBUTTONDOWN , 0 , MAKELPARAM ( cursorPos . x , cursorPos . y ) ) ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
// Do they want to right-justify text?
OutputDebug ( L " Keyup Shift: %x \n " , GetAsyncKeyState ( VK_SHIFT ) ) ;
if ( GetAsyncKeyState ( VK_SHIFT ) ! = 0 ) {
g_TypeMode = TypeModeRightJustify ;
g_TextBuffer . clear ( ) ;
// Also empty all previous lines
g_TextBufferPreviousLines = { } ;
}
else {
g_TypeMode = TypeModeLeftJustify ;
}
textStartPt = cursorPos ;
textPt = prevPt ;
g_HaveTyped = FALSE ;
// Get a font of a decent size
DeleteObject ( hTypingFont ) ;
g_LogFont . lfHeight = max ( ( int ) ( height / zoomLevel ) / g_FontScale , 12 ) ;
if ( g_LogFont . lfHeight < 20 )
g_LogFont . lfQuality = NONANTIALIASED_QUALITY ;
else
g_LogFont . lfQuality = ANTIALIASED_QUALITY ;
hTypingFont = CreateFontIndirect ( & g_LogFont ) ;
SelectObject ( hdcScreenCompat , hTypingFont ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// If lparam == 0 that means that we sent the message as part of a font resize
if ( g_Drawing & & lParam ! = 0 ) {
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
PushDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else if ( ! g_Drawing ) {
textPt = cursorPos ;
}
// Draw the typing cursor
DrawTypingCursor ( hWnd , & textPt , hdcScreenCompat , hdcScreenCursorCompat , & cursorRc , true ) ;
prevPt = textPt ;
}
break ;
case WM_KEYDOWN :
[ZoomIt] Fix ampersand typing bug and debug assertion failure (#43679)
## Summary of the Pull Request
This PR fixes two typing-related issues in ZoomIt:
1. Ampersands could be typed even when Type Mode or Draw Mode were not
engaged
2. On Debug builds, typing a non-alphanumeric character in Type Mode
would crash ZoomIt with a CRT assertion failure
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43676
- [x] Closes: #43677
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
## Detailed Description of the Pull Request / Additional comments
### Assertion failure on Debug builds
**Root Cause**
This occurred because of a combination of a type-coercion issue and the
use of `isprint()` in `WM_KEYDOWN`, which operates on virtual key codes,
not characters.
This is the code with the fault:
```cpp
if( (g_TypeMode != TypeModeOff) && g_HaveTyped && static_cast<char>(wParam) != VK_UP && static_cast<char>(wParam) != VK_DOWN &&
(isprint( static_cast<char>(wParam)) ||
wParam == VK_RETURN || wParam == VK_DELETE || wParam == VK_BACK )) {
```
There are a few issues here:
1. There is no need for the `VK_UP` / `VK_DOWN` check. The block only
executes if `VK_RETURN`, `VK_DELETE` or `VK_BACK` are pressed, which
cannot be `VK_UP` or `VK_DOWN` by definition. This should be removed.
2. Casting the `wParam` to `char` means casting an unsigned int value to
a signed char. This works for alphanumeric characters, as the VK_-codes
correspond to their char counterparts. But it fails for values with
their high bit set, e.g. a hyphen:
- The virtual key code for the hyphen key is `VK_OEM_MINUS`, or `0xBD`
- `0xBD` (10111101) becomes `-67` when cast to a char
- In Debug builds, a call to `isprint()` includes a range check to
ensure the value is in the range 1 to 255. A negative value trips this
assertion.
3. The casts are not needed.
**Fix**
Remove both the `isprint()` call (the WM_CHAR handler has an
`iswprint()` check) and remove the check against `VK_UP` and `VK_DOWN`.
### Ampersand issue
This is a simple operator precedence issue with this statement:
```cpp
if ((g_TypeMode != TypeModeOff) && iswprint(static_cast<TCHAR>(wParam)) ||
(static_cast<TCHAR>(wParam) == L'&'))
```
The intention is to continue if one of the Type Modes is engaged (either
left-to-right or right-to-left) and either the typed character is
printable or (a special-case) the ampersand (presumably for legacy
issues when `DT_NOPREFIX` was not present on all draw text calls).
Unfortunately, the parentheses are placed incorrectly, resulting in the
expression actually being:
`if (Type Mode is active AND a printable character was pressed) OR
(Ampersand was pressed)`
(Meaning the code will always execute if ampersand is pressed regardless
of the mode.)
**Fix**
Correcting the placement of the parentheses fixes the issue.
Note: I think `DT_NOPREFIX` exists on all `DrawText()` calls which
render characters, so we could potentially remove the ampersand check
entirely in the future, assuming that was the original issue which
required the special casing.
## Validation Steps Performed
- Ensure ampersand does not result in the character appearing and/or
glitches occurring where the cursor is when Type Mode or Draw Mode are
not active.
- Ensure ampersands may still be typed as normal in Type Mode.
- Confirm that non-alphanumeric characters can be typed without issue in
Type Mode on both Debug and Release builds.
- Test draw operations in combination with text notes.
- Test backspace, return and delete keys in Type Mode.
- Test that Type Mode engages repeatedly and can be exited.
2026-02-13 11:29:38 +00:00
if ( ( g_TypeMode ! = TypeModeOff ) & & g_HaveTyped & &
( wParam = = VK_RETURN | | wParam = = VK_DELETE | | wParam = = VK_BACK ) ) {
2025-01-16 20:52:24 +00:00
if ( wParam = = VK_RETURN ) {
// Clear the typing cursor
ClearTypingCursor ( hdcScreenCompat , hdcScreenCursorCompat , cursorRc , g_BlankedScreen ) ;
if ( g_TypeMode = = TypeModeRightJustify )
{
g_TextBufferPreviousLines . push_back ( g_TextBuffer ) ;
g_TextBuffer . clear ( ) ;
}
else
{
// Insert a fake return key in the list to undo.
P_TYPED_KEY newKey = static_cast < P_TYPED_KEY > ( malloc ( sizeof ( TYPED_KEY ) ) ) ;
newKey - > rc . left = textPt . x ;
newKey - > rc . top = textPt . y ;
newKey - > rc . right = newKey - > rc . left ;
newKey - > rc . bottom = newKey - > rc . top ;
newKey - > Next = typedKeyList ;
typedKeyList = newKey ;
}
wParam = ' X ' ;
DrawText ( hdcScreenCompat , reinterpret_cast < PTCHAR > ( & wParam ) , 1 , & rc , DT_CALCRECT ) ;
textPt . x = prevPt . x ; // + g_PenWidth;
textPt . y + = rc . bottom - rc . top ;
// Draw the typing cursor
DrawTypingCursor ( hWnd , & textPt , hdcScreenCompat , hdcScreenCursorCompat , & cursorRc ) ;
} else if ( wParam = = VK_DELETE | | wParam = = VK_BACK ) {
P_TYPED_KEY deletedKey = typedKeyList ;
if ( deletedKey ) {
// Clear the typing cursor
ClearTypingCursor ( hdcScreenCompat , hdcScreenCursorCompat , cursorRc , g_BlankedScreen ) ;
if ( g_TypeMode = = TypeModeRightJustify ) {
if ( ! g_TextBuffer . empty ( ) | | ! g_TextBufferPreviousLines . empty ( ) ) {
PopDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ;
}
PushDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ;
rc . left = textPt . x ;
rc . top = textPt . y ;
// Restore the previous lines.
wParam = ' X ' ;
DrawText ( hdcScreenCompat , reinterpret_cast < PTCHAR > ( & wParam ) , 1 , & rc , DT_CALCRECT ) ;
const auto lineHeight = rc . bottom - rc . top ;
const bool lineWasEmpty = g_TextBuffer . empty ( ) ;
drawAllRightJustifiedLines ( lineHeight , true ) ;
if ( lineWasEmpty & & ! g_TextBufferPreviousLines . empty ( ) )
{
g_TextBuffer = g_TextBufferPreviousLines . back ( ) ;
g_TextBufferPreviousLines . pop_back ( ) ;
textPt . y - = lineHeight ;
}
}
else {
RECT rect = deletedKey - > rc ;
if ( g_BlankedScreen ) {
BlankScreenArea ( hdcScreenCompat , & rect , g_BlankedScreen ) ;
}
else {
BitBlt ( hdcScreenCompat , rect . left , rect . top , rect . right - rect . left ,
rect . bottom - rect . top , hdcScreenSaveCompat , rect . left , rect . top , SRCCOPY | CAPTUREBLT ) ;
}
InvalidateRect ( hWnd , NULL , FALSE ) ;
textPt . x = rect . left ;
textPt . y = rect . top ;
typedKeyList = deletedKey - > Next ;
free ( deletedKey ) ;
// Refresh cursor if we deleted the last key
if ( typedKeyList = = NULL ) {
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( prevPt . x , prevPt . y ) ) ;
}
}
DrawTypingCursor ( hWnd , & textPt , hdcScreenCompat , hdcScreenCursorCompat , & cursorRc ) ;
}
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
break ;
}
2025-11-10 11:57:13 -08:00
switch ( wParam ) {
2025-01-16 20:52:24 +00:00
case ' R ' :
case ' B ' :
case ' Y ' :
case ' O ' :
case ' G ' :
case ' X ' :
case ' P ' :
if ( ( g_Zoomed | | g_TimerActive ) & & ( g_TypeMode = = TypeModeOff ) ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
PDWORD penColor ;
if ( g_TimerActive )
penColor = & g_BreakPenColor ;
else
penColor = & g_PenColor ;
if ( wParam = = ' R ' ) * penColor = COLOR_RED ;
else if ( wParam = = ' G ' ) * penColor = COLOR_GREEN ;
else if ( wParam = = ' B ' ) * penColor = COLOR_BLUE ;
else if ( wParam = = ' Y ' ) * penColor = COLOR_YELLOW ;
else if ( wParam = = ' O ' ) * penColor = COLOR_ORANGE ;
else if ( wParam = = ' P ' ) * penColor = COLOR_PINK ;
else if ( wParam = = ' X ' )
{
if ( GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED )
{
// Blur is not supported in LiveDraw
break ;
}
* penColor = COLOR_BLUR ;
}
bool shift = GetKeyState ( VK_SHIFT ) & 0x8000 ;
if ( shift & & ( GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) )
{
// Highlight is not supported in LiveDraw
break ;
}
reg . WriteRegSettings ( RegSettings ) ;
DeleteObject ( hDrawingPen ) ;
SetTextColor ( hdcScreenCompat , * penColor ) ;
// Highlight and blur level
if ( shift & & * penColor ! = COLOR_BLUR )
{
* penColor | = ( g_AlphaBlend < < 24 ) ;
}
else
{
if ( * penColor = = COLOR_BLUR )
{
g_BlurRadius = shift ? STRONG_BLUR_RADIUS : NORMAL_BLUR_RADIUS ;
}
* penColor | = ( 0xFF < < 24 ) ;
}
hDrawingPen = CreatePen ( PS_SOLID , g_PenWidth , * penColor & 0xFFFFFF ) ;
SelectObject ( hdcScreenCompat , hDrawingPen ) ;
if ( g_Drawing ) {
2025-11-10 11:57:13 -08:00
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( prevPt . x , prevPt . y ) ) ;
2025-01-16 20:52:24 +00:00
} else if ( g_TimerActive ) {
2025-11-10 11:57:13 -08:00
InvalidateRect ( hWnd , NULL , FALSE ) ;
2025-01-16 20:52:24 +00:00
} else if ( g_TypeMode ! = TypeModeOff ) {
ClearTypingCursor ( hdcScreenCompat , hdcScreenCursorCompat , cursorRc , g_BlankedScreen ) ;
DrawTypingCursor ( hWnd , & textPt , hdcScreenCompat , hdcScreenCursorCompat , & cursorRc ) ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
}
}
break ;
case ' Z ' :
if ( ( GetKeyState ( VK_CONTROL ) & 0x8000 ) & & g_HaveDrawn & & ! g_Tracing ) {
if ( PopDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
if ( g_Drawing ) {
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
2025-11-10 11:57:13 -08:00
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( prevPt . x , prevPt . y ) ) ;
2025-01-16 20:52:24 +00:00
}
else {
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
}
InvalidateRect ( hWnd , NULL , FALSE ) ;
}
}
break ;
case VK_SPACE :
if ( g_Drawing & & ! g_Tracing ) {
SetCursorPos ( boundRc . left + ( boundRc . right - boundRc . left ) / 2 ,
boundRc . top + ( boundRc . bottom - boundRc . top ) / 2 ) ;
2025-11-10 11:57:13 -08:00
SendMessage ( hWnd , WM_MOUSEMOVE , 0 ,
2025-01-16 20:52:24 +00:00
MAKELPARAM ( ( boundRc . right - boundRc . left ) / 2 ,
( boundRc . bottom - boundRc . top ) / 2 ) ) ;
}
break ;
case ' W ' :
case ' K ' :
2026-03-26 13:21:43 +01:00
// Break timer: change background color
if ( g_TimerActive )
{
g_BreakBackgroundColor = ( wParam = = ' K ' ) ? 1 : 0 ;
reg . WriteRegSettings ( RegSettings ) ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
break ;
}
2025-01-16 20:52:24 +00:00
// Block user-driven sketch pad in liveDraw
if ( lParam ! = LIVE_DRAW_ZOOM
& & ( GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) )
{
break ;
}
// Don't allow screen blanking while we've got the typing cursor active
// because we don't really handle going from white to black.
if ( g_Zoomed & & ( g_TypeMode = = TypeModeOff ) ) {
if ( ! g_Drawing ) {
SendMessage ( hWnd , WM_LBUTTONDOWN , 0 , MAKELPARAM ( cursorPos . x , cursorPos . y ) ) ;
}
// Restore area where cursor was previously
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
PushDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ;
g_BlankedScreen = static_cast < int > ( wParam ) ;
rc . top = rc . left = 0 ;
rc . bottom = height ;
rc . right = width ;
BlankScreenArea ( hdcScreenCompat , & rc , g_BlankedScreen ) ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
// Save area that's going to be occupied by new cursor position
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
2025-11-10 11:57:13 -08:00
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( prevPt . x , prevPt . y ) ) ;
2025-01-16 20:52:24 +00:00
}
break ;
case ' E ' :
// Don't allow erase while we have the typing cursor active
if ( g_HaveDrawn & & ( g_TypeMode = = TypeModeOff ) ) {
DeleteDrawUndoList ( & drawUndoList ) ;
g_HaveDrawn = FALSE ;
OutputDebug ( L " Erase \n " ) ;
if ( GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) {
SendMessage ( hWnd , WM_KEYDOWN , ' K ' , 0 ) ;
}
else {
BitBlt ( hdcScreenCompat , 0 , 0 , bmp . bmWidth ,
bmp . bmHeight , hdcScreenSaveCompat , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
if ( g_Drawing ) {
OutputDebug ( L " Erase: draw cursor \n " ) ;
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
DrawCursor ( hdcScreenCompat , prevPt , zoomLevel , width , height ) ;
g_HaveDrawn = TRUE ;
}
}
InvalidateRect ( hWnd , NULL , FALSE ) ;
g_BlankedScreen = FALSE ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
break ;
case VK_UP :
2025-11-10 11:57:13 -08:00
SendMessage ( hWnd , WM_MOUSEWHEEL ,
MAKEWPARAM ( GetAsyncKeyState ( VK_LCONTROL ) ! = 0 | | GetAsyncKeyState ( VK_RCONTROL ) ! = 0 ?
2025-01-16 20:52:24 +00:00
MK_CONTROL : 0 , WHEEL_DELTA ) , 0 ) ;
return TRUE ;
case VK_DOWN :
2025-11-10 11:57:13 -08:00
SendMessage ( hWnd , WM_MOUSEWHEEL ,
MAKEWPARAM ( GetAsyncKeyState ( VK_LCONTROL ) ! = 0 | | GetAsyncKeyState ( VK_RCONTROL ) ! = 0 ?
2025-01-16 20:52:24 +00:00
MK_CONTROL : 0 , - WHEEL_DELTA ) , 0 ) ;
return TRUE ;
case VK_LEFT :
case VK_RIGHT :
if ( wParam = = VK_RIGHT ) delta = 10 ;
else delta = - 10 ;
if ( g_TimerActive & & ( breakTimeout > 0 | | delta ) ) {
if ( breakTimeout < 0 ) breakTimeout = 0 ;
breakTimeout + = delta ;
breakTimeout - = ( breakTimeout % 10 ) ;
if ( breakTimeout < 0 ) breakTimeout = 0 ;
KillTimer ( hWnd , 0 ) ;
SetTimer ( hWnd , 0 , 1000 , NULL ) ;
InvalidateRect ( hWnd , NULL , TRUE ) ;
}
break ;
2025-11-10 11:57:13 -08:00
case VK_ESCAPE :
2025-01-16 20:52:24 +00:00
if ( g_TypeMode ! = TypeModeOff ) {
// Turn off
SendMessage ( hWnd , WM_USER_TYPING_OFF , 0 , 0 ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else {
forcePenResize = TRUE ;
PostMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
// In case we were in liveDraw
if ( GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) {
KillTimer ( hWnd , 3 ) ;
LONG_PTR exStyle = GetWindowLongPtr ( hWnd , GWL_EXSTYLE ) ;
SetWindowLongPtr ( hWnd , GWL_EXSTYLE , exStyle & ~ WS_EX_LAYERED ) ;
pMagSetWindowFilterList ( g_hWndLiveZoomMag , MW_FILTERMODE_EXCLUDE , 1 , & hWnd ) ;
SendMessage ( g_hWndLiveZoom , WM_USER_MAGNIFY_CURSOR , TRUE , 0 ) ;
}
}
break ;
}
return TRUE ;
case WM_RBUTTONDOWN :
SendMessage ( hWnd , WM_USER_EXIT_MODE , 0 , 0 ) ;
break ;
case WM_MOUSEMOVE :
OutputDebug ( L " MOUSEMOVE: zoomed: %d drawing: %d tracing: %d \n " ,
g_Zoomed , g_Drawing , g_Tracing ) ;
OutputDebug ( L " Window visible: %d Topmost: %d \n " , IsWindowVisible ( hWnd ) , GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_TOPMOST ) ;
2025-11-11 16:42:59 +01:00
if ( g_Zoomed & & g_TelescopingZoomLastTick ! = 0ull & & ! g_Drawing & & ! g_Tracing )
{
ULONG64 now = GetTickCount64 ( ) ;
if ( now - g_TelescopingZoomLastTick > = ZOOM_LEVEL_STEP_TIME )
{
doTelescopingZoomTimer ( false ) ;
}
}
2025-01-16 20:52:24 +00:00
if ( g_Zoomed & & ( g_TypeMode = = TypeModeOff ) & & ! g_bSaveInProgress ) {
if ( g_Drawing ) {
2025-10-07 11:20:00 -07:00
OutputDebug ( L " Mousemove: Drawing \n " ) ;
2025-01-16 20:52:24 +00:00
POINT currentPt ;
// Are we in pen mode on a tablet?
2025-11-10 11:57:13 -08:00
lParam = ScalePenPosition ( zoomLevel , & monInfo , boundRc , message , lParam ) ;
2025-01-16 20:52:24 +00:00
currentPt . x = LOWORD ( lParam ) ;
currentPt . y = HIWORD ( lParam ) ;
if ( lParam = = 0 ) {
// Drop it
OutputDebug ( L " Mousemove: Dropping \n " ) ;
break ;
} else if ( g_DrawingShape ) {
2025-11-10 11:57:13 -08:00
SetROP2 ( hdcScreenCompat , R2_NOTXORPEN ) ;
// If a previous target rectangle exists, erase
// it by drawing another rectangle on top.
2025-01-16 20:52:24 +00:00
if ( g_rcRectangle . top ! = g_rcRectangle . bottom | |
g_rcRectangle . left ! = g_rcRectangle . right )
{
if ( prevPenWidth ! = g_PenWidth )
{
auto penWidth = g_PenWidth ;
g_PenWidth = prevPenWidth ;
auto prevPen = CreatePen ( PS_SOLID , g_PenWidth , g_PenColor & 0xFFFFFF ) ;
SelectObject ( hdcScreenCompat , prevPen ) ;
DrawShape ( g_DrawingShape , hdcScreenCompat , & g_rcRectangle ) ;
g_PenWidth = penWidth ;
SelectObject ( hdcScreenCompat , hDrawingPen ) ;
DeleteObject ( prevPen ) ;
}
else
{
2025-10-07 11:20:00 -07:00
if ( PEN_COLOR_HIGHLIGHT ( g_PenColor ) )
{
// copy original bitmap to screen bitmap to erase previous highlight
BitBlt ( hdcScreenCompat , 0 , 0 , bmp . bmWidth , bmp . bmHeight , drawUndoList - > hDc , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
}
else
{
DrawShape ( g_DrawingShape , hdcScreenCompat , & g_rcRectangle , PEN_COLOR_HIGHLIGHT ( g_PenColor ) ) ;
}
2025-01-16 20:52:24 +00:00
}
}
2025-11-10 11:57:13 -08:00
// Save the coordinates of the target rectangle.
2025-01-16 20:52:24 +00:00
// Avoid invalid rectangles by ensuring that the
2025-11-10 11:57:13 -08:00
// value of the left coordinate is greater than
// that of the right, and that the value of the
// bottom coordinate is greater than that of
// the top.
2025-01-16 20:52:24 +00:00
if ( g_DrawingShape = = DRAW_LINE | |
g_DrawingShape = = DRAW_ARROW ) {
g_rcRectangle . right = static_cast < LONG > ( LOWORD ( lParam ) ) ;
g_rcRectangle . bottom = static_cast < LONG > ( HIWORD ( lParam ) ) ;
} else {
2025-11-10 11:57:13 -08:00
if ( ( g_RectangleAnchor . x < currentPt . x ) & &
2025-01-16 20:52:24 +00:00
( g_RectangleAnchor . y > currentPt . y ) ) {
2025-11-10 11:57:13 -08:00
SetRect ( & g_rcRectangle , g_RectangleAnchor . x , currentPt . y ,
currentPt . x , g_RectangleAnchor . y ) ;
2025-01-16 20:52:24 +00:00
2025-11-10 11:57:13 -08:00
} else if ( ( g_RectangleAnchor . x > currentPt . x ) & &
2025-01-16 20:52:24 +00:00
( g_RectangleAnchor . y > currentPt . y ) ) {
2025-11-10 11:57:13 -08:00
SetRect ( & g_rcRectangle , currentPt . x ,
currentPt . y , g_RectangleAnchor . x , g_RectangleAnchor . y ) ;
2025-01-16 20:52:24 +00:00
2025-11-10 11:57:13 -08:00
} else if ( ( g_RectangleAnchor . x > currentPt . x ) & &
2025-01-16 20:52:24 +00:00
( g_RectangleAnchor . y < currentPt . y ) ) {
2025-11-10 11:57:13 -08:00
SetRect ( & g_rcRectangle , currentPt . x , g_RectangleAnchor . y ,
g_RectangleAnchor . x , currentPt . y ) ;
2025-01-16 20:52:24 +00:00
} else {
2025-11-10 11:57:13 -08:00
SetRect ( & g_rcRectangle , g_RectangleAnchor . x , g_RectangleAnchor . y ,
currentPt . x , currentPt . y ) ;
2025-01-16 20:52:24 +00:00
}
}
if ( g_rcRectangle . left ! = g_rcRectangle . right | |
g_rcRectangle . top ! = g_rcRectangle . bottom ) {
2025-11-10 11:57:13 -08:00
// Draw the new target rectangle.
2025-10-07 11:20:00 -07:00
DrawShape ( g_DrawingShape , hdcScreenCompat , & g_rcRectangle , PEN_COLOR_HIGHLIGHT ( g_PenColor ) ) ;
2025-11-10 11:57:13 -08:00
OutputDebug ( L " SHAPE: (%d, %d) - (%d, %d) \n " , g_rcRectangle . left , g_rcRectangle . top ,
2025-01-16 20:52:24 +00:00
g_rcRectangle . right , g_rcRectangle . bottom ) ;
}
prevPenWidth = g_PenWidth ;
SetROP2 ( hdcScreenCompat , R2_NOP ) ;
}
else if ( g_Tracing ) {
OutputDebug ( L " Mousemove: Tracing \n " ) ;
g_HaveDrawn = TRUE ;
Gdiplus : : Graphics dstGraphics ( hdcScreenCompat ) ;
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : Pen pen ( color , static_cast < Gdiplus : : REAL > ( g_PenWidth ) ) ;
pen . SetLineCap ( Gdiplus : : LineCapRound , Gdiplus : : LineCapRound , Gdiplus : : DashCapRound ) ;
// If highlighting, use a double layer approach
OutputDebug ( L " PenColor: %x \n " , g_PenColor ) ;
OutputDebug ( L " Blur color: %x \n " , COLOR_BLUR ) ;
if ( ( g_PenColor & 0xFFFFFF ) = = COLOR_BLUR ) {
OutputDebug ( L " BLUR \n " ) ;
// Restore area where cursor was previously
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
// Create a new bitmap that's the size of the area covered by the line + 2 * g_PenWidth
Gdiplus : : Rect lineBounds = GetLineBounds ( prevPt , currentPt , g_PenWidth ) ;
Gdiplus : : Bitmap * lineBitmap = DrawBitmapLine ( lineBounds , prevPt , currentPt , & pen ) ;
Gdiplus : : BitmapData * lineData = LockGdiPlusBitmap ( lineBitmap ) ;
BYTE * pPixels = static_cast < BYTE * > ( lineData - > Scan0 ) ;
// Create a GDI bitmap that's the size of the lineBounds rectangle
2025-11-10 11:57:13 -08:00
Gdiplus : : Bitmap * blurBitmap = CreateGdiplusBitmap ( hdcScreenCompat , // oldestUndo->hDc,
2025-01-16 20:52:24 +00:00
lineBounds . X , lineBounds . Y , lineBounds . Width , lineBounds . Height ) ;
// Blur it
BitmapBlur ( blurBitmap ) ;
BlurScreen ( hdcScreenCompat , & lineBounds , blurBitmap , pPixels ) ;
// Unlock the bits
lineBitmap - > UnlockBits ( lineData ) ;
delete lineBitmap ;
delete blurBitmap ;
// Invalidate the updated rectangle
InvalidateGdiplusRect ( hWnd , lineBounds ) ;
// Save area that's going to be occupied by new cursor position
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , currentPt ) ;
// Draw new cursor
DrawCursor ( hdcScreenCompat , currentPt , zoomLevel , width , height ) ;
2025-11-10 11:57:13 -08:00
}
else if ( PEN_COLOR_HIGHLIGHT ( g_PenColor ) ) {
2025-01-16 20:52:24 +00:00
2025-10-07 11:20:00 -07:00
OutputDebug ( L " HIGHLIGHT \n " ) ;
2025-01-16 20:52:24 +00:00
// This is a highlighting pen color
Gdiplus : : Rect lineBounds = GetLineBounds ( prevPt , currentPt , g_PenWidth ) ;
Gdiplus : : Bitmap * lineBitmap = DrawBitmapLine ( lineBounds , prevPt , currentPt , & pen ) ;
Gdiplus : : BitmapData * lineData = LockGdiPlusBitmap ( lineBitmap ) ;
BYTE * pPixels = static_cast < BYTE * > ( lineData - > Scan0 ) ;
// Create a DIB section for efficient pixel manipulation
BITMAPINFO bmi = { 0 } ;
bmi . bmiHeader . biSize = sizeof ( BITMAPINFOHEADER ) ;
bmi . bmiHeader . biWidth = lineBounds . Width ;
bmi . bmiHeader . biHeight = - lineBounds . Height ; // Top-down DIB
bmi . bmiHeader . biPlanes = 1 ;
bmi . bmiHeader . biBitCount = 32 ; // 32 bits per pixel
bmi . bmiHeader . biCompression = BI_RGB ;
VOID * pDIBBits ;
HBITMAP hDIB = CreateDIBSection ( hdcScreenCompat , & bmi , DIB_RGB_COLORS , & pDIBBits , NULL , 0 ) ;
HDC hdcDIB = CreateCompatibleDC ( hdcScreenCompat ) ;
SelectObject ( hdcDIB , hDIB ) ;
// Copy the relevant part of hdcScreenCompat to the DIB
BitBlt ( hdcDIB , 0 , 0 , lineBounds . Width , lineBounds . Height , hdcScreenCompat , lineBounds . X , lineBounds . Y , SRCCOPY ) ;
// Pointer to the DIB bits
BYTE * pDestPixels = static_cast < BYTE * > ( pDIBBits ) ;
// Pointer to screen bits
HDC hdcDIBOrig ;
HBITMAP hDibOrigBitmap , hDibBitmap ;
P_DRAW_UNDO oldestUndo = GetOldestUndo ( drawUndoList ) ;
2025-11-10 11:57:13 -08:00
BYTE * pDestPixels2 = CreateBitmapMemoryDIB ( hdcScreenCompat , oldestUndo - > hDc , & lineBounds ,
2025-01-16 20:52:24 +00:00
& hdcDIBOrig , & hDibBitmap , & hDibOrigBitmap ) ;
for ( int local_y = 0 ; local_y < lineBounds . Height ; + + local_y ) {
for ( int local_x = 0 ; local_x < lineBounds . Width ; + + local_x ) {
int index = ( local_y * lineBounds . Width * 4 ) + ( local_x * 4 ) ; // Assuming 4 bytes per pixel
// BYTE b = pPixels[index + 0]; // Blue channel
// BYTE g = pPixels[index + 1]; // Green channel
// BYTE r = pPixels[index + 2]; // Red channel
BYTE a = pPixels [ index + 3 ] ; // Alpha channel
// Check if this is a drawn pixel
if ( a ! = 0 ) {
// Assuming pDestPixels is a valid pointer to the destination bitmap's pixel data
BYTE destB = pDestPixels2 [ index + 0 ] ; // Blue channel
BYTE destG = pDestPixels2 [ index + 1 ] ; // Green channel
BYTE destR = pDestPixels2 [ index + 2 ] ; // Red channel
// Create a COLORREF value from the destination pixel data
COLORREF currentPixel = RGB ( destR , destG , destB ) ;
// Blend the colors
COLORREF newPixel = BlendColors ( currentPixel , g_PenColor ) ;
// Update the destination pixel data with the new color
pDestPixels [ index + 0 ] = GetBValue ( newPixel ) ;
pDestPixels [ index + 1 ] = GetGValue ( newPixel ) ;
pDestPixels [ index + 2 ] = GetRValue ( newPixel ) ;
}
}
}
// Copy the updated DIB back to hdcScreenCompat
BitBlt ( hdcScreenCompat , lineBounds . X , lineBounds . Y , lineBounds . Width , lineBounds . Height , hdcDIB , 0 , 0 , SRCCOPY ) ;
// Clean up
DeleteObject ( hDIB ) ;
DeleteDC ( hdcDIB ) ;
SelectObject ( hdcDIBOrig , hDibOrigBitmap ) ;
DeleteObject ( hDibBitmap ) ;
DeleteDC ( hdcDIBOrig ) ;
// Invalidate the updated rectangle
InvalidateGdiplusRect ( hWnd , lineBounds ) ;
}
else {
// Normal tracing
dstGraphics . DrawLine ( & pen , static_cast < INT > ( prevPt . x ) , static_cast < INT > ( prevPt . y ) ,
static_cast < INT > ( currentPt . x ) , static_cast < INT > ( currentPt . y ) ) ;
}
} else {
OutputDebug ( L " Mousemove: Moving cursor \n " ) ;
// Restore area where cursor was previously
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// Save area that's going to be occupied by new cursor position
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , currentPt ) ;
// Draw new cursor
DrawCursor ( hdcScreenCompat , currentPt , zoomLevel , width , height ) ;
}
if ( g_DrawingShape ) {
InvalidateRect ( hWnd , NULL , FALSE ) ;
} else {
// Invalidate area just modified
InvalidateCursorMoveArea ( hWnd , zoomLevel , width , height , currentPt , prevPt , cursorPos ) ;
}
prevPt = currentPt ;
Updates for check-spelling v0.0.25 (#40386)
## Summary of the Pull Request
- #39572 updated check-spelling but ignored:
> 🐣 Breaking Changes
[Code Scanning action requires a Code Scanning
Ruleset](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset)
If you use SARIF reporting, then instead of the workflow yielding an ❌
when it fails, it will rely on [github-advanced-security
🤖](https://github.com/apps/github-advanced-security) to report the
failure. You will need to adjust your checks for PRs.
This means that check-spelling hasn't been properly doing its job 😦.
I'm sorry, I should have pushed a thing to this repo earlier,...
Anyway, as with most refreshes, this comes with a number of fixes, some
are fixes for typos that snuck in before the 0.0.25 upgrade, some are
for things that snuck in after, some are based on new rules in
spell-check-this, and some are hand written patterns based on running
through this repository a few times.
About the 🐣 **breaking change**: someone needs to create a ruleset for
this repository (see [Code Scanning action requires a Code Scanning
Ruleset: Sample ruleset
](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset#sample-ruleset)).
The alternative to adding a ruleset is to change the condition to not
use sarif for this repository. In general, I think the github
integration from sarif is prettier/more helpful, so I think that it's
the better choice.
You can see an example of it working in:
- https://github.com/check-spelling-sandbox/PowerToys/pull/23
---------
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
2025-07-08 18:16:52 -04:00
// In liveDraw we miss the mouse up
2025-01-16 20:52:24 +00:00
if ( GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED ) {
if ( ( GetAsyncKeyState ( VK_LBUTTON ) & 0x8000 ) = = 0 ) {
OutputDebug ( L " LIVE_DRAW missed mouse up. Sending synthetic. \n " ) ;
SendMessage ( hWnd , WM_LBUTTONUP , wParam , lParam ) ;
}
}
} else {
cursorPos . x = LOWORD ( lParam ) ;
cursorPos . y = HIWORD ( lParam ) ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
}
} else if ( g_Zoomed & & ( g_TypeMode ! = TypeModeOff ) & & ! g_HaveTyped ) {
ClearTypingCursor ( hdcScreenCompat , hdcScreenCursorCompat , cursorRc , g_BlankedScreen ) ;
textPt . x = prevPt . x = LOWORD ( lParam ) ;
textPt . y = prevPt . y = HIWORD ( lParam ) ;
// Draw the typing cursor
DrawTypingCursor ( hWnd , & textPt , hdcScreenCompat , hdcScreenCursorCompat , & cursorRc , true ) ;
prevPt = textPt ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
}
#if 0
{
static int index = 0 ;
2025-11-10 11:57:13 -08:00
OutputDebug ( L " %d: foreground: %x focus: %x (hwnd: %x) \n " ,
2025-01-16 20:52:24 +00:00
index + + , ( DWORD ) PtrToUlong ( GetForegroundWindow ( ) ) , PtrToUlong ( GetFocus ( ) ) , PtrToUlong ( hWnd ) ) ;
}
# endif
return TRUE ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
case WM_LBUTTONDOWN :
g_StraightDirection = 0 ;
if ( g_Zoomed & & ( g_TypeMode = = TypeModeOff ) & & zoomTelescopeTarget = = zoomLevel ) {
OutputDebug ( L " LBUTTONDOWN: drawing \n " ) ;
// Save current bitmap to undo history
if ( g_HaveDrawn ) {
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
}
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// don't push undo if we sent this to ourselves for a pen resize
if ( wParam ! = - 1 ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
PushDrawUndo ( hdcScreenCompat , & drawUndoList , width , height ) ;
} else {
wParam = 0 ;
}
// Are we in pen mode on a tablet?
lParam = ScalePenPosition ( zoomLevel , & monInfo , boundRc ,
message , lParam ) ;
if ( lParam = = 0 ) {
// Drop it
break ;
} else if ( g_Drawing ) {
// is the user drawing a rectangle?
if ( wParam & MK_CONTROL | |
wParam & MK_SHIFT | |
GetKeyState ( VK_TAB ) < 0 ) {
// Restore area where cursor was previously
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
if ( wParam & MK_SHIFT & & wParam & MK_CONTROL )
g_DrawingShape = DRAW_ARROW ;
2025-11-10 11:57:13 -08:00
else if ( wParam & MK_CONTROL )
2025-01-16 20:52:24 +00:00
g_DrawingShape = DRAW_RECTANGLE ;
else if ( wParam & MK_SHIFT )
g_DrawingShape = DRAW_LINE ;
else
g_DrawingShape = DRAW_ELLIPSE ;
g_RectangleAnchor . x = LOWORD ( lParam ) ;
g_RectangleAnchor . y = HIWORD ( lParam ) ;
2025-11-10 11:57:13 -08:00
SetRect ( & g_rcRectangle , g_RectangleAnchor . x , g_RectangleAnchor . y ,
g_RectangleAnchor . x , g_RectangleAnchor . y ) ;
2025-01-16 20:52:24 +00:00
} else {
Gdiplus : : Graphics dstGraphics ( hdcScreenCompat ) ;
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : Pen pen ( color , static_cast < Gdiplus : : REAL > ( g_PenWidth ) ) ;
Gdiplus : : GraphicsPath path ;
pen . SetLineJoin ( Gdiplus : : LineJoinRound ) ;
path . AddLine ( static_cast < INT > ( prevPt . x ) , prevPt . y , prevPt . x , prevPt . y ) ;
dstGraphics . DrawPath ( & pen , & path ) ;
}
g_Tracing = TRUE ;
SetROP2 ( hdcScreenCompat , R2_COPYPEN ) ;
2025-11-10 11:57:13 -08:00
prevPt . x = LOWORD ( lParam ) ;
prevPt . y = HIWORD ( lParam ) ;
2025-01-16 20:52:24 +00:00
g_HaveDrawn = TRUE ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else {
OutputDebug ( L " Tracing on \n " ) ;
// Turn on drawing
if ( ! g_HaveDrawn ) {
// refresh drawing bitmap with original screen image
BitBlt ( hdcScreenCompat , 0 , 0 , bmp . bmWidth ,
bmp . bmHeight , hdcScreenSaveCompat , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
g_HaveDrawn = TRUE ;
}
DeleteObject ( hDrawingPen ) ;
hDrawingPen = CreatePen ( PS_SOLID , g_PenWidth , g_PenColor & 0xFFFFFF ) ;
SelectObject ( hdcScreenCompat , hDrawingPen ) ;
// is the user drawing a rectangle?
if ( wParam & MK_CONTROL & & g_Drawing ) {
// Restore area where cursor was previously
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
// Configure rectangle drawing
g_DrawingShape = TRUE ;
g_RectangleAnchor . x = LOWORD ( lParam ) ;
g_RectangleAnchor . y = HIWORD ( lParam ) ;
SetRect ( & g_rcRectangle , g_RectangleAnchor . x , g_RectangleAnchor . y ,
g_RectangleAnchor . x , g_RectangleAnchor . y ) ;
OutputDebug ( L " RECTANGLE: %d, %d \n " , prevPt . x , prevPt . y ) ;
} else {
prevPt . x = LOWORD ( lParam ) ;
prevPt . y = HIWORD ( lParam ) ;
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
Gdiplus : : Graphics dstGraphics ( hdcScreenCursorCompat ) ;
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : Pen pen ( color , static_cast < Gdiplus : : REAL > ( g_PenWidth ) ) ;
Gdiplus : : GraphicsPath path ;
pen . SetLineJoin ( Gdiplus : : LineJoinRound ) ;
path . AddLine ( static_cast < INT > ( prevPt . x ) , prevPt . y , prevPt . x , prevPt . y ) ;
dstGraphics . DrawPath ( & pen , & path ) ;
}
InvalidateRect ( hWnd , NULL , FALSE ) ;
// If we're in live zoom, make the drawing pen larger to compensate
if ( g_ZoomOnLiveZoom & & forcePenResize )
{
forcePenResize = FALSE ;
ResizePen ( hWnd , hdcScreenCompat , hdcScreenCursorCompat , prevPt , g_Tracing ,
& g_Drawing , g_LiveZoomLevel , FALSE , min ( static_cast < int > ( g_LiveZoomLevel * g_RootPenWidth ) ,
static_cast < int > ( g_LiveZoomLevel * MAX_PEN_WIDTH ) ) ) ;
OutputDebug ( L " LIVEZOOM_DRAW: zoomLevel: %d rootPenWidth: %d penWidth: %d \n " ,
static_cast < int > ( g_LiveZoomLevel ) , g_RootPenWidth , g_PenWidth ) ;
}
else if ( ! g_ZoomOnLiveZoom & & forcePenResize )
{
forcePenResize = FALSE ;
// Scale pen down to root for regular drawing mode
ResizePen ( hWnd , hdcScreenCompat , hdcScreenCursorCompat , prevPt , g_Tracing ,
& g_Drawing , g_LiveZoomLevel , FALSE , g_RootPenWidth ) ;
}
g_Drawing = TRUE ;
EnableDisableStickyKeys ( FALSE ) ;
OutputDebug ( L " LBUTTONDOWN: %d, %d \n " , prevPt . x , prevPt . y ) ;
// Constrain the mouse to the visible region
boundRc = BoundMouse ( zoomLevel , & monInfo , width , height , & cursorPos ) ;
}
} else if ( g_TypeMode ! = TypeModeOff ) {
if ( ! g_HaveTyped ) {
g_HaveTyped = TRUE ;
} else {
SendMessage ( hWnd , WM_USER_TYPING_OFF , 0 , 0 ) ;
}
}
return TRUE ;
case WM_LBUTTONUP :
2025-11-10 11:57:13 -08:00
OutputDebug ( L " LBUTTONUP: zoomed: %d drawing: %d tracing: %d \n " ,
2025-01-16 20:52:24 +00:00
g_Zoomed , g_Drawing , g_Tracing ) ;
if ( g_Zoomed & & g_Drawing & & g_Tracing ) {
// Are we in pen mode on a tablet?
lParam = ScalePenPosition ( zoomLevel , & monInfo , boundRc ,
message , lParam ) ;
OutputDebug ( L " LBUTTONUP: %d, %d \n " , LOWORD ( lParam ) , HIWORD ( lParam ) ) ;
if ( lParam = = 0 ) {
// Drop it
break ;
}
POINT adjustPos ;
adjustPos . x = LOWORD ( lParam ) ;
adjustPos . y = HIWORD ( lParam ) ;
if ( g_StraightDirection = = - 1 ) {
adjustPos . x = prevPt . x ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else {
adjustPos . y = prevPt . y ;
}
2025-11-10 11:57:13 -08:00
lParam = MAKELPARAM ( adjustPos . x , adjustPos . y ) ;
2025-01-16 20:52:24 +00:00
if ( ! g_DrawingShape ) {
zoomit bug fixes (#41773)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #41040, #41041, #41043
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Manually tested and ensured that the issues are
resolved.
- [ ] **Localization:** N/A
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** No new binaries added
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
This PR includes code-changes to restore some of the features of ZoomIt
that existed in older versions and lost in later versions, such as when
in Draw mode, after drawing, if an area is snipped, it doesn't clear the
drawing immediately, giving the user an option to cancel snip and update
the drawing. Also, in draw mode, when left mouse is clicked, it results
in a dot, as it was in previous versions. This PR also addresses some
race conditions during Recording that results in error when the
Recording is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually tested and validated the draw and snip modes.
2025-09-14 22:59:48 -07:00
// If the point has changed, draw a line to it
2025-10-07 11:20:00 -07:00
if ( ! PEN_COLOR_HIGHLIGHT ( g_PenColor ) )
{
if ( prevPt . x ! = LOWORD ( lParam ) | | prevPt . y ! = HIWORD ( lParam ) )
zoomit bug fixes (#41773)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #41040, #41041, #41043
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Manually tested and ensured that the issues are
resolved.
- [ ] **Localization:** N/A
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** No new binaries added
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
This PR includes code-changes to restore some of the features of ZoomIt
that existed in older versions and lost in later versions, such as when
in Draw mode, after drawing, if an area is snipped, it doesn't clear the
drawing immediately, giving the user an option to cancel snip and update
the drawing. Also, in draw mode, when left mouse is clicked, it results
in a dot, as it was in previous versions. This PR also addresses some
race conditions during Recording that results in error when the
Recording is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually tested and validated the draw and snip modes.
2025-09-14 22:59:48 -07:00
{
2025-10-07 11:20:00 -07:00
Gdiplus : : Graphics dstGraphics ( hdcScreenCompat ) ;
if ( ( GetWindowLong ( g_hWndMain , GWL_EXSTYLE ) & WS_EX_LAYERED ) = = 0 )
{
dstGraphics . SetSmoothingMode ( Gdiplus : : SmoothingModeAntiAlias ) ;
}
Gdiplus : : Color color = ColorFromColorRef ( g_PenColor ) ;
Gdiplus : : Pen pen ( color , static_cast < Gdiplus : : REAL > ( g_PenWidth ) ) ;
Gdiplus : : GraphicsPath path ;
pen . SetLineJoin ( Gdiplus : : LineJoinRound ) ;
path . AddLine ( prevPt . x , prevPt . y , LOWORD ( lParam ) , HIWORD ( lParam ) ) ;
dstGraphics . DrawPath ( & pen , & path ) ;
}
// Draw a dot at the current point, if the point hasn't changed
else
{
MoveToEx ( hdcScreenCompat , prevPt . x , prevPt . y , NULL ) ;
LineTo ( hdcScreenCompat , LOWORD ( lParam ) , HIWORD ( lParam ) ) ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
zoomit bug fixes (#41773)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #41040, #41041, #41043
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Manually tested and ensured that the issues are
resolved.
- [ ] **Localization:** N/A
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** No new binaries added
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
This PR includes code-changes to restore some of the features of ZoomIt
that existed in older versions and lost in later versions, such as when
in Draw mode, after drawing, if an area is snipped, it doesn't clear the
drawing immediately, giving the user an option to cancel snip and update
the drawing. Also, in draw mode, when left mouse is clicked, it results
in a dot, as it was in previous versions. This PR also addresses some
race conditions during Recording that results in error when the
Recording is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually tested and validated the draw and snip modes.
2025-09-14 22:59:48 -07:00
}
2025-01-16 20:52:24 +00:00
}
prevPt . x = LOWORD ( lParam ) ;
prevPt . y = HIWORD ( lParam ) ;
if ( ( g_PenColor & 0xFFFFFF ) = = COLOR_BLUR ) {
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
}
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
DrawCursor ( hdcScreenCompat , prevPt , zoomLevel , width , height ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else if ( g_rcRectangle . top ! = g_rcRectangle . bottom | |
g_rcRectangle . left ! = g_rcRectangle . right ) {
// erase previous
2025-10-07 11:20:00 -07:00
if ( ! PEN_COLOR_HIGHLIGHT ( g_PenColor ) )
{
SetROP2 ( hdcScreenCompat , R2_NOTXORPEN ) ;
DrawShape ( g_DrawingShape , hdcScreenCompat , & g_rcRectangle ) ;
}
2025-01-16 20:52:24 +00:00
// Draw the final shape
HBRUSH hBrush = static_cast < HBRUSH > ( GetStockObject ( NULL_BRUSH ) ) ;
HBRUSH oldHbrush = static_cast < HBRUSH > ( SelectObject ( hdcScreenCompat , hBrush ) ) ;
SetROP2 ( hdcScreenCompat , R2_COPYPEN ) ;
// smooth line
if ( g_SnapToGrid ) {
if ( g_DrawingShape = = DRAW_LINE | |
g_DrawingShape = = DRAW_ARROW ) {
if ( abs ( g_rcRectangle . bottom - g_rcRectangle . top ) <
abs ( g_rcRectangle . right - g_rcRectangle . left ) / 10 ) {
g_rcRectangle . bottom = g_rcRectangle . top - 1 ;
}
if ( abs ( g_rcRectangle . right - g_rcRectangle . left ) <
abs ( g_rcRectangle . bottom - g_rcRectangle . top ) / 10 ) {
g_rcRectangle . right = g_rcRectangle . left - 1 ;
}
}
}
// Draw final one using Gdi+
DrawShape ( g_DrawingShape , hdcScreenCompat , & g_rcRectangle , true ) ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
DeleteObject ( hBrush ) ;
SelectObject ( hdcScreenCompat , oldHbrush ) ;
prevPt . x = LOWORD ( lParam ) ;
prevPt . y = HIWORD ( lParam ) ;
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
}
g_Tracing = FALSE ;
g_DrawingShape = FALSE ;
OutputDebug ( L " LBUTTONUP: " ) ;
}
return TRUE ;
case WM_GETMINMAXINFO :
reinterpret_cast < MINMAXINFO * > ( lParam ) - > ptMaxSize . x = width ;
reinterpret_cast < MINMAXINFO * > ( lParam ) - > ptMaxSize . y = height ;
reinterpret_cast < MINMAXINFO * > ( lParam ) - > ptMaxPosition . x = 0 ;
reinterpret_cast < MINMAXINFO * > ( lParam ) - > ptMaxPosition . y = 0 ;
return TRUE ;
case WM_USER_TYPING_OFF : {
if ( g_TypeMode ! = TypeModeOff ) {
g_TypeMode = TypeModeOff ;
ClearTypingCursor ( hdcScreenCompat , hdcScreenCursorCompat , cursorRc , g_BlankedScreen ) ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
DeleteTypedText ( & typedKeyList ) ;
// 1 means don't reset the cursor. We get that for font resizing
2026-02-03 13:05:31 -08:00
// Only move the cursor if we're drawing, else the screen moves to center
2025-01-16 20:52:24 +00:00
// on the new cursor position
if ( wParam ! = 1 & & g_Drawing ) {
prevPt . x = cursorRc . left ;
prevPt . y = cursorRc . top ;
2025-11-10 11:57:13 -08:00
SetCursorPos ( monInfo . rcMonitor . left + prevPt . x ,
2025-01-16 20:52:24 +00:00
monInfo . rcMonitor . top + prevPt . y ) ;
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( prevPt . x , prevPt . y ) ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else if ( ! g_Drawing ) {
// FIX: would be nice to reset cursor so screen doesn't move
prevPt = textStartPt ;
SaveCursorArea ( hdcScreenCursorCompat , hdcScreenCompat , prevPt ) ;
SetCursorPos ( prevPt . x , prevPt . y ) ;
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( prevPt . x , prevPt . y ) ) ;
}
}
}
return TRUE ;
case WM_USER_TRAY_ACTIVATE :
switch ( lParam ) {
case WM_RBUTTONUP :
case WM_LBUTTONUP :
case WM_CONTEXTMENU :
{
// Set the foreground window so the menu can be closed by clicking elsewhere when
// opened via right click, and so keyboard navigation works when opened with the menu
// key or Shift-F10.
SetForegroundWindow ( hWndOptions ? hWndOptions : hWnd ) ;
// Pop up context menu
POINT pt ;
GetCursorPos ( & pt ) ;
hPopupMenu = CreatePopupMenu ( ) ;
if ( ! g_StartedByPowerToys ) {
// Exiting will happen through disabling in PowerToys, not the context menu.
InsertMenu ( hPopupMenu , 0 , MF_BYPOSITION , IDCANCEL , L " E&xit " ) ;
InsertMenu ( hPopupMenu , 0 , MF_BYPOSITION | MF_SEPARATOR , 0 , NULL ) ;
}
InsertMenu ( hPopupMenu , 0 , MF_BYPOSITION | ( g_RecordToggle ? MF_CHECKED : 0 ) , IDC_RECORD , L " &Record " ) ;
InsertMenu ( hPopupMenu , 0 , MF_BYPOSITION , IDC_ZOOM , L " &Zoom " ) ;
InsertMenu ( hPopupMenu , 0 , MF_BYPOSITION , IDC_DRAW , L " &Draw " ) ;
InsertMenu ( hPopupMenu , 0 , MF_BYPOSITION , IDC_BREAK , L " &Break Timer " ) ;
if ( ! g_StartedByPowerToys ) {
// When started by PowerToys, options are configured through the PowerToys Settings.
InsertMenu ( hPopupMenu , 0 , MF_BYPOSITION | MF_SEPARATOR , 0 , NULL ) ;
InsertMenu ( hPopupMenu , 0 , MF_BYPOSITION , IDC_OPTIONS , L " &Options " ) ;
}
2026-02-03 13:05:31 -08:00
// Apply dark mode theme to the menu
ApplyDarkModeToMenu ( hPopupMenu ) ;
2025-01-16 20:52:24 +00:00
TrackPopupMenu ( hPopupMenu , 0 , pt . x , pt . y , 0 , hWnd , NULL ) ;
DestroyMenu ( hPopupMenu ) ;
break ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
case WM_LBUTTONDBLCLK :
if ( ! g_TimerActive ) {
SendMessage ( hWnd , WM_COMMAND , IDC_OPTIONS , 0 ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else {
SetForegroundWindow ( hWnd ) ;
}
break ;
}
break ;
case WM_USER_STOP_RECORDING :
StopRecording ( ) ;
break ;
case WM_USER_SAVE_CURSOR :
if ( g_Zoomed = = TRUE )
{
GetCursorPos ( & savedCursorPos ) ;
if ( g_Drawing = = TRUE )
{
ClipCursor ( NULL ) ;
}
}
break ;
case WM_USER_RESTORE_CURSOR :
if ( g_Zoomed = = TRUE )
{
if ( g_Drawing = = TRUE )
{
boundRc = BoundMouse ( zoomLevel , & monInfo , width , height , & cursorPos ) ;
}
SetCursorPos ( savedCursorPos . x , savedCursorPos . y ) ;
}
break ;
case WM_USER_EXIT_MODE :
if ( g_Zoomed )
{
// Turn off
if ( g_TypeMode ! = TypeModeOff )
{
SendMessage ( hWnd , WM_USER_TYPING_OFF , 0 , 0 ) ;
}
else if ( ! g_Drawing )
{
// Turn off
PostMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
}
else
{
if ( ! g_Tracing )
{
RestoreCursorArea ( hdcScreenCompat , hdcScreenCursorCompat , prevPt ) ;
// Ensure the cursor area is painted before returning
InvalidateRect ( hWnd , NULL , FALSE ) ;
UpdateWindow ( hWnd ) ;
// Make the magnified cursor visible again if LiveDraw is on in LiveZoom
if ( GetWindowLong ( hWnd , GWL_EXSTYLE ) & WS_EX_LAYERED )
{
if ( IsWindowVisible ( g_hWndLiveZoom ) )
{
SendMessage ( g_hWndLiveZoom , WM_USER_MAGNIFY_CURSOR , TRUE , 0 ) ;
}
}
}
if ( zoomLevel ! = 1 )
{
// Restore the cursor position to prevent moving the view in static zoom
SetCursorPos ( monInfo . rcMonitor . left + cursorPos . x , monInfo . rcMonitor . top + cursorPos . y ) ;
}
g_Drawing = FALSE ;
g_Tracing = FALSE ;
EnableDisableStickyKeys ( TRUE ) ;
SendMessage ( hWnd , WM_USER_TYPING_OFF , 0 , 0 ) ;
// Unclip cursor
ClipCursor ( NULL ) ;
}
}
else if ( g_TimerActive )
{
// Turn off
PostMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
}
break ;
case WM_USER_RELOAD_SETTINGS :
{
// Reload the settings. This message is called from PowerToys after a setting is changed by the user.
reg . ReadRegSettings ( RegSettings ) ;
2026-02-03 13:05:31 -08:00
// Refresh dark mode state after loading theme override from registry
RefreshDarkModeState ( ) ;
2025-11-14 17:36:31 -08:00
if ( g_RecordingFormat = = RecordingFormat : : GIF )
{
g_RecordScaling = g_RecordScalingGIF ;
2026-02-03 13:05:31 -08:00
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE ;
2025-11-14 17:36:31 -08:00
}
else
{
g_RecordScaling = g_RecordScalingMP4 ;
g_RecordFrameRate = RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE ;
}
2025-01-16 20:52:24 +00:00
// Apply tray icon setting
EnableDisableTrayIcon ( hWnd , g_ShowTrayIcon ) ;
2025-01-23 23:18:13 +00:00
// This is also called by ZoomIt when it starts and loads the Settings. Opacity is added after loading from registry, so we use the same pattern.
if ( ( g_PenColor > > 24 ) = = 0 )
{
g_PenColor | = 0xFF < < 24 ;
}
2025-01-16 20:52:24 +00:00
// Apply hotkey settings
UnregisterAllHotkeys ( hWnd ) ;
g_ToggleMod = GetKeyMod ( g_ToggleKey ) ;
g_LiveZoomToggleMod = GetKeyMod ( g_LiveZoomToggleKey ) ;
g_DrawToggleMod = GetKeyMod ( g_DrawToggleKey ) ;
g_BreakToggleMod = GetKeyMod ( g_BreakToggleKey ) ;
g_DemoTypeToggleMod = GetKeyMod ( g_DemoTypeToggleKey ) ;
g_SnipToggleMod = GetKeyMod ( g_SnipToggleKey ) ;
2026-03-26 13:21:43 +01:00
g_SnipPanoramaToggleMod = GetKeyMod ( g_SnipPanoramaToggleKey ) ;
g_SnipOcrToggleMod = GetKeyMod ( g_SnipOcrToggleKey ) ;
2025-01-16 20:52:24 +00:00
g_RecordToggleMod = GetKeyMod ( g_RecordToggleKey ) ;
BOOL showOptions = FALSE ;
if ( g_ToggleKey )
{
if ( ! RegisterHotKey ( hWnd , ZOOM_HOTKEY , g_ToggleMod , g_ToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified zoom toggle hotkey is already in use. \n Select a different zoom toggle hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
if ( g_LiveZoomToggleKey )
{
if ( ! RegisterHotKey ( hWnd , LIVE_HOTKEY , g_LiveZoomToggleMod , g_LiveZoomToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , LIVE_DRAW_HOTKEY , g_LiveZoomToggleMod ^ MOD_SHIFT , g_LiveZoomToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified live-zoom toggle hotkey is already in use. \n Select a different zoom toggle hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
if ( g_DrawToggleKey )
{
if ( ! RegisterHotKey ( hWnd , DRAW_HOTKEY , g_DrawToggleMod , g_DrawToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified draw w/out zoom hotkey is already in use. \n Select a different draw w/out zoom hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
if ( g_BreakToggleKey )
{
if ( ! RegisterHotKey ( hWnd , BREAK_HOTKEY , g_BreakToggleMod , g_BreakToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified break timer hotkey is already in use. \n Select a different break timer hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
if ( g_DemoTypeToggleKey )
{
if ( ! RegisterHotKey ( hWnd , DEMOTYPE_HOTKEY , g_DemoTypeToggleMod , g_DemoTypeToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , DEMOTYPE_RESET_HOTKEY , ( g_DemoTypeToggleMod ^ MOD_SHIFT ) , g_DemoTypeToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified live-type hotkey is already in use. \n Select a different live-type hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
if ( g_SnipToggleKey )
{
if ( ! RegisterHotKey ( hWnd , SNIP_HOTKEY , g_SnipToggleMod , g_SnipToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , SNIP_SAVE_HOTKEY , ( g_SnipToggleMod ^ MOD_SHIFT ) , g_SnipToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified snip hotkey is already in use. \n Select a different snip hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
2026-03-26 13:21:43 +01:00
if ( g_SnipPanoramaToggleKey & &
( g_SnipPanoramaToggleKey ! = g_SnipToggleKey | | g_SnipPanoramaToggleMod ! = g_SnipToggleMod ) )
{
if ( ! RegisterHotKey ( hWnd , SNIP_PANORAMA_HOTKEY , g_SnipPanoramaToggleMod | MOD_NOREPEAT , g_SnipPanoramaToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , SNIP_PANORAMA_SAVE_HOTKEY , ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT , g_SnipPanoramaToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified panorama snip hotkey is already in use. \n Select a different panorama snip hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
if ( g_SnipOcrToggleKey )
{
if ( ! RegisterHotKey ( hWnd , SNIP_OCR_HOTKEY , g_SnipOcrToggleMod , g_SnipOcrToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified snip OCR hotkey is already in use. \n Select a different snip OCR hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
2025-01-16 20:52:24 +00:00
if ( g_RecordToggleKey )
{
if ( ! RegisterHotKey ( hWnd , RECORD_HOTKEY , g_RecordToggleMod | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , RECORD_CROP_HOTKEY , ( g_RecordToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) | |
! RegisterHotKey ( hWnd , RECORD_WINDOW_HOTKEY , ( g_RecordToggleMod ^ MOD_ALT ) | MOD_NOREPEAT , g_RecordToggleKey & 0xFF ) )
{
MessageBox ( hWnd , L " The specified record hotkey is already in use. \n Select a different record hotkey. " , APPNAME , MB_ICONERROR ) ;
showOptions = TRUE ;
}
}
if ( showOptions )
{
// To open the PowerToys settings in the ZoomIt page.
SendMessage ( hWnd , WM_COMMAND , IDC_OPTIONS , 0 ) ;
}
break ;
}
case WM_COMMAND :
switch ( LOWORD ( wParam ) ) {
case IDC_SAVE_CROP :
case IDC_SAVE :
{
POINT local_savedCursorPos { } ;
if ( lParam ! = SHALLOW_ZOOM )
{
GetCursorPos ( & local_savedCursorPos ) ;
}
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
// Determine the user's desired save area in zoomed viewport coordinates.
// This will be the entire viewport if the user does not select a crop
// rectangle.
int copyX = 0 , copyY = 0 , copyWidth = width , copyHeight = height ;
2025-01-16 20:52:24 +00:00
if ( LOWORD ( wParam ) = = IDC_SAVE_CROP )
{
g_RecordCropping = TRUE ;
SelectRectangle selectRectangle ;
if ( ! selectRectangle . Start ( hWnd ) )
{
g_RecordCropping = FALSE ;
if ( lParam ! = SHALLOW_ZOOM )
{
SetCursorPos ( local_savedCursorPos . x , local_savedCursorPos . y ) ;
}
break ;
}
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
2025-01-16 20:52:24 +00:00
auto copyRc = selectRectangle . SelectedRect ( ) ;
selectRectangle . Stop ( ) ;
g_RecordCropping = FALSE ;
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
2025-01-16 20:52:24 +00:00
copyX = copyRc . left ;
copyY = copyRc . top ;
copyWidth = copyRc . right - copyRc . left ;
copyHeight = copyRc . bottom - copyRc . top ;
}
OutputDebug ( L " ***x: %d, y: %d, width: %d, height: %d \n " , copyX , copyY , copyWidth , copyHeight ) ;
RECT oldClipRect { } ;
GetClipCursor ( & oldClipRect ) ;
ClipCursor ( NULL ) ;
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
// Translate the viewport selection into coordinates for the 1:1 source
// bitmap hdcScreenCompat.
int viewportX , viewportY ;
GetZoomedTopLeftCoordinates (
zoomLevel , & cursorPos , & viewportX , width , & viewportY , height ) ;
int saveX = viewportX + static_cast < int > ( copyX / zoomLevel ) ;
int saveY = viewportY + static_cast < int > ( copyY / zoomLevel ) ;
int saveWidth = static_cast < int > ( copyWidth / zoomLevel ) ;
int saveHeight = static_cast < int > ( copyHeight / zoomLevel ) ;
// Create a pixel-accurate copy of the desired area from the source bitmap.
wil : : unique_hdc hdcActualSize ( CreateCompatibleDC ( hdcScreen ) ) ;
wil : : unique_hbitmap hbmActualSize (
CreateCompatibleBitmap ( hdcScreen , saveWidth , saveHeight ) ) ;
// Note: we do not need to restore the existing context later. The objects
// are transient and not reused.
SelectObject ( hdcActualSize . get ( ) , hbmActualSize . get ( ) ) ;
// Perform a direct 1:1 copy from the backing bitmap.
BitBlt ( hdcActualSize . get ( ) ,
0 , 0 ,
saveWidth , saveHeight ,
hdcScreenCompat ,
saveX , saveY ,
SRCCOPY | CAPTUREBLT ) ;
// Open the Save As dialog and capture the desired file path and whether to
// save the zoomed display or the source bitmap pixels.
2025-01-16 20:52:24 +00:00
g_bSaveInProgress = true ;
2026-02-03 13:05:31 -08:00
// Get a unique filename suggestion
auto suggestedName = GetUniqueScreenshotFilename ( ) ;
// Create modern IFileSaveDialog
auto saveDialog = wil : : CoCreateInstance < IFileSaveDialog > ( CLSID_FileSaveDialog ) ;
FILEOPENDIALOGOPTIONS options ;
if ( SUCCEEDED ( saveDialog - > GetOptions ( & options ) ) )
saveDialog - > SetOptions ( options | FOS_FORCEFILESYSTEM | FOS_OVERWRITEPROMPT ) ;
// Set file types - index is 1-based when retrieved via GetFileTypeIndex
COMDLG_FILTERSPEC fileTypes [ ] = {
{ L " Zoomed PNG " , L " *.png " } ,
{ L " Actual size PNG " , L " *.png " }
} ;
saveDialog - > SetFileTypes ( _countof ( fileTypes ) , fileTypes ) ;
saveDialog - > SetFileTypeIndex ( 1 ) ; // Default to "Zoomed PNG"
saveDialog - > SetDefaultExtension ( L " png " ) ;
saveDialog - > SetFileName ( suggestedName . c_str ( ) ) ;
saveDialog - > SetTitle ( L " ZoomIt: Save Zoomed Screen... " ) ;
// Set default folder to the last save location if available
if ( ! g_ScreenshotSaveLocation . empty ( ) )
{
std : : filesystem : : path lastPath ( g_ScreenshotSaveLocation ) ;
if ( lastPath . has_parent_path ( ) )
{
wil : : com_ptr < IShellItem > folderItem ;
if ( SUCCEEDED ( SHCreateItemFromParsingName ( lastPath . parent_path ( ) . c_str ( ) ,
nullptr , IID_PPV_ARGS ( & folderItem ) ) ) )
{
saveDialog - > SetFolder ( folderItem . get ( ) ) ;
}
}
}
OpenSaveDialogEvents * pEvents = new OpenSaveDialogEvents ( ) ;
DWORD dwCookie = 0 ;
saveDialog - > Advise ( pEvents , & dwCookie ) ;
UINT selectedFilterIndex = 1 ;
std : : wstring selectedFilePath ;
if ( SUCCEEDED ( saveDialog - > Show ( hWnd ) ) )
{
wil : : com_ptr < IShellItem > resultItem ;
if ( SUCCEEDED ( saveDialog - > GetResult ( & resultItem ) ) )
{
wil : : unique_cotaskmem_string pathStr ;
if ( SUCCEEDED ( resultItem - > GetDisplayName ( SIGDN_FILESYSPATH , & pathStr ) ) )
{
selectedFilePath = pathStr . get ( ) ;
}
}
saveDialog - > GetFileTypeIndex ( & selectedFilterIndex ) ;
}
saveDialog - > Unadvise ( dwCookie ) ;
pEvents - > Release ( ) ;
if ( ! selectedFilePath . empty ( ) )
{
std : : wstring targetFilePath = selectedFilePath ;
if ( targetFilePath . find ( L ' . ' ) = = std : : wstring : : npos )
2025-01-16 20:52:24 +00:00
{
2026-02-03 13:05:31 -08:00
targetFilePath + = L " .png " ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
if ( selectedFilterIndex = = 2 )
2025-01-16 20:52:24 +00:00
{
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
// Save at actual size.
2026-02-03 13:05:31 -08:00
SavePng ( targetFilePath . c_str ( ) , hbmActualSize . get ( ) ) ;
2025-01-16 20:52:24 +00:00
}
else
{
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
// Save zoomed-in image at screen resolution.
# if SCALE_HALFTONE
const int bltMode = HALFTONE ;
# else
// Use HALFTONE for better quality when smooth image is enabled
const int bltMode = g_SmoothImage ? HALFTONE : COLORONCOLOR ;
# endif
// Recreate the zoomed-in view by upscaling from our source bitmap.
wil : : unique_hdc hdcZoomed ( CreateCompatibleDC ( hdcScreen ) ) ;
wil : : unique_hbitmap hbmZoomed (
CreateCompatibleBitmap ( hdcScreen , copyWidth , copyHeight ) ) ;
SelectObject ( hdcZoomed . get ( ) , hbmZoomed . get ( ) ) ;
2025-01-16 20:52:24 +00:00
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
SetStretchBltMode ( hdcZoomed . get ( ) , bltMode ) ;
2025-01-16 20:52:24 +00:00
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
StretchBlt ( hdcZoomed . get ( ) ,
2025-01-16 20:52:24 +00:00
0 , 0 ,
copyWidth , copyHeight ,
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
hdcActualSize . get ( ) ,
0 , 0 ,
saveWidth , saveHeight ,
2025-01-16 20:52:24 +00:00
SRCCOPY | CAPTUREBLT ) ;
2025-11-10 11:57:13 -08:00
2026-02-03 13:05:31 -08:00
SavePng ( targetFilePath . c_str ( ) , hbmZoomed . get ( ) ) ;
2025-01-16 20:52:24 +00:00
}
2026-02-03 13:05:31 -08:00
// Remember the save location for next time and persist to registry
g_ScreenshotSaveLocation = targetFilePath ;
wcsncpy_s ( g_ScreenshotSaveLocationBuffer , g_ScreenshotSaveLocation . c_str ( ) , _TRUNCATE ) ;
reg . WriteRegSettings ( RegSettings ) ;
2025-01-16 20:52:24 +00:00
}
g_bSaveInProgress = false ;
if ( lParam ! = SHALLOW_ZOOM )
{
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
SetCursorPos ( local_savedCursorPos . x , local_savedCursorPos . y ) ;
2025-01-16 20:52:24 +00:00
}
ClipCursor ( & oldClipRect ) ;
[ZoomIt] Fix screenshot save accuracy issue and GDI object leak (#43375)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes two issues with the ZoomIt screenshot save function:
- The "actual size" saved bitmap was recomputed from the zoomed-in
viewport, leading to artefacts and a non-1:1 pixel accurate output if
smooth mode was active.
- Two bitmap objects were not deleted after being selected, leading to a
leak of 1 or 2 GDI objects per screenshot save operation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #43323
- [x] Closes: #43352
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
### GDI object leak
In the prior code, bitmap objects were created:
```cpp
// Capture the screen before displaying the save dialog
hInterimSaveDc = CreateCompatibleDC( hdcScreen );
hInterimSaveBitmap = CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight );
SelectObject( hInterimSaveDc, hInterimSaveBitmap );
...
hSaveBitmap = CreateCompatibleBitmap( hdcScreen, saveWidth, saveHeight );
SelectObject( hSaveDc, hSaveBitmap );
```
but were not deleted in the clean-up:
```cpp
DeleteDC( hInterimSaveDc );
DeleteDC( hSaveDc );
```
Deleting their associated device contexts orphans the bitmaps and the
GDI objects will not be recovered until ZoomIt is closed.
### Fix
The code now uses RAII for handles to DCs and Bitmaps, e.g.:
```cpp
wil::unique_hdc hdcZoomed( CreateCompatibleDC(hdcScreen) );
wil::unique_hbitmap hbmZoomed(
CreateCompatibleBitmap( hdcScreen, copyWidth, copyHeight ) );
SelectObject( hdcZoomed.get(), hbmZoomed.get() );
```
The handles are automatically cleaned up when the variables go out of
scope.
The existing `DeleteDC` code has been removed.
### Screenshot Actual Size save issue
ZoomIt's save screenshot routine operated on the currently-displayed
view, i.e. the zoomed-in viewport, executing a copy of the full monitor
resolution image to begin:
```cpp
StretchBlt( hInterimSaveDc,
0, 0,
copyWidth, copyHeight,
hdcScreen,
monInfo.rcMonitor.left + copyX,
monInfo.rcMonitor.top + copyY,
copyWidth, copyHeight,
SRCCOPY|CAPTUREBLT );
```
(Here hdcScreen represents the zoomed-in image.)
When "Actual size" was selected by the user, this bitmap was scaled back
down to attempt to reproduce the 1:1 pixel source:
```cpp
StretchBlt( hSaveDc,
0, 0,
saveWidth, saveHeight,
hInterimSaveDc,
0,
0,
copyWidth, copyHeight,
SRCCOPY | CAPTUREBLT );
SavePng( targetFilePath, hSaveBitmap );
```
This mostly works if the smooth mode is not applied because the zoom
levels tend to produce integer zoomed pixel sizes, although it still
produces inexact results at times.
The main issue is that the new smooth mode produces a halftone-smoothed
output on the display. Attempting to scale this back down to a
pixel-accurate source removes high frequency detail and does not reflect
the underlying bitmap:
Original source:
<img width="523" height="186" alt="image"
src="https://github.com/user-attachments/assets/7a6dca02-8608-44ed-917f-c6fd1a7b112c"
/>
"Actual size" save result before fix:
<img width="524" height="211" alt="image"
src="https://github.com/user-attachments/assets/29c63018-1e8d-4e74-a572-3615686aaa61"
/>
### Fix
This fix reverses the prior logic. Instead of using the zoomed-in
viewport as the screenshot source, we start by blitting a copy of the
source bitmap itself, from `hdcScreenCompat`. If a zoomed screenshot is
required, this is used as the source of the resize, i.e. we replicate
the zoom the user sees in their viewport.
This approach:
- Is faster and saves memory. It removes the need for an initial
`StretchBlt` operation, and the working bitmap itself is significantly
smaller if the user is zoomed in.
- Saves on the second resize operation if "Actual size" is chosen. We
can simply save the copy as-is.
- Removes the need to care about monitor coords. All calculations are
relative to the top-left of the source bitmap copy.
- Simplifies the code. In addition to being able to remove some code,
locality is improved - the creation of the zoomed image (and the
application of smoothing, if required) is now immediately next to where
it is saved.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
(Manual validation on standalone ZoomIt build.)
- Confirmed that "Actual size" screenshots are now 1:1 copies of the
underlying bitmap, not scaled copies of the screen display.
- Confirmed that screenshots no longer leak GDI objects in either
"Zoomed" or "Actual size" modes.
- Tested cropped and non-cropped saves, i.e. using
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> versus
<kbd>Ctrl</kbd>+<kbd>S</kbd>.
- Tested that user-added drawing/text was preserved in screenshots.
- Tested that both smooth and non-smooth zoom modes operate as they did
previously.
- Tested on multiple monitors, including a high-DPI external monitor
running at 175% scaling.
---------
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-11-13 04:51:54 +00:00
2025-01-16 20:52:24 +00:00
break ;
}
case IDC_COPY_CROP :
case IDC_COPY : {
HBITMAP hSaveBitmap ;
HDC hSaveDc ;
int copyX , copyY ;
int copyWidth , copyHeight ;
if ( LOWORD ( wParam ) = = IDC_COPY_CROP )
{
g_RecordCropping = TRUE ;
POINT local_savedCursorPos { } ;
if ( lParam ! = SHALLOW_ZOOM )
{
GetCursorPos ( & local_savedCursorPos ) ;
}
SelectRectangle selectRectangle ;
if ( ! selectRectangle . Start ( hWnd ) )
{
g_RecordCropping = FALSE ;
break ;
}
auto copyRc = selectRectangle . SelectedRect ( ) ;
selectRectangle . Stop ( ) ;
if ( lParam ! = SHALLOW_ZOOM )
{
SetCursorPos ( local_savedCursorPos . x , local_savedCursorPos . y ) ;
}
g_RecordCropping = FALSE ;
copyX = copyRc . left ;
copyY = copyRc . top ;
copyWidth = copyRc . right - copyRc . left ;
copyHeight = copyRc . bottom - copyRc . top ;
}
else
{
copyX = 0 ;
copyY = 0 ;
copyWidth = width ;
copyHeight = height ;
}
OutputDebug ( L " ***x: %d, y: %d, width: %d, height: %d \n " , copyX , copyY , copyWidth , copyHeight ) ;
hSaveBitmap = CreateCompatibleBitmap ( hdcScreen , copyWidth , copyHeight ) ;
hSaveDc = CreateCompatibleDC ( hdcScreen ) ;
SelectObject ( hSaveDc , hSaveBitmap ) ;
# if SCALE_HALFTONE
SetStretchBltMode ( hSaveDc , HALFTONE ) ;
# else
2025-10-07 11:20:00 -07:00
// Use HALFTONE for better quality when smooth image is enabled
if ( g_SmoothImage ) {
SetStretchBltMode ( hSaveDc , HALFTONE ) ;
} else {
SetStretchBltMode ( hSaveDc , COLORONCOLOR ) ;
}
2025-01-16 20:52:24 +00:00
# endif
StretchBlt ( hSaveDc ,
0 , 0 ,
copyWidth , copyHeight ,
hdcScreen ,
monInfo . rcMonitor . left + copyX ,
monInfo . rcMonitor . top + copyY ,
copyWidth , copyHeight ,
2025-11-10 11:57:13 -08:00
SRCCOPY | CAPTUREBLT ) ;
2025-01-16 20:52:24 +00:00
if ( OpenClipboard ( hWnd ) ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
EmptyClipboard ( ) ;
SetClipboardData ( CF_BITMAP , hSaveBitmap ) ;
CloseClipboard ( ) ;
}
DeleteDC ( hSaveDc ) ;
}
break ;
2026-03-26 13:21:43 +01:00
case IDC_COPY_OCR :
{
int copyX , copyY ;
int copyWidth , copyHeight ;
g_RecordCropping = TRUE ;
POINT local_savedCursorPos { } ;
if ( lParam ! = SHALLOW_ZOOM )
{
GetCursorPos ( & local_savedCursorPos ) ;
}
SelectRectangle selectRectangle ;
if ( ! selectRectangle . Start ( hWnd ) )
{
g_RecordCropping = FALSE ;
break ;
}
auto copyRc = selectRectangle . SelectedRect ( ) ;
selectRectangle . Stop ( ) ;
if ( lParam ! = SHALLOW_ZOOM )
{
SetCursorPos ( local_savedCursorPos . x , local_savedCursorPos . y ) ;
}
g_RecordCropping = FALSE ;
copyX = copyRc . left ;
copyY = copyRc . top ;
copyWidth = copyRc . right - copyRc . left ;
copyHeight = copyRc . bottom - copyRc . top ;
if ( copyWidth < = 0 | | copyHeight < = 0 )
{
break ;
}
// Capture the selected region into an HBITMAP
HBITMAP hSaveBitmap = CreateCompatibleBitmap ( hdcScreen , copyWidth , copyHeight ) ;
HDC hSaveDc = CreateCompatibleDC ( hdcScreen ) ;
SelectObject ( hSaveDc , hSaveBitmap ) ;
if ( g_SmoothImage )
{
SetStretchBltMode ( hSaveDc , HALFTONE ) ;
}
else
{
SetStretchBltMode ( hSaveDc , COLORONCOLOR ) ;
}
StretchBlt ( hSaveDc ,
0 , 0 ,
copyWidth , copyHeight ,
hdcScreen ,
monInfo . rcMonitor . left + copyX ,
monInfo . rcMonitor . top + copyY ,
copyWidth , copyHeight ,
SRCCOPY | CAPTUREBLT ) ;
// Run OCR on the captured bitmap
std : : wstring ocrText = OcrFromHBITMAP ( hSaveBitmap ) ;
// Copy OCR text to clipboard
if ( ! ocrText . empty ( ) & & OpenClipboard ( hWnd ) )
{
EmptyClipboard ( ) ;
size_t cbSize = ( ocrText . size ( ) + 1 ) * sizeof ( wchar_t ) ;
HGLOBAL hGlobal = GlobalAlloc ( GMEM_MOVEABLE , cbSize ) ;
if ( hGlobal )
{
void * pMem = GlobalLock ( hGlobal ) ;
if ( pMem )
{
memcpy ( pMem , ocrText . c_str ( ) , cbSize ) ;
GlobalUnlock ( hGlobal ) ;
SetClipboardData ( CF_UNICODETEXT , hGlobal ) ;
}
else
{
GlobalFree ( hGlobal ) ;
}
}
CloseClipboard ( ) ;
}
DeleteObject ( hSaveBitmap ) ;
DeleteDC ( hSaveDc ) ;
break ;
}
2025-11-10 11:57:13 -08:00
case IDC_DRAW :
2025-01-16 20:52:24 +00:00
PostMessage ( hWnd , WM_HOTKEY , DRAW_HOTKEY , 1 ) ;
break ;
case IDC_ZOOM :
PostMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 1 ) ;
break ;
case IDC_RECORD :
PostMessage ( hWnd , WM_HOTKEY , RECORD_HOTKEY , 1 ) ;
break ;
case IDC_OPTIONS :
// Don't show win32 forms options if started by PowerToys.
// Show the PowerToys Settings application instead.
if ( g_StartedByPowerToys )
{
# ifdef __ZOOMIT_POWERTOYS__
OpenPowerToysSettingsApp ( ) ;
# endif // __ZOOMIT_POWERTOYS__
}
else
{
DialogBox ( g_hInstance , L " OPTIONS " , hWnd , OptionsProc ) ;
}
break ;
case IDC_BREAK :
{
// Manage handles, clean visual transitions, and Options delta
if ( g_TimerActive )
{
if ( activeBreakShowBackgroundFile ! = g_BreakShowBackgroundFile | |
activeBreakShowDesktop ! = g_BreakShowDesktop )
{
if ( g_BreakShowBackgroundFile & & ! g_BreakShowDesktop )
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , SHALLOW_DESTROY ) ;
}
else
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
}
}
else
{
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , SHALLOW_DESTROY ) ;
g_TimerActive = TRUE ;
}
}
hdcScreen = CreateDC ( L " DISPLAY " , static_cast < PTCHAR > ( NULL ) , static_cast < PTCHAR > ( NULL ) , static_cast < CONST DEVMODE * > ( NULL ) ) ;
// toggle second monitor
// FIX: we should save whether or not we've switched to a second monitor
// rather than just assume that the setting hasn't changed since the break timer
// became active
if ( g_BreakOnSecondary )
{
EnableDisableSecondaryDisplay ( hWnd , TRUE , & secondaryDevMode ) ;
}
// Determine what monitor we're on
GetCursorPos ( & cursorPos ) ;
UpdateMonitorInfo ( cursorPos , & monInfo ) ;
width = monInfo . rcMonitor . right - monInfo . rcMonitor . left ;
height = monInfo . rcMonitor . bottom - monInfo . rcMonitor . top ;
// Trigger desktop recapture as necessary when switching monitors
if ( g_TimerActive & & g_BreakShowDesktop & & lastMonInfo . rcMonitor ! = monInfo . rcMonitor )
{
lastMonInfo = monInfo ;
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
PostMessage ( hWnd , WM_COMMAND , IDC_BREAK , 0 ) ;
break ;
}
lastMonInfo = monInfo ;
// If the background is a file that hasn't been collected, grab it now
if ( g_BreakShowBackgroundFile & & ! g_BreakShowDesktop & &
( ! g_TimerActive | | wcscmp ( activeBreakBackgroundFile , g_BreakBackgroundFile ) ) )
{
_tcscpy ( activeBreakBackgroundFile , g_BreakBackgroundFile ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
DeleteObject ( g_hBackgroundBmp ) ;
DeleteDC ( g_hDcBackgroundFile ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
g_hBackgroundBmp = NULL ;
g_hBackgroundBmp = LoadImageFile ( g_BreakBackgroundFile ) ;
if ( g_hBackgroundBmp = = NULL )
{
// Clean up hanging handles
SendMessage ( hWnd , WM_HOTKEY , ZOOM_HOTKEY , 0 ) ;
ErrorDialog ( hWnd , L " Error loading background bitmap " , GetLastError ( ) ) ;
break ;
}
g_hDcBackgroundFile = CreateCompatibleDC ( hdcScreen ) ;
SelectObject ( g_hDcBackgroundFile , g_hBackgroundBmp ) ;
}
// If the background is a desktop that hasn't been collected, grab it now
else if ( g_BreakShowBackgroundFile & & g_BreakShowDesktop & & ! g_TimerActive )
{
g_hBackgroundBmp = CreateFadedDesktopBackground ( GetDC ( NULL ) , & monInfo . rcMonitor , NULL ) ;
g_hDcBackgroundFile = CreateCompatibleDC ( hdcScreen ) ;
SelectObject ( g_hDcBackgroundFile , g_hBackgroundBmp ) ;
}
// Track Options.Break delta
activeBreakShowBackgroundFile = g_BreakShowBackgroundFile ;
activeBreakShowDesktop = g_BreakShowDesktop ;
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
Trace : : ZoomItActivateBreak ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
breakTimeout = g_BreakTimeout * 60 + 1 ;
2026-03-26 13:21:43 +01:00
//
// If lock workstation is enabled, skip the local break timer
// window entirely — the embedded screensaver will run it on
// the ScreenSaver Desktop with authentication required.
//
if ( g_BreakLockWorkstation )
{
DeleteDC ( hdcScreen ) ;
ActivateBreakScreenSaver ( hWnd , breakTimeout ) ;
break ;
}
g_TimerActive = TRUE ;
2025-01-16 20:52:24 +00:00
// Create font
g_LogFont . lfHeight = height / 5 ;
hTimerFont = CreateFontIndirect ( & g_LogFont ) ;
g_LogFont . lfHeight = height / 8 ;
hNegativeTimerFont = CreateFontIndirect ( & g_LogFont ) ;
// Create backing bitmap
2025-11-10 11:57:13 -08:00
hdcScreenCompat = CreateCompatibleDC ( hdcScreen ) ;
2025-01-16 20:52:24 +00:00
bmp . bmBitsPixel = static_cast < BYTE > ( GetDeviceCaps ( hdcScreen , BITSPIXEL ) ) ;
bmp . bmPlanes = static_cast < BYTE > ( GetDeviceCaps ( hdcScreen , PLANES ) ) ;
bmp . bmWidth = width ;
bmp . bmHeight = height ;
2025-11-10 11:57:13 -08:00
bmp . bmWidthBytes = ( ( bmp . bmWidth + 15 ) & ~ 15 ) / 8 ;
hbmpCompat = CreateBitmap ( bmp . bmWidth , bmp . bmHeight ,
bmp . bmPlanes , bmp . bmBitsPixel , static_cast < CONST VOID * > ( NULL ) ) ;
SelectObject ( hdcScreenCompat , hbmpCompat ) ;
2025-01-16 20:52:24 +00:00
SetTextColor ( hdcScreenCompat , g_BreakPenColor ) ;
SetBkMode ( hdcScreenCompat , TRANSPARENT ) ;
SelectObject ( hdcScreenCompat , hTimerFont ) ;
EnableDisableOpacity ( hWnd , TRUE ) ;
EnableDisableScreenSaver ( FALSE ) ;
SendMessage ( hWnd , WM_TIMER , 0 , 0 ) ;
SetTimer ( hWnd , 0 , 1000 , NULL ) ;
BringWindowToTop ( hWnd ) ;
SetForegroundWindow ( hWnd ) ;
SetActiveWindow ( hWnd ) ;
2025-11-10 11:57:13 -08:00
SetWindowPos ( hWnd , HWND_NOTOPMOST , monInfo . rcMonitor . left , monInfo . rcMonitor . top ,
2025-01-16 20:52:24 +00:00
width , height , SWP_SHOWWINDOW ) ;
}
break ;
case IDCANCEL :
memset ( & tNotifyIconData , 0 , sizeof ( tNotifyIconData ) ) ;
2025-11-10 11:57:13 -08:00
tNotifyIconData . cbSize = sizeof ( NOTIFYICONDATA ) ;
tNotifyIconData . hWnd = hWnd ;
tNotifyIconData . uID = 1 ;
Shell_NotifyIcon ( NIM_DELETE , & tNotifyIconData ) ;
2025-01-16 20:52:24 +00:00
reg . WriteRegSettings ( RegSettings ) ;
if ( hWndOptions )
{
DestroyWindow ( hWndOptions ) ;
}
DestroyWindow ( hWnd ) ;
break ;
}
break ;
case WM_TIMER :
switch ( wParam ) {
case 0 :
//
// Break timer
//
breakTimeout - = 1 ;
InvalidateRect ( hWnd , NULL , FALSE ) ;
if ( breakTimeout = = 0 & & g_BreakPlaySoundFile ) {
PlaySound ( g_BreakSoundFile , NULL , SND_FILENAME | SND_ASYNC ) ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
break ;
case 2 :
case 1 :
2025-11-11 16:42:59 +01:00
doTelescopingZoomTimer ( ) ;
2025-01-16 20:52:24 +00:00
break ;
case 3 :
POINT mousePos ;
GetCursorPos ( & mousePos ) ;
if ( mousePos . x ! = cursorPos . x | | mousePos . y ! = cursorPos . y )
{
MONITORINFO monitorInfo = { sizeof ( MONITORINFO ) } ;
UpdateMonitorInfo ( mousePos , & monitorInfo ) ;
mousePos . x - = monitorInfo . rcMonitor . left ;
mousePos . y - = monitorInfo . rcMonitor . top ;
OutputDebug ( L " RETRACKING MOUSE: x: %d y: %d \n " , mousePos . x , mousePos . y ) ;
SendMessage ( hWnd , WM_MOUSEMOVE , 0 , MAKELPARAM ( mousePos . x , mousePos . y ) ) ;
}
break ;
2026-03-26 13:21:43 +01:00
case 4 :
// Break screensaver post-cleanup watchdog.
if ( HasOrphanedScreenSaverSettings ( ) )
{
if ( ! IsBreakScreenSaverRunning ( ) )
{
RestoreScreenSaverSettings ( ) ;
KillTimer ( hWnd , 4 ) ;
}
}
else
{
KillTimer ( hWnd , 4 ) ;
}
break ;
case 5 :
{
//
// Deferred SPI re-apply after session unlock.
// Winlogon may override our SPI_SETSCREENSAVEACTIVE /
// SPI_SETSCREENSAVETIMEOUT calls during the desktop switch.
// Re-read the (already-restored) registry values and push
// them into the runtime SPI state.
//
KillTimer ( hWnd , 5 ) ;
TCHAR regActive [ 16 ] = { } ;
DWORD cbActive = sizeof ( regActive ) ;
BOOL rtActive = FALSE ;
if ( RegGetValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " ScreenSaveActive " , RRF_RT_REG_SZ , NULL ,
regActive , & cbActive ) = = ERROR_SUCCESS )
{
rtActive = ( wcstol ( regActive , nullptr , 10 ) ! = 0 ) ;
}
SystemParametersInfo ( SPI_SETSCREENSAVEACTIVE , rtActive , 0 , SPIF_SENDCHANGE ) ;
TCHAR regTimeout [ 16 ] = { } ;
DWORD cbTimeout = sizeof ( regTimeout ) ;
UINT timeout = 300 ; // safe default
if ( RegGetValue ( HKEY_CURRENT_USER , L " Control Panel \\ Desktop " ,
L " ScreenSaveTimeOut " , RRF_RT_REG_SZ , NULL ,
regTimeout , & cbTimeout ) = = ERROR_SUCCESS )
{
long parsed = wcstol ( regTimeout , nullptr , 10 ) ;
if ( parsed > 0 )
timeout = static_cast < UINT > ( parsed ) ;
}
SystemParametersInfo ( SPI_SETSCREENSAVETIMEOUT , timeout , 0 , SPIF_SENDCHANGE ) ;
break ;
}
2025-01-16 20:52:24 +00:00
}
break ;
case WM_PAINT :
2025-11-10 11:57:13 -08:00
hDc = BeginPaint ( hWnd , & ps ) ;
2025-01-16 20:52:24 +00:00
if ( ( ( g_RecordCropping = = FALSE ) | | ( zoomLevel = = 1 ) ) & & g_Zoomed ) {
OutputDebug ( L " PAINT x: %d y: %d width: %d height: %d zoomLevel: %g \n " ,
cursorPos . x , cursorPos . y , width , height , zoomLevel ) ;
GetZoomedTopLeftCoordinates ( zoomLevel , & cursorPos , & x , width , & y , height ) ;
# if SCALE_GDIPLUS
if ( zoomLevel > = zoomTelescopeTarget ) {
// do a high-quality render
2025-11-10 11:57:13 -08:00
extern void ScaleImage ( HDC hdcDst , float xDst , float yDst , float wDst , float hDst ,
2025-01-16 20:52:24 +00:00
HBITMAP bmSrc , float xSrc , float ySrc , float wSrc , float hSrc ) ;
2025-11-10 11:57:13 -08:00
ScaleImage ( ps . hdc ,
0 , 0 ,
( float ) bmp . bmWidth , ( float ) bmp . bmHeight ,
hbmpCompat ,
( float ) x , ( float ) y ,
width / zoomLevel , height / zoomLevel ) ;
2025-01-16 20:52:24 +00:00
} else {
2025-10-07 11:20:00 -07:00
// do a fast, less accurate render (but use smooth if enabled)
SetStretchBltMode ( hDc , g_SmoothImage ? HALFTONE : COLORONCOLOR ) ;
2025-11-10 11:57:13 -08:00
StretchBlt ( ps . hdc ,
0 , 0 ,
bmp . bmWidth , bmp . bmHeight ,
hdcScreenCompat ,
x , y ,
2025-01-16 20:52:24 +00:00
( int ) ( width / zoomLevel ) , ( int ) ( height / zoomLevel ) ,
2025-11-10 11:57:13 -08:00
SRCCOPY ) ;
2025-01-16 20:52:24 +00:00
}
# else
# if SCALE_HALFTONE
SetStretchBltMode ( hDc , zoomLevel = = zoomTelescopeTarget ? HALFTONE : COLORONCOLOR ) ;
# else
2025-10-07 11:20:00 -07:00
// Use HALFTONE for better quality when smooth image is enabled
if ( g_SmoothImage ) {
SetStretchBltMode ( hDc , HALFTONE ) ;
} else {
SetStretchBltMode ( hDc , COLORONCOLOR ) ;
}
2025-01-16 20:52:24 +00:00
# endif
2025-11-10 11:57:13 -08:00
StretchBlt ( ps . hdc ,
0 , 0 ,
bmp . bmWidth , bmp . bmHeight ,
hdcScreenCompat ,
x , y ,
2025-01-16 20:52:24 +00:00
static_cast < int > ( width / zoomLevel ) , static_cast < int > ( height / zoomLevel ) ,
2025-11-10 11:57:13 -08:00
SRCCOPY | CAPTUREBLT ) ;
2025-01-16 20:52:24 +00:00
# endif
} else if ( g_TimerActive ) {
2026-03-26 13:21:43 +01:00
// Fill background (white by default, black if saved as 1)
2025-01-16 20:52:24 +00:00
rc . top = rc . left = 0 ;
rc . bottom = height ;
rc . right = width ;
2026-03-26 13:21:43 +01:00
BlankScreenArea ( hdcScreenCompat , & rc , g_BreakBackgroundColor ? ' K ' : ' W ' ) ;
2025-01-16 20:52:24 +00:00
// If there's a background bitmap, draw it in the center
if ( g_hBackgroundBmp ) {
BITMAP local_bmp ;
GetObject ( g_hBackgroundBmp , sizeof ( local_bmp ) , & local_bmp ) ;
2025-10-07 11:20:00 -07:00
SetStretchBltMode ( hdcScreenCompat , g_SmoothImage ? HALFTONE : COLORONCOLOR ) ;
2025-01-16 20:52:24 +00:00
if ( g_BreakBackgroundStretch ) {
StretchBlt ( hdcScreenCompat , 0 , 0 , width , height ,
g_hDcBackgroundFile , 0 , 0 , local_bmp . bmWidth , local_bmp . bmHeight , SRCCOPY | CAPTUREBLT ) ;
} else {
2025-11-10 11:57:13 -08:00
BitBlt ( hdcScreenCompat , width / 2 - local_bmp . bmWidth / 2 , height / 2 - local_bmp . bmHeight / 2 ,
2025-01-16 20:52:24 +00:00
local_bmp . bmWidth , local_bmp . bmHeight , g_hDcBackgroundFile , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
}
}
// Draw time
if ( breakTimeout > 0 ) {
_stprintf ( timerText , L " % 2d:%02d " , breakTimeout / 60 , breakTimeout % 60 ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else {
_tcscpy ( timerText , L " 0:00 " ) ;
}
rc . left = rc . top = 0 ;
2025-11-10 11:57:13 -08:00
DrawText ( hdcScreenCompat , timerText , - 1 , & rc ,
2025-01-16 20:52:24 +00:00
DT_NOCLIP | DT_LEFT | DT_NOPREFIX | DT_CALCRECT ) ;
rc1 . left = rc1 . right = rc1 . bottom = rc1 . top = 0 ;
if ( g_ShowExpiredTime & & breakTimeout < 0 ) {
_stprintf ( negativeTimerText , L " (-% 2d:%02d) " ,
- breakTimeout / 60 , - breakTimeout % 60 ) ;
HFONT prevFont = static_cast < HFONT > ( SelectObject ( hdcScreenCompat , hNegativeTimerFont ) ) ;
2025-11-10 11:57:13 -08:00
DrawText ( hdcScreenCompat , negativeTimerText , - 1 , & rc1 ,
2025-01-16 20:52:24 +00:00
DT_NOCLIP | DT_LEFT | DT_NOPREFIX | DT_CALCRECT ) ;
SelectObject ( hdcScreenCompat , prevFont ) ;
}
// Position time vertically
switch ( g_BreakTimerPosition ) {
case 0 :
case 1 :
case 2 :
rc . top = 50 ;
break ;
case 3 :
case 4 :
case 5 :
rc . top = ( height - ( rc . bottom - rc . top ) ) / 2 ;
break ;
case 6 :
case 7 :
case 8 :
rc . top = height - rc . bottom - 50 - rc1 . bottom ;
break ;
}
// Position time horizontally
switch ( g_BreakTimerPosition ) {
case 0 :
case 3 :
case 6 :
rc . left = 50 ;
break ;
case 1 :
case 4 :
case 7 :
rc . left = ( width - ( rc . right - rc . left ) ) / 2 ;
break ;
case 2 :
case 5 :
case 8 :
rc . left = width - rc . right - 50 ;
break ;
}
rc . bottom + = rc . top ;
rc . right + = rc . left ;
DrawText ( hdcScreenCompat , timerText , - 1 , & rc , DT_NOCLIP | DT_LEFT | DT_NOPREFIX ) ;
if ( g_ShowExpiredTime & & breakTimeout < 0 ) {
rc1 . top = rc . bottom + 10 ;
rc1 . left = rc . left + ( ( rc . right - rc . left ) - ( rc1 . right - rc1 . left ) ) / 2 ;
HFONT prevFont = static_cast < HFONT > ( SelectObject ( hdcScreenCompat , hNegativeTimerFont ) ) ;
2025-11-10 11:57:13 -08:00
DrawText ( hdcScreenCompat , negativeTimerText , - 1 , & rc1 ,
2025-01-16 20:52:24 +00:00
DT_NOCLIP | DT_LEFT | DT_NOPREFIX ) ;
SelectObject ( hdcScreenCompat , prevFont ) ;
}
// Copy to screen
BitBlt ( ps . hdc , 0 , 0 , width , height , hdcScreenCompat , 0 , 0 , SRCCOPY | CAPTUREBLT ) ;
}
2025-11-10 11:57:13 -08:00
EndPaint ( hWnd , & ps ) ;
2025-01-16 20:52:24 +00:00
return TRUE ;
2026-03-26 13:21:43 +01:00
case WM_WTSSESSION_CHANGE :
//
// When the user unlocks the workstation after a break timer screensaver
// session, restore the original screensaver settings so the break timer
// doesn't reactivate.
//
if ( wParam = = WTS_SESSION_UNLOCK | |
wParam = = WTS_SESSION_LOGON | |
wParam = = WTS_CONSOLE_CONNECT )
{
RestoreScreenSaverSettings ( ) ;
//
// Schedule a deferred SPI re-apply. Winlogon may re-read the
// (now-stale) screensaver policy during the desktop switch,
// overriding the SPI_SETSCREENSAVEACTIVE / TIMEOUT calls we
// just made. Timer 5 fires after the transition completes
// and re-asserts the correct runtime state.
//
SetTimer ( hWnd , 5 , 5000 , NULL ) ;
}
break ;
2025-01-16 20:52:24 +00:00
case WM_DESTROY :
2026-03-26 13:21:43 +01:00
// Restore screensaver settings on clean shutdown in case the
// break screensaver was still active.
if ( HasOrphanedScreenSaverSettings ( ) )
RestoreScreenSaverSettings ( ) ;
WTSUnRegisterSessionNotification ( hWnd ) ;
2026-02-03 13:05:31 -08:00
CleanupDarkModeResources ( ) ;
2025-01-16 20:52:24 +00:00
PostQuitMessage ( 0 ) ;
break ;
default :
if ( message = = wmTaskbarCreated )
{
if ( g_ShowTrayIcon )
{
EnableDisableTrayIcon ( hWnd , TRUE ) ;
}
return TRUE ;
}
return DefWindowProc ( hWnd , message , wParam , lParam ) ;
}
return 0 ;
}
//----------------------------------------------------------------------------
//
// LiveZoomWndProc
//
//----------------------------------------------------------------------------
LRESULT CALLBACK LiveZoomWndProc ( HWND hWnd , UINT message , WPARAM wParam , LPARAM lParam )
{
RECT rc ;
POINT cursorPos ;
static int width , height ;
static MONITORINFO monInfo ;
HDC hdcScreen ;
#if 0
int delta ;
BOOLEAN zoomIn ;
# endif
static POINT lastCursorPos ;
POINT adjustedCursorPos , zoomCenterPos ;
int moveWidth , moveHeight ;
int sourceRectHeight , sourceRectWidth ;
DWORD curTickCount ;
RECT sourceRect { } ;
static RECT lastSourceRect ;
static float zoomLevel ;
static float zoomTelescopeStep ;
static float zoomTelescopeTarget ;
static DWORD prevZoomStepTickCount = 0 ;
static BOOL dwmEnabled = FALSE ;
static BOOLEAN startedInPresentationMode = FALSE ;
MAGTRANSFORM matrix ;
switch ( message ) {
case WM_CREATE :
// Initialize
pMagInitialize ( ) ;
if ( pDwmIsCompositionEnabled ) pDwmIsCompositionEnabled ( & dwmEnabled ) ;
// Create the zoom window
if ( ! g_fullScreenWorkaround ) {
g_hWndLiveZoomMag = CreateWindowEx ( 0 ,
WC_MAGNIFIER ,
TEXT ( " MagnifierWindow " ) ,
WS_CHILD | MS_SHOWMAGNIFIEDCURSOR | WS_VISIBLE ,
0 , 0 , 0 , 0 , hWnd , NULL , g_hInstance , NULL ) ;
}
ShowWindow ( hWnd , SW_SHOW ) ;
InvalidateRect ( g_hWndLiveZoomMag , NULL , TRUE ) ;
if ( ! g_fullScreenWorkaround )
SetForegroundWindow ( static_cast < HWND > ( reinterpret_cast < LPCREATESTRUCT > ( lParam ) - > lpCreateParams ) ) ;
// If we're not on Win7+, then set a timer to go off two hours from
// now
if ( g_OsVersion < WIN7_VERSION ) {
startedInPresentationMode = IsPresentationMode ( ) ;
// if we're not in presentation mode, kill ourselves after a timeout
if ( ! startedInPresentationMode ) {
SetTimer ( hWnd , 1 , LIVEZOOM_WINDOW_TIMEOUT , NULL ) ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
}
break ;
case WM_SHOWWINDOW :
if ( wParam = = TRUE ) {
// Determine what monitor we're on
lastCursorPos . x = - 1 ;
hdcScreen = GetDC ( NULL ) ;
GetCursorPos ( & cursorPos ) ;
UpdateMonitorInfo ( cursorPos , & monInfo ) ;
width = monInfo . rcMonitor . right - monInfo . rcMonitor . left ;
height = monInfo . rcMonitor . bottom - monInfo . rcMonitor . top ;
lastSourceRect . left = lastSourceRect . right = 0 ;
lastSourceRect . right = width ;
lastSourceRect . bottom = height ;
// Set window size
if ( ! g_fullScreenWorkaround ) {
SetWindowPos ( hWnd , NULL , monInfo . rcMonitor . left , monInfo . rcMonitor . top ,
monInfo . rcMonitor . right - monInfo . rcMonitor . left ,
monInfo . rcMonitor . bottom - monInfo . rcMonitor . top ,
SWP_NOACTIVATE | SWP_NOZORDER ) ;
UpdateWindow ( hWnd ) ;
}
2025-11-10 11:57:13 -08:00
// Are we coming back from a static zoom that
2025-01-16 20:52:24 +00:00
// was started while we were live zoomed?
if ( g_ZoomOnLiveZoom ) {
// Force a zoom to 2x without telescope
prevZoomStepTickCount = 0 ;
zoomLevel = static_cast < float > ( 1.9 ) ;
zoomTelescopeTarget = 2.0 ;
zoomTelescopeStep = 2.0 ;
} else {
zoomTelescopeStep = ZOOM_LEVEL_STEP_IN ;
zoomTelescopeTarget = g_ZoomLevels [ g_SliderZoomLevel ] ;
prevZoomStepTickCount = 0 ;
if ( dwmEnabled ) {
zoomLevel = static_cast < float > ( 1 ) ;
} else {
zoomLevel = static_cast < float > ( 1.9 ) ;
}
}
RegisterHotKey ( hWnd , 0 , MOD_CONTROL , VK_UP ) ;
RegisterHotKey ( hWnd , 1 , MOD_CONTROL , VK_DOWN ) ;
// Hide hardware cursor
if ( ! g_fullScreenWorkaround )
if ( pMagShowSystemCursor ) pMagShowSystemCursor ( FALSE ) ;
if ( g_RecordToggle )
g_RecordingSession - > EnableCursorCapture ( false ) ;
GetCursorPos ( & lastCursorPos ) ;
SetCursorPos ( lastCursorPos . x , lastCursorPos . y ) ;
SendMessage ( hWnd , WM_TIMER , 0 , 0 ) ;
SetTimer ( hWnd , 0 , ZOOM_LEVEL_STEP_TIME , NULL ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else {
KillTimer ( hWnd , 0 ) ;
if ( g_RecordToggle )
g_RecordingSession - > EnableCursorCapture ( ) ;
if ( ! g_fullScreenWorkaround )
if ( pMagShowSystemCursor ) pMagShowSystemCursor ( TRUE ) ;
// Reset the timer to expire two hours from now
if ( g_OsVersion < WIN7_VERSION & & ! IsPresentationMode ( ) ) {
KillTimer ( hWnd , 1 ) ;
SetTimer ( hWnd , 1 , LIVEZOOM_WINDOW_TIMEOUT , NULL ) ;
} else {
DestroyWindow ( hWnd ) ;
}
UnregisterHotKey ( hWnd , 0 ) ;
UnregisterHotKey ( hWnd , 1 ) ;
}
break ;
case WM_TIMER :
switch ( wParam ) {
case 0 : {
// if we're cropping, do not move
if ( g_RecordCropping = = TRUE )
{
// Still redraw to keep the contents live
InvalidateRect ( g_hWndLiveZoomMag , nullptr , TRUE ) ;
break ;
}
GetCursorPos ( & cursorPos ) ;
2025-11-10 11:57:13 -08:00
// Reclaim topmost status, to prevent unmagnified menus from remaining in view.
2025-01-16 20:52:24 +00:00
memset ( & matrix , 0 , sizeof ( matrix ) ) ;
if ( ! g_fullScreenWorkaround ) {
pSetLayeredWindowAttributes ( hWnd , 0 , 255 , LWA_ALPHA ) ;
SetWindowPos ( hWnd , HWND_TOPMOST , 0 , 0 , 0 , 0 ,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
OutputDebug ( L " LIVEZOOM RECLAIM \n " ) ;
}
sourceRectWidth = lastSourceRect . right - lastSourceRect . left ;
sourceRectHeight = lastSourceRect . bottom - lastSourceRect . top ;
moveWidth = sourceRectWidth / LIVEZOOM_MOVE_REGIONS ;
moveHeight = sourceRectHeight / LIVEZOOM_MOVE_REGIONS ;
curTickCount = GetTickCount ( ) ;
2025-11-10 11:57:13 -08:00
if ( zoomLevel ! = zoomTelescopeTarget & &
2025-01-16 20:52:24 +00:00
( prevZoomStepTickCount = = 0 | | ( curTickCount - prevZoomStepTickCount > ZOOM_LEVEL_STEP_TIME ) ) ) {
prevZoomStepTickCount = curTickCount ;
if ( ( zoomTelescopeStep > 1 & & zoomLevel * zoomTelescopeStep > = zoomTelescopeTarget ) | |
( zoomTelescopeStep < 1 & & zoomLevel * zoomTelescopeStep < = zoomTelescopeTarget ) ) {
zoomLevel = zoomTelescopeTarget ;
} else {
zoomLevel * = zoomTelescopeStep ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
// Time to exit zoom mode?
if ( zoomTelescopeTarget = = 1 & & zoomLevel = = 1 ) {
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( g_RecordToggle )
g_LiveZoomLevelOne = true ;
else
# endif
ShowWindow ( hWnd , SW_HIDE ) ;
} else {
matrix . v [ 0 ] [ 0 ] = zoomLevel ;
matrix . v [ 0 ] [ 2 ] = ( static_cast < float > ( - lastSourceRect . left ) * zoomLevel ) ;
matrix . v [ 1 ] [ 1 ] = zoomLevel ;
matrix . v [ 1 ] [ 2 ] = ( static_cast < float > ( - lastSourceRect . top ) * zoomLevel ) ;
matrix . v [ 2 ] [ 2 ] = 1.0f ;
}
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
//
// Pre-adjust for monitor boundary
//
adjustedCursorPos . x = cursorPos . x - monInfo . rcMonitor . left ;
adjustedCursorPos . y = cursorPos . y - monInfo . rcMonitor . top ;
2025-11-10 11:57:13 -08:00
GetZoomedTopLeftCoordinates ( zoomLevel , & adjustedCursorPos , reinterpret_cast < int * > ( & zoomCenterPos . x ) , width ,
2025-01-16 20:52:24 +00:00
reinterpret_cast < int * > ( & zoomCenterPos . y ) , height ) ;
//
// Add back monitor boundary
//
zoomCenterPos . x + = monInfo . rcMonitor . left + static_cast < LONG > ( width / zoomLevel / 2 ) ;
zoomCenterPos . y + = monInfo . rcMonitor . top + static_cast < LONG > ( height / zoomLevel / 2 ) ;
} else {
int xOffset = cursorPos . x - lastSourceRect . left ;
int yOffset = cursorPos . y - lastSourceRect . top ;
2025-11-10 11:57:13 -08:00
zoomCenterPos . x = 0 ;
zoomCenterPos . y = 0 ;
if ( xOffset < moveWidth )
2025-01-16 20:52:24 +00:00
zoomCenterPos . x = lastSourceRect . left + sourceRectWidth / 2 - ( moveWidth - xOffset ) ;
2025-11-10 11:57:13 -08:00
else if ( xOffset > moveWidth * ( LIVEZOOM_MOVE_REGIONS - 1 ) )
2025-01-16 20:52:24 +00:00
zoomCenterPos . x = lastSourceRect . left + sourceRectWidth / 2 + ( xOffset - moveWidth * ( LIVEZOOM_MOVE_REGIONS - 1 ) ) ;
if ( yOffset < moveHeight )
zoomCenterPos . y = lastSourceRect . top + sourceRectHeight / 2 - ( moveHeight - yOffset ) ;
else if ( yOffset > moveHeight * ( LIVEZOOM_MOVE_REGIONS - 1 ) )
zoomCenterPos . y = lastSourceRect . top + sourceRectHeight / 2 + ( yOffset - moveHeight * ( LIVEZOOM_MOVE_REGIONS - 1 ) ) ;
}
if ( matrix . v [ 0 ] [ 0 ] | | zoomCenterPos . x | | zoomCenterPos . y ) {
2025-11-10 11:57:13 -08:00
if ( zoomCenterPos . y = = 0 )
2025-01-16 20:52:24 +00:00
zoomCenterPos . y = lastSourceRect . top + sourceRectHeight / 2 ;
if ( zoomCenterPos . x = = 0 )
zoomCenterPos . x = lastSourceRect . left + sourceRectWidth / 2 ;
int zoomWidth = static_cast < int > ( width / zoomLevel ) ;
int zoomHeight = static_cast < int > ( height / zoomLevel ) ;
sourceRect . left = zoomCenterPos . x - zoomWidth / 2 ;
sourceRect . top = zoomCenterPos . y - zoomHeight / 2 ;
// Don't scroll outside desktop area.
2025-11-10 11:57:13 -08:00
if ( sourceRect . left < monInfo . rcMonitor . left )
2025-01-16 20:52:24 +00:00
sourceRect . left = monInfo . rcMonitor . left ;
else if ( sourceRect . left > monInfo . rcMonitor . right - zoomWidth )
sourceRect . left = monInfo . rcMonitor . right - zoomWidth ;
sourceRect . right = sourceRect . left + zoomWidth ;
2025-11-10 11:57:13 -08:00
if ( sourceRect . top < monInfo . rcMonitor . top )
2025-01-16 20:52:24 +00:00
sourceRect . top = monInfo . rcMonitor . top ;
2025-11-10 11:57:13 -08:00
else if ( sourceRect . top > monInfo . rcMonitor . bottom - zoomHeight )
2025-01-16 20:52:24 +00:00
sourceRect . top = monInfo . rcMonitor . bottom - zoomHeight ;
sourceRect . bottom = sourceRect . top + zoomHeight ;
if ( g_ZoomOnLiveZoom ) {
matrix . v [ 0 ] [ 0 ] = static_cast < float > ( 1.0 ) ;
matrix . v [ 0 ] [ 2 ] = ( static_cast < float > ( - monInfo . rcMonitor . left ) ) ;
matrix . v [ 1 ] [ 1 ] = static_cast < float > ( 1.0 ) ;
matrix . v [ 1 ] [ 2 ] = ( static_cast < float > ( - monInfo . rcMonitor . top ) ) ;
matrix . v [ 2 ] [ 2 ] = 1.0f ;
} else if ( lastSourceRect . left ! = sourceRect . left | |
lastSourceRect . top ! = sourceRect . top ) {
matrix . v [ 0 ] [ 0 ] = zoomLevel ;
matrix . v [ 0 ] [ 2 ] = ( static_cast < float > ( - sourceRect . left ) * zoomLevel ) ;
matrix . v [ 1 ] [ 1 ] = zoomLevel ;
matrix . v [ 1 ] [ 2 ] = ( static_cast < float > ( - sourceRect . top ) * zoomLevel ) ;
matrix . v [ 2 ] [ 2 ] = 1.0f ;
}
lastSourceRect = sourceRect ;
}
lastCursorPos = cursorPos ;
// Update source and zoom if necessary
if ( matrix . v [ 0 ] [ 0 ] ) {
OutputDebug ( L " LIVEZOOM update \n " ) ;
if ( g_fullScreenWorkaround ) {
pMagSetFullscreenTransform ( zoomLevel , sourceRect . left , sourceRect . top ) ;
pMagSetInputTransform ( TRUE , & sourceRect , & monInfo . rcMonitor ) ;
}
else {
pMagSetWindowTransform ( g_hWndLiveZoomMag , & matrix ) ;
}
}
if ( ! g_fullScreenWorkaround ) {
// Force redraw to refresh screen contents
InvalidateRect ( g_hWndLiveZoomMag , NULL , TRUE ) ;
}
// are we done zooming?
if ( zoomLevel = = 1 ) {
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( g_RecordToggle ) {
g_LiveZoomLevelOne = true ;
}
else {
# endif
if ( g_OsVersion < WIN7_VERSION ) {
ShowWindow ( hWnd , SW_HIDE ) ;
} else {
DestroyWindow ( hWnd ) ;
}
}
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
}
# endif
}
break ;
case 1 : {
if ( ! IsWindowVisible ( hWnd ) ) {
// This is the cached window timeout. If not in presentation mode,
// time to exit
if ( ! IsPresentationMode ( ) ) {
DestroyWindow ( hWnd ) ;
}
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
}
break ;
}
break ;
case WM_SETTINGCHANGE :
if ( g_OsVersion < WIN7_VERSION ) {
if ( startedInPresentationMode & & ! IsPresentationMode ( ) ) {
// Existing presentation mode
DestroyWindow ( hWnd ) ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
} else if ( ! startedInPresentationMode & & IsPresentationMode ( ) ) {
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
// Kill the timer if one was configured, because now
// we're going to go away when they exit presentation mode
KillTimer ( hWnd , 1 ) ;
}
}
break ;
case WM_HOTKEY : {
float newZoomLevel = zoomLevel ;
switch ( wParam ) {
case 0 :
2025-11-10 11:57:13 -08:00
// zoom in
if ( newZoomLevel < ZOOM_LEVEL_MAX )
2025-01-16 20:52:24 +00:00
newZoomLevel * = 2 ;
zoomTelescopeStep = ZOOM_LEVEL_STEP_IN ;
break ;
case 1 :
2025-11-10 11:57:13 -08:00
if ( newZoomLevel > 2 )
2025-01-16 20:52:24 +00:00
newZoomLevel / = 2 ;
else {
2025-11-10 11:57:13 -08:00
newZoomLevel * = .75 ;
if ( newZoomLevel < ZOOM_LEVEL_MIN )
2025-01-16 20:52:24 +00:00
newZoomLevel = ZOOM_LEVEL_MIN ;
}
zoomTelescopeStep = ZOOM_LEVEL_STEP_OUT ;
break ;
}
zoomTelescopeTarget = newZoomLevel ;
if ( ! dwmEnabled ) {
zoomLevel = newZoomLevel ;
}
}
break ;
// NOTE: keyboard and mouse input actually don't get sent to us at all when in live zoom mode
case WM_KEYDOWN :
switch ( wParam ) {
case VK_ESCAPE :
zoomTelescopeStep = ZOOM_LEVEL_STEP_OUT ;
zoomTelescopeTarget = 1.0 ;
if ( ! dwmEnabled ) {
zoomLevel = static_cast < float > ( 1.1 ) ;
}
break ;
case VK_UP :
2025-11-10 11:57:13 -08:00
SendMessage ( hWnd , WM_MOUSEWHEEL ,
2025-01-16 20:52:24 +00:00
MAKEWPARAM ( GetAsyncKeyState ( VK_LCONTROL ) ! = 0 ? MK_CONTROL : 0 , WHEEL_DELTA ) , 0 ) ;
return TRUE ;
case VK_DOWN :
2025-11-10 11:57:13 -08:00
SendMessage ( hWnd , WM_MOUSEWHEEL ,
2025-01-16 20:52:24 +00:00
MAKEWPARAM ( GetAsyncKeyState ( VK_LCONTROL ) ! = 0 ? MK_CONTROL : 0 , - WHEEL_DELTA ) , 0 ) ;
return TRUE ;
}
break ;
case WM_DESTROY :
g_hWndLiveZoom = NULL ;
break ;
2025-11-10 11:57:13 -08:00
2025-01-16 20:52:24 +00:00
case WM_SIZE :
GetClientRect ( hWnd , & rc ) ;
2025-11-10 11:57:13 -08:00
SetWindowPos ( g_hWndLiveZoomMag , NULL ,
2025-01-16 20:52:24 +00:00
rc . left , rc . top , rc . right , rc . bottom , 0 ) ;
break ;
case WM_USER_GET_ZOOM_LEVEL :
return reinterpret_cast < LRESULT > ( & zoomLevel ) ;
case WM_USER_GET_SOURCE_RECT :
return reinterpret_cast < LRESULT > ( & lastSourceRect ) ;
case WM_USER_MAGNIFY_CURSOR :
{
auto style = GetWindowLong ( g_hWndLiveZoomMag , GWL_STYLE ) ;
if ( wParam = = TRUE )
{
style | = MS_SHOWMAGNIFIEDCURSOR ;
}
else
{
style & = ~ MS_SHOWMAGNIFIEDCURSOR ;
}
SetWindowLong ( g_hWndLiveZoomMag , GWL_STYLE , style ) ;
InvalidateRect ( g_hWndLiveZoomMag , nullptr , TRUE ) ;
RedrawWindow ( hWnd , nullptr , nullptr , RDW_ALLCHILDREN | RDW_UPDATENOW ) ;
}
break ;
case WM_USER_SET_ZOOM :
{
if ( g_RecordToggle )
{
g_SelectRectangle . UpdateOwner ( hWnd ) ;
}
if ( lParam ! = NULL ) {
lastSourceRect = * reinterpret_cast < RECT * > ( lParam ) ;
}
# if WINDOWS_CURSOR_RECORDING_WORKAROUND
if ( g_LiveZoomLevelOne ) {
g_LiveZoomLevelOne = FALSE ;
zoomTelescopeTarget = static_cast < float > ( wParam ) ;
zoomTelescopeStep = ZOOM_LEVEL_STEP_IN ;
prevZoomStepTickCount = 0 ;
zoomLevel = 1.0 ;
break ;
}
# endif
zoomLevel = static_cast < float > ( wParam ) ;
zoomTelescopeTarget = zoomLevel ;
matrix . v [ 0 ] [ 0 ] = zoomLevel ;
matrix . v [ 0 ] [ 2 ] = ( static_cast < float > ( - lastSourceRect . left ) * static_cast < float > ( wParam ) ) ;
matrix . v [ 1 ] [ 1 ] = zoomLevel ;
matrix . v [ 1 ] [ 2 ] = ( static_cast < float > ( - lastSourceRect . top ) * static_cast < float > ( wParam ) ) ;
matrix . v [ 2 ] [ 2 ] = 1.0f ;
if ( g_fullScreenWorkaround ) {
pMagSetFullscreenTransform ( zoomLevel , lastSourceRect . left , lastSourceRect . top ) ;
pMagSetInputTransform ( TRUE , & lastSourceRect , & monInfo . rcMonitor ) ;
}
else {
pMagSetWindowTransform ( g_hWndLiveZoomMag , & matrix ) ;
}
}
break ;
default :
return DefWindowProc ( hWnd , message , wParam , lParam ) ;
}
2025-11-10 11:57:13 -08:00
return 0 ;
2025-01-16 20:52:24 +00:00
}
//----------------------------------------------------------------------------
//
// Wrapper functions for explicit linking to d3d11.dll
//
//----------------------------------------------------------------------------
HRESULT __stdcall WrapCreateDirect3D11DeviceFromDXGIDevice (
IDXGIDevice * dxgiDevice ,
IInspectable * * graphicsDevice )
{
if ( pCreateDirect3D11DeviceFromDXGIDevice = = nullptr )
return E_NOINTERFACE ;
return pCreateDirect3D11DeviceFromDXGIDevice ( dxgiDevice , graphicsDevice ) ;
}
HRESULT __stdcall WrapCreateDirect3D11SurfaceFromDXGISurface (
IDXGISurface * dxgiSurface ,
IInspectable * * graphicsSurface )
{
if ( pCreateDirect3D11SurfaceFromDXGISurface = = nullptr )
return E_NOINTERFACE ;
return pCreateDirect3D11SurfaceFromDXGISurface ( dxgiSurface , graphicsSurface ) ;
}
HRESULT __stdcall WrapD3D11CreateDevice (
IDXGIAdapter * pAdapter ,
D3D_DRIVER_TYPE DriverType ,
HMODULE Software ,
UINT Flags ,
const D3D_FEATURE_LEVEL * pFeatureLevels ,
UINT FeatureLevels ,
UINT SDKVersion ,
ID3D11Device * * ppDevice ,
D3D_FEATURE_LEVEL * pFeatureLevel ,
ID3D11DeviceContext * * ppImmediateContext )
{
if ( pD3D11CreateDevice = = nullptr )
return E_NOINTERFACE ;
return pD3D11CreateDevice ( pAdapter , DriverType , Software , Flags , pFeatureLevels ,
FeatureLevels , SDKVersion , ppDevice , pFeatureLevel , ppImmediateContext ) ;
}
//----------------------------------------------------------------------------
//
// InitInstance
//
//----------------------------------------------------------------------------
2025-11-10 11:57:13 -08:00
HWND InitInstance ( HINSTANCE hInstance , int nCmdShow )
2025-01-16 20:52:24 +00:00
{
WNDCLASS wcZoomIt ;
HWND hWndMain ;
g_hInstance = hInstance ;
// If magnification, set default hotkey for live zoom
if ( pMagInitialize ) {
// register live zoom host window
wcZoomIt . style = CS_HREDRAW | CS_VREDRAW ;
wcZoomIt . lpfnWndProc = LiveZoomWndProc ;
wcZoomIt . cbClsExtra = 0 ;
wcZoomIt . cbWndExtra = 0 ;
wcZoomIt . hInstance = hInstance ;
2025-11-10 11:57:13 -08:00
wcZoomIt . hIcon = 0 ;
2025-01-16 20:52:24 +00:00
wcZoomIt . hCursor = LoadCursor ( NULL , IDC_ARROW ) ;
wcZoomIt . hbrBackground = NULL ;
wcZoomIt . lpszMenuName = NULL ;
wcZoomIt . lpszClassName = L " MagnifierClass " ;
RegisterClass ( & wcZoomIt ) ;
} else {
g_LiveZoomToggleKey = 0 ;
}
2025-11-10 11:57:13 -08:00
wcZoomIt . style = 0 ;
wcZoomIt . lpfnWndProc = ( WNDPROC ) MainWndProc ;
wcZoomIt . cbClsExtra = 0 ;
wcZoomIt . cbWndExtra = 0 ;
2025-01-16 20:52:24 +00:00
wcZoomIt . hInstance = hInstance ; wcZoomIt . hIcon = NULL ;
wcZoomIt . hCursor = LoadCursor ( hInstance , L " NULLCURSOR " ) ;
wcZoomIt . hbrBackground = NULL ;
2025-11-10 11:57:13 -08:00
wcZoomIt . lpszMenuName = NULL ;
2025-01-16 20:52:24 +00:00
wcZoomIt . lpszClassName = L " ZoomitClass " ;
if ( ! RegisterClass ( & wcZoomIt ) )
return FALSE ;
2025-11-10 11:57:13 -08:00
hWndMain = CreateWindowEx ( WS_EX_TOOLWINDOW , L " ZoomitClass " ,
L " Zoomit Zoom Window " ,
2025-01-16 20:52:24 +00:00
WS_POPUP ,
0 , 0 ,
2025-11-10 11:57:13 -08:00
0 , 0 ,
NULL ,
NULL ,
hInstance ,
2025-01-16 20:52:24 +00:00
NULL ) ;
2025-11-10 11:57:13 -08:00
// If window could not be created, return "failure"
2025-01-16 20:52:24 +00:00
if ( ! hWndMain )
return NULL ;
2025-11-10 11:57:13 -08:00
// Make the window visible; update its client area; and return "success"
2025-01-16 20:52:24 +00:00
ShowWindow ( hWndMain , SW_HIDE ) ;
// Add tray icon
EnableDisableTrayIcon ( hWndMain , g_ShowTrayIcon ) ;
2025-11-10 11:57:13 -08:00
return hWndMain ;
2025-01-16 20:52:24 +00:00
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
2025-12-23 21:07:44 +08:00
// Dispatch commands coming from the PowerToys IPC channel.
# ifdef __ZOOMIT_POWERTOYS__
void ZoomIt_DispatchCommand ( ZoomItCommand cmd )
{
auto post_hotkey = [ ] ( WPARAM id )
{
if ( g_hWndMain ! = nullptr )
{
PostMessage ( g_hWndMain , WM_HOTKEY , id , 0 ) ;
}
} ;
switch ( cmd )
{
case ZoomItCommand : : Zoom :
if ( g_hWndMain ! = nullptr )
{
PostMessage ( g_hWndMain , WM_COMMAND , IDC_ZOOM , 0 ) ;
}
Trace : : ZoomItActivateZoom ( ) ;
break ;
case ZoomItCommand : : Draw :
post_hotkey ( DRAW_HOTKEY ) ;
Trace : : ZoomItActivateDraw ( ) ;
break ;
case ZoomItCommand : : Break :
post_hotkey ( BREAK_HOTKEY ) ;
Trace : : ZoomItActivateBreak ( ) ;
break ;
case ZoomItCommand : : LiveZoom :
post_hotkey ( LIVE_HOTKEY ) ;
Trace : : ZoomItActivateLiveZoom ( ) ;
break ;
case ZoomItCommand : : Snip :
post_hotkey ( SNIP_HOTKEY ) ;
Trace : : ZoomItActivateSnip ( ) ;
break ;
2026-03-26 13:21:43 +01:00
case ZoomItCommand : : SnipOcr :
post_hotkey ( SNIP_OCR_HOTKEY ) ;
Trace : : ZoomItActivateSnipOcr ( ) ;
break ;
2025-12-23 21:07:44 +08:00
case ZoomItCommand : : Record :
post_hotkey ( RECORD_HOTKEY ) ;
Trace : : ZoomItActivateRecord ( ) ;
break ;
default :
break ;
}
}
# endif
2025-01-16 20:52:24 +00:00
//----------------------------------------------------------------------------
//
// WinMain
//
//----------------------------------------------------------------------------
int APIENTRY wWinMain ( _In_ HINSTANCE hInstance , _In_opt_ HINSTANCE hPrevInstance ,
_In_ PWSTR lpCmdLine , _In_ int nCmdShow )
{
2025-11-10 11:57:13 -08:00
MSG msg ;
2025-01-16 20:52:24 +00:00
HACCEL hAccel ;
2026-03-26 13:21:43 +01:00
// Enable panorama frame/log dumps in release builds when requested.
if ( lpCmdLine ! = nullptr & & wcsstr ( lpCmdLine , L " /panorama-debug " ) ! = nullptr )
{
g_PanoramaDebugMode = true ;
}
# ifdef _DEBUG
if ( lpCmdLine ! = nullptr & & wcsstr ( lpCmdLine , L " /panorama-selftest " ) ! = nullptr )
{
const bool selfTestPassed = RunPanoramaStitchSelfTest ( ) ;
return selfTestPassed ? 0 : 2 ;
}
if ( lpCmdLine ! = nullptr & & wcsstr ( lpCmdLine , L " /panorama-stitch-latest " ) ! = nullptr )
{
const bool replayStitchPassed = RunPanoramaStitchLatestDebugDump ( ) ;
return replayStitchPassed ? 0 : 3 ;
}
{
const wchar_t * replayArg = lpCmdLine ! = nullptr ? wcsstr ( lpCmdLine , L " /panorama-stitch-replay " ) : nullptr ;
if ( replayArg ! = nullptr )
{
const wchar_t * path = replayArg + wcslen ( L " /panorama-stitch-replay " ) ;
while ( * path = = L ' ' | | * path = = L ' \" ' ) + + path ;
std : : wstring replayPath ( path ) ;
while ( ! replayPath . empty ( ) & & ( replayPath . back ( ) = = L ' \" ' | | replayPath . back ( ) = = L ' ' ) )
replayPath . pop_back ( ) ;
const bool ok = RunPanoramaStitchDumpDirectory ( replayPath . c_str ( ) ) ;
return ok ? 0 : 3 ;
}
}
# endif // _DEBUG
2025-01-16 20:52:24 +00:00
if ( ! ShowEula ( APPNAME , NULL , NULL ) ) return 1 ;
# ifdef __ZOOMIT_POWERTOYS__
2025-01-21 11:26:23 +00:00
if ( powertoys_gpo : : getConfiguredZoomItEnabledValue ( ) = = powertoys_gpo : : gpo_rule_configured_disabled )
{
Logger : : warn ( L " Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator. " ) ;
return 1 ;
}
2025-01-16 20:52:24 +00:00
Shared : : Trace : : ETWTrace * trace = nullptr ;
std : : wstring pid = std : : wstring ( lpCmdLine ) ; // The PowerToys pid is the argument to the process.
auto mainThreadId = GetCurrentThreadId ( ) ;
if ( ! pid . empty ( ) )
{
g_StartedByPowerToys = TRUE ;
trace = new Shared : : Trace : : ETWTrace ( ) ;
Trace : : RegisterProvider ( ) ;
trace - > UpdateState ( true ) ;
Trace : : ZoomItStarted ( ) ;
// Initialize logger
LoggerHelpers : : init_logger ( L " ZoomIt " , L " " , LogSettings : : zoomItLoggerName ) ;
ProcessWaiter : : OnProcessTerminate ( pid , [ mainThreadId ] ( int err ) {
if ( err ! = ERROR_SUCCESS )
{
Logger : : error ( L " Failed to wait for parent process exit. {} " , get_last_error_or_default ( err ) ) ;
}
else
{
Logger : : trace ( L " PowerToys runner exited. " ) ;
}
Logger : : trace ( L " Exiting ZoomIt " ) ;
PostThreadMessage ( mainThreadId , WM_QUIT , 0 , 0 ) ;
} ) ;
}
# endif // __ZOOMIT_POWERTOYS__
# ifndef _WIN64
if ( ! g_StartedByPowerToys )
{
// Launch 64-bit version if necessary
SetAutostartFilePath ( ) ;
if ( RunningOnWin64 ( ) ) {
// Record where we are if we're the 32-bit version
return Run64bitVersion ( ) ;
}
}
# endif
// Single instance per desktop
if ( ! CreateEvent ( NULL , FALSE , FALSE , _T ( " Local \\ ZoomitActive " ) ) ) {
CreateEvent ( NULL , FALSE , FALSE , _T ( " ZoomitActive " ) ) ;
2025-11-10 11:57:13 -08:00
}
2025-01-16 20:52:24 +00:00
if ( GetLastError ( ) = = ERROR_ALREADY_EXISTS ) {
if ( g_StartedByPowerToys )
{
MessageBox ( NULL , L " We've detected another instance of ZoomIt is already running. \n Can't start a new ZoomIt instance from PowerToys. " ,
APPNAME , MB_ICONERROR | MB_SETFOREGROUND ) ;
return 1 ;
}
// Tell the other instance to show the options dialog
g_hWndMain = FindWindow ( L " ZoomitClass " , NULL ) ;
if ( g_hWndMain ! = NULL ) {
PostMessage ( g_hWndMain , WM_COMMAND , IDC_OPTIONS , 0 ) ;
int count = 0 ;
while ( count + + < 5 ) {
HWND local_hWndOptions = FindWindow ( NULL , L " ZoomIt - Sysinternals: www.sysinternals.com " ) ;
if ( local_hWndOptions ) {
SetForegroundWindow ( local_hWndOptions ) ;
2025-11-10 11:57:13 -08:00
SetWindowPos ( local_hWndOptions , HWND_TOPMOST , 0 , 0 , 0 , 0 , SWP_NOSIZE | SWP_NOMOVE | SWP_SHOWWINDOW ) ;
2025-01-16 20:52:24 +00:00
break ;
}
Sleep ( 100 ) ;
}
}
return 0 ;
}
g_OsVersion = GetVersion ( ) & 0xFFFF ;
// load accelerators
hAccel = LoadAccelerators ( hInstance , TEXT ( " ACCELERATORS " ) ) ;
if ( FAILED ( CoInitialize ( 0 ) ) )
{
return 0 ;
}
pEnableThemeDialogTexture = ( type_pEnableThemeDialogTexture ) GetProcAddress ( GetModuleHandle ( L " uxtheme.dll " ) ,
" EnableThemeDialogTexture " ) ;
2026-02-03 13:05:31 -08:00
// Initialize dark mode support early, before any windows are created
// This is required for popup menus to use dark mode
InitializeDarkMode ( ) ;
2025-01-16 20:52:24 +00:00
pMonitorFromPoint = ( type_MonitorFromPoint ) GetProcAddress ( LoadLibrarySafe ( L " User32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MonitorFromPoint " ) ;
pGetMonitorInfo = ( type_pGetMonitorInfo ) GetProcAddress ( LoadLibrarySafe ( L " User32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" GetMonitorInfoA " ) ;
pSHAutoComplete = ( type_pSHAutoComplete ) GetProcAddress ( LoadLibrarySafe ( L " Shlwapi.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" SHAutoComplete " ) ;
pSetLayeredWindowAttributes = ( type_pSetLayeredWindowAttributes ) GetProcAddress ( LoadLibrarySafe ( L " user32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" SetLayeredWindowAttributes " ) ;
pMagSetWindowSource = ( type_pMagSetWindowSource ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagSetWindowSource " ) ;
pGetPointerType = ( type_pGetPointerType ) GetProcAddress ( LoadLibrarySafe ( L " user32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" GetPointerType " ) ;
pGetPointerPenInfo = ( type_pGetPointerPenInfo ) GetProcAddress ( LoadLibrarySafe ( L " user32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" GetPointerPenInfo " ) ;
pMagInitialize = ( type_pMagInitialize ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagInitialize " ) ;
pMagSetWindowTransform = ( type_pMagSetWindowTransform ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagSetWindowTransform " ) ;
pMagSetFullscreenTransform = ( type_pMagSetFullscreenTransform ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagSetFullscreenTransform " ) ;
2025-10-07 11:20:00 -07:00
pMagSetFullscreenUseBitmapSmoothing = ( type_MagSetFullscreenUseBitmapSmoothing ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagSetFullscreenUseBitmapSmoothing " ) ;
pMagSetLensUseBitmapSmoothing = ( type_pMagSetLensUseBitmapSmoothing ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagSetLensUseBitmapSmoothing " ) ;
2025-01-16 20:52:24 +00:00
pMagSetInputTransform = ( type_pMagSetInputTransform ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagSetInputTransform " ) ;
pMagShowSystemCursor = ( type_pMagShowSystemCursor ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagShowSystemCursor " ) ;
pMagSetWindowFilterList = ( type_pMagSetWindowFilterList ) GetProcAddress ( LoadLibrarySafe ( L " magnification.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" MagSetWindowFilterList " ) ;
pSHQueryUserNotificationState = ( type_pSHQueryUserNotificationState ) GetProcAddress ( LoadLibrarySafe ( L " shell32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" SHQueryUserNotificationState " ) ;
pDwmIsCompositionEnabled = ( type_pDwmIsCompositionEnabled ) GetProcAddress ( LoadLibrarySafe ( L " dwmapi.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" DwmIsCompositionEnabled " ) ;
pSetProcessDPIAware = ( type_pSetProcessDPIAware ) GetProcAddress ( LoadLibrarySafe ( L " User32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" SetProcessDPIAware " ) ;
pSystemParametersInfoForDpi = ( type_pSystemParametersInfoForDpi ) GetProcAddress ( LoadLibrarySafe ( L " User32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" SystemParametersInfoForDpi " ) ;
pGetDpiForWindow = ( type_pGetDpiForWindow ) GetProcAddress ( LoadLibrarySafe ( L " User32.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" GetDpiForWindow " ) ;
pCreateDirect3D11DeviceFromDXGIDevice = ( type_pCreateDirect3D11DeviceFromDXGIDevice ) GetProcAddress ( LoadLibrarySafe ( L " d3d11.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" CreateDirect3D11DeviceFromDXGIDevice " ) ;
pCreateDirect3D11SurfaceFromDXGISurface = ( type_pCreateDirect3D11SurfaceFromDXGISurface ) GetProcAddress ( LoadLibrarySafe ( L " d3d11.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" CreateDirect3D11SurfaceFromDXGISurface " ) ;
pD3D11CreateDevice = ( type_pD3D11CreateDevice ) GetProcAddress ( LoadLibrarySafe ( L " d3d11.dll " , DLL_LOAD_LOCATION_SYSTEM ) ,
" D3D11CreateDevice " ) ;
// Windows Server 2022 (and including Windows 11) introduced a bug where the cursor disappears
// in live zoom. Use the full-screen magnifier as a workaround on those versions only. It is
// currently impractical as a replacement; it requires calling MagSetInputTransform for all
2026-02-03 13:05:31 -08:00
// input to be transformed. Else, some hit-testing is misdirected. MagSetInputTransform
2025-01-16 20:52:24 +00:00
// fails without token UI access, which is impractical; it requires copying the executable
// under either %ProgramFiles% or %SystemRoot%, which requires elevation.
//
// TODO: Update the Windows 11 21H2 revision check when the final number is known. Also add a
// check for the Windows Server 2022 revision if that bug (https://task.ms/38611091) is
// fixed.
DWORD windowsRevision , windowsBuild = GetWindowsBuild ( & windowsRevision ) ;
if ( ( windowsBuild = = BUILD_WINDOWS_SERVER_2022 ) | |
( ( windowsBuild = = BUILD_WINDOWS_11_21H2 ) & & ( windowsRevision < 829 ) ) ) {
if ( pMagSetFullscreenTransform & & pMagSetInputTransform )
g_fullScreenWorkaround = TRUE ;
}
# if 1
// Calling this causes Windows to mess with our query of monitor height and width
if ( pSetProcessDPIAware ) {
pSetProcessDPIAware ( ) ;
}
# endif
/* Perform initializations that apply to a specific instance */
g_hWndMain = InitInstance ( hInstance , nCmdShow ) ;
if ( ! g_hWndMain )
return FALSE ;
# ifdef __ZOOMIT_POWERTOYS__
HANDLE m_reload_settings_event_handle = NULL ;
HANDLE m_exit_event_handle = NULL ;
2025-12-23 21:07:44 +08:00
HANDLE m_zoom_event_handle = NULL ;
HANDLE m_draw_event_handle = NULL ;
HANDLE m_break_event_handle = NULL ;
HANDLE m_live_zoom_event_handle = NULL ;
HANDLE m_snip_event_handle = NULL ;
2026-03-26 13:21:43 +01:00
HANDLE m_snip_ocr_event_handle = NULL ;
2025-12-23 21:07:44 +08:00
HANDLE m_record_event_handle = NULL ;
2025-01-16 20:52:24 +00:00
std : : thread m_event_triggers_thread ;
if ( g_StartedByPowerToys ) {
// Start a thread to listen to PowerToys Events.
m_reload_settings_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_REFRESH_SETTINGS_EVENT ) ;
m_exit_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_EXIT_EVENT ) ;
2025-12-23 21:07:44 +08:00
m_zoom_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_ZOOM_EVENT ) ;
m_draw_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_DRAW_EVENT ) ;
m_break_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_BREAK_EVENT ) ;
m_live_zoom_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_LIVEZOOM_EVENT ) ;
m_snip_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_SNIP_EVENT ) ;
2026-03-26 13:21:43 +01:00
m_snip_ocr_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_SNIPOCR_EVENT ) ;
2025-12-23 21:07:44 +08:00
m_record_event_handle = CreateEventW ( nullptr , false , false , CommonSharedConstants : : ZOOMIT_RECORD_EVENT ) ;
2026-03-26 13:21:43 +01:00
if ( ! m_reload_settings_event_handle | | ! m_exit_event_handle | | ! m_zoom_event_handle | | ! m_draw_event_handle | | ! m_break_event_handle | | ! m_live_zoom_event_handle | | ! m_snip_event_handle | | ! m_snip_ocr_event_handle | | ! m_record_event_handle )
2025-01-16 20:52:24 +00:00
{
Logger : : warn ( L " Failed to create events. {} " , get_last_error_or_default ( GetLastError ( ) ) ) ;
return 1 ;
}
2026-03-26 13:21:43 +01:00
const std : : array < HANDLE , 9 > event_handles {
2025-12-23 21:07:44 +08:00
m_reload_settings_event_handle ,
m_exit_event_handle ,
m_zoom_event_handle ,
m_draw_event_handle ,
m_break_event_handle ,
m_live_zoom_event_handle ,
m_snip_event_handle ,
2026-03-26 13:21:43 +01:00
m_snip_ocr_event_handle ,
2025-12-23 21:07:44 +08:00
m_record_event_handle ,
} ;
const DWORD handle_count = static_cast < DWORD > ( event_handles . size ( ) ) ;
m_event_triggers_thread = std : : thread ( [ event_handles , handle_count ] ( ) {
2025-01-16 20:52:24 +00:00
MSG msg ;
while ( g_running )
{
2025-12-23 21:07:44 +08:00
DWORD dwEvt = MsgWaitForMultipleObjects ( handle_count , event_handles . data ( ) , false , INFINITE , QS_ALLINPUT ) ;
if ( dwEvt = = WAIT_FAILED )
{
Logger : : error ( L " ZoomIt event wait failed. {} " , get_last_error_or_default ( GetLastError ( ) ) ) ;
break ;
}
2025-01-16 20:52:24 +00:00
if ( ! g_running )
{
break ;
}
2025-12-23 21:07:44 +08:00
if ( dwEvt = = WAIT_OBJECT_0 + handle_count )
{
if ( PeekMessageW ( & msg , nullptr , 0 , 0 , PM_REMOVE ) )
{
TranslateMessage ( & msg ) ;
DispatchMessageW ( & msg ) ;
}
continue ;
}
2025-01-16 20:52:24 +00:00
switch ( dwEvt )
{
case WAIT_OBJECT_0 :
{
// Reload Settings Event
Logger : : trace ( L " Received a reload settings event. " ) ;
PostMessage ( g_hWndMain , WM_USER_RELOAD_SETTINGS , 0 , 0 ) ;
break ;
}
case WAIT_OBJECT_0 + 1 :
{
// Exit Event
PostMessage ( g_hWndMain , WM_QUIT , 0 , 0 ) ;
break ;
}
case WAIT_OBJECT_0 + 2 :
2025-12-23 21:07:44 +08:00
ZoomIt_DispatchCommand ( ZoomItCommand : : Zoom ) ;
break ;
case WAIT_OBJECT_0 + 3 :
ZoomIt_DispatchCommand ( ZoomItCommand : : Draw ) ;
break ;
case WAIT_OBJECT_0 + 4 :
ZoomIt_DispatchCommand ( ZoomItCommand : : Break ) ;
break ;
case WAIT_OBJECT_0 + 5 :
ZoomIt_DispatchCommand ( ZoomItCommand : : LiveZoom ) ;
break ;
case WAIT_OBJECT_0 + 6 :
ZoomIt_DispatchCommand ( ZoomItCommand : : Snip ) ;
2025-01-16 20:52:24 +00:00
break ;
2025-12-23 21:07:44 +08:00
case WAIT_OBJECT_0 + 7 :
2026-03-26 13:21:43 +01:00
ZoomIt_DispatchCommand ( ZoomItCommand : : SnipOcr ) ;
break ;
case WAIT_OBJECT_0 + 8 :
2025-12-23 21:07:44 +08:00
ZoomIt_DispatchCommand ( ZoomItCommand : : Record ) ;
2025-01-16 20:52:24 +00:00
break ;
2025-12-23 21:07:44 +08:00
default : break ;
2025-01-16 20:52:24 +00:00
}
}
} ) ;
}
# endif // __ZOOMIT_POWERTOYS__
/* Acquire and dispatch messages until a WM_QUIT message is received. */
while ( GetMessage ( & msg , NULL , 0 , 0 ) ) {
if ( ! TranslateAccelerator ( g_hWndMain , hAccel , & msg ) ) {
TranslateMessage ( & msg ) ;
DispatchMessage ( & msg ) ;
}
}
int retCode = ( int ) msg . wParam ;
g_running = FALSE ;
# ifdef __ZOOMIT_POWERTOYS__
if ( g_StartedByPowerToys )
{
if ( trace ! = nullptr ) {
trace - > Flush ( ) ;
delete trace ;
}
Trace : : UnregisterProvider ( ) ;
// Needed to unblock MsgWaitForMultipleObjects one last time
SetEvent ( m_reload_settings_event_handle ) ;
CloseHandle ( m_reload_settings_event_handle ) ;
CloseHandle ( m_exit_event_handle ) ;
2025-12-23 21:07:44 +08:00
CloseHandle ( m_zoom_event_handle ) ;
CloseHandle ( m_draw_event_handle ) ;
CloseHandle ( m_break_event_handle ) ;
CloseHandle ( m_live_zoom_event_handle ) ;
CloseHandle ( m_snip_event_handle ) ;
2026-03-26 13:21:43 +01:00
CloseHandle ( m_snip_ocr_event_handle ) ;
2025-12-23 21:07:44 +08:00
CloseHandle ( m_record_event_handle ) ;
2025-01-16 20:52:24 +00:00
m_event_triggers_thread . join ( ) ;
}
# endif // __ZOOMIT_POWERTOYS__
return retCode ;
}