Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/devdocs/modules/cropandlock.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Creates a window showing the selected area of the original window. Changes in th
### Reparent Mode
Creates a window that replaces the original window, showing only the selected area. The application is controlled through the cropped window.

### Screenshot Mode
Creates a window showing a freezed snapshot of the original window.

## Code Structure

### Project Layout
Expand All @@ -30,6 +33,7 @@ The Crop and Lock module is part of the PowerToys solution. All the logic-relate
- **OverlayWindow.cpp**: Thumbnail module type's window concrete implementation.
- **ReparentCropAndLockWindow.cpp**: Defines the UI for the reparent mode.
- **ChildWindow.cpp**: Reparent module type's window concrete implementation.
- **ScreenshotCropAndLockWindow.cpp**: Defines the UI for the screenshot mode.

## Known Issues

Expand Down
1 change: 1 addition & 0 deletions src/common/interop/shared_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ namespace CommonSharedConstants
// Path to the events used by CropAndLock
const wchar_t CROP_AND_LOCK_REPARENT_EVENT[] = L"Local\\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36";
const wchar_t CROP_AND_LOCK_THUMBNAIL_EVENT[] = L"Local\\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434";
const wchar_t CROP_AND_LOCK_SCREENSHOT_EVENT[] = L"Local\\PowerToysCropAndLockScreenshotEvent-ff077ab2-8360-4bd1-864a-637389d35593";
const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a";

// Path to the events used by EnvironmentVariables
Expand Down
2 changes: 2 additions & 0 deletions src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
<ClCompile Include="ChildWindow.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="ReparentCropAndLockWindow.cpp" />
<ClCompile Include="ScreenshotCropAndLockWindow.cpp" />
<ClCompile Include="ThumbnailCropAndLockWindow.cpp" />
<ClCompile Include="OverlayWindow.cpp" />
<ClCompile Include="pch.cpp">
Expand All @@ -126,6 +127,7 @@
<ClInclude Include="DisplaysUtil.h" />
<ClInclude Include="ModuleConstants.h" />
<ClInclude Include="ReparentCropAndLockWindow.h" />
<ClInclude Include="ScreenshotCropAndLockWindow.h" />
<ClInclude Include="ThumbnailCropAndLockWindow.h" />
<ClInclude Include="SettingsWindow.h" />
<ClInclude Include="OverlayWindow.h" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ClCompile Include="ReparentCropAndLockWindow.cpp" />
<ClCompile Include="ChildWindow.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="ScreenshotCropAndLockWindow.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
Expand All @@ -28,6 +29,7 @@
<ClInclude Include="trace.h" />
<ClInclude Include="ModuleConstants.h" />
<ClInclude Include="DispatcherQueue.desktop.interop.h" />
<ClInclude Include="ScreenshotCropAndLockWindow.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CropAndLock.rc" />
Expand Down
181 changes: 181 additions & 0 deletions src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#include "pch.h"
#include "ScreenshotCropAndLockWindow.h"

const std::wstring ScreenshotCropAndLockWindow::ClassName = L"CropAndLock.ScreenshotCropAndLockWindow";
std::once_flag ScreenshotCropAndLockWindowClassRegistration;

void ScreenshotCropAndLockWindow::RegisterWindowClass()
{
auto instance = winrt::check_pointer(GetModuleHandleW(nullptr));
WNDCLASSEXW wcex = {};
wcex.cbSize = sizeof(wcex);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.hInstance = instance;
wcex.hIcon = LoadIconW(instance, IDI_APPLICATION);
wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wcex.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
wcex.lpszClassName = ClassName.c_str();
wcex.hIconSm = LoadIconW(wcex.hInstance, IDI_APPLICATION);
winrt::check_bool(RegisterClassExW(&wcex));
}

ScreenshotCropAndLockWindow::ScreenshotCropAndLockWindow(std::wstring const& titleString, int width, int height)
{
auto instance = winrt::check_pointer(GetModuleHandleW(nullptr));

std::call_once(ScreenshotCropAndLockWindowClassRegistration, []() { RegisterWindowClass(); });

auto exStyle = 0;
auto style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN;

RECT rect = { 0, 0, width, height };
winrt::check_bool(AdjustWindowRectEx(&rect, style, false, exStyle));
auto adjustedWidth = rect.right - rect.left;
auto adjustedHeight = rect.bottom - rect.top;

winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), titleString.c_str(), style, CW_USEDEFAULT, CW_USEDEFAULT, adjustedWidth, adjustedHeight, nullptr, nullptr, instance, this));
WINRT_ASSERT(m_window);
}

ScreenshotCropAndLockWindow::~ScreenshotCropAndLockWindow()
{
DestroyWindow(m_window);
}

LRESULT ScreenshotCropAndLockWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam)
{
switch (message)
{
case WM_DESTROY:
if (m_closedCallback != nullptr && !m_destroyed)
{
m_destroyed = true;
m_closedCallback(m_window);
}
break;
case WM_PAINT:
if (m_captured && m_bitmap)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(m_window, &ps);
HDC memDC = CreateCompatibleDC(hdc);
SelectObject(memDC, m_bitmap.get());

RECT clientRect = {};
GetClientRect(m_window, &clientRect);
int clientWidth = clientRect.right - clientRect.left;
int clientHeight = clientRect.bottom - clientRect.top;

int srcWidth = m_destRect.right - m_destRect.left;
int srcHeight = m_destRect.bottom - m_destRect.top;

float srcAspect = static_cast<float>(srcWidth) / srcHeight;
float dstAspect = static_cast<float>(clientWidth) / clientHeight;

int drawWidth = clientWidth;
int drawHeight = static_cast<int>(clientWidth / srcAspect);
if (dstAspect > srcAspect)
{
drawHeight = clientHeight;
drawWidth = static_cast<int>(clientHeight * srcAspect);
}

int offsetX = (clientWidth - drawWidth) / 2;
int offsetY = (clientHeight - drawHeight) / 2;

SetStretchBltMode(hdc, HALFTONE);
StretchBlt(hdc, offsetX, offsetY, drawWidth, drawHeight, memDC, 0, 0, srcWidth, srcHeight, SRCCOPY);
DeleteDC(memDC);
EndPaint(m_window, &ps);
}
break;
default:
return base_type::MessageHandler(message, wparam, lparam);
}
return 0;
}

void ScreenshotCropAndLockWindow::CropAndLock(HWND windowToCrop, RECT cropRect)
{
if (m_captured)
{
return;
}

// Get full window bounds
RECT windowRect{};
winrt::check_hresult(DwmGetWindowAttribute(
windowToCrop,
DWMWA_EXTENDED_FRAME_BOUNDS,
&windowRect,
sizeof(windowRect)));

RECT clientRect = ClientAreaInScreenSpace(windowToCrop);
auto offsetX = clientRect.left - windowRect.left;
auto offsetY = clientRect.top - windowRect.top;

m_sourceRect = {
cropRect.left + offsetX,
cropRect.top + offsetY,
cropRect.right + offsetX,
cropRect.bottom + offsetY
};

int fullWidth = windowRect.right - windowRect.left;
int fullHeight = windowRect.bottom - windowRect.top;

HDC fullDC = CreateCompatibleDC(nullptr);
HBITMAP fullBitmap = CreateCompatibleBitmap(GetDC(nullptr), fullWidth, fullHeight);
HGDIOBJ oldFullBitmap = SelectObject(fullDC, fullBitmap);
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern of selecting objects and restoring them should use RAII or at minimum ensure proper cleanup in case of early returns. Consider wrapping this in a helper class or function to prevent resource leaks if exceptions occur between selection and restoration.

Suggested change
HGDIOBJ oldFullBitmap = SelectObject(fullDC, fullBitmap);
ScopedHDC fullDC(CreateCompatibleDC(nullptr));
HDC screenDC = GetDC(nullptr);
ScopedHBITMAP fullBitmap(CreateCompatibleBitmap(screenDC, fullWidth, fullHeight));
ScopedSelectObject selectFullBitmap(fullDC, fullBitmap);

Copilot uses AI. Check for mistakes.

// Capture full window
winrt::check_bool(PrintWindow(windowToCrop, fullDC, PW_RENDERFULLCONTENT));


// Crop
int cropWidth = m_sourceRect.right - m_sourceRect.left;
int cropHeight = m_sourceRect.bottom - m_sourceRect.top;

HDC cropDC = CreateCompatibleDC(nullptr);
HBITMAP cropBitmap = CreateCompatibleBitmap(GetDC(nullptr), cropWidth, cropHeight);
HGDIOBJ oldCropBitmap = SelectObject(cropDC, cropBitmap);
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern of selecting objects and restoring them should use RAII or at minimum ensure proper cleanup in case of early returns. Consider wrapping this in a helper class or function to prevent resource leaks if exceptions occur between selection and restoration.

Suggested change
HGDIOBJ oldCropBitmap = SelectObject(cropDC, cropBitmap);
ScopedSelectObject scopedCropBitmap(cropDC, cropBitmap);

Copilot uses AI. Check for mistakes.

BitBlt(
cropDC,
0,
0,
cropWidth,
cropHeight,
fullDC,
m_sourceRect.left,
m_sourceRect.top,
SRCCOPY);

SelectObject(fullDC, oldFullBitmap);
DeleteObject(fullBitmap);
DeleteDC(fullDC);

SelectObject(cropDC, oldCropBitmap);
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern of selecting objects and restoring them should use RAII or at minimum ensure proper cleanup in case of early returns. Consider wrapping this in a helper class or function to prevent resource leaks if exceptions occur between selection and restoration.

Suggested change
SelectObject(cropDC, oldCropBitmap);
// Use RAII for fullDC bitmap restoration as well
// (Assuming oldFullBitmap was selected into fullDC earlier)
struct ScopedSelectObjectFull
{
HDC dc;
HGDIOBJ oldObj;
ScopedSelectObjectFull(HDC dc_, HGDIOBJ obj) : dc(dc_), oldObj(obj) {}
~ScopedSelectObjectFull() { if (dc && oldObj) SelectObject(dc, oldObj); }
ScopedSelectObjectFull(const ScopedSelectObjectFull&) = delete;
ScopedSelectObjectFull& operator=(const ScopedSelectObjectFull&) = delete;
};
ScopedSelectObjectFull fullSelect(fullDC, oldFullBitmap);
DeleteObject(fullBitmap);
DeleteDC(fullDC);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern of selecting objects and restoring them should use RAII or at minimum ensure proper cleanup in case of early returns. Consider wrapping this in a helper class or function to prevent resource leaks if exceptions occur between selection and restoration.

Suggested change
SelectObject(cropDC, oldCropBitmap);

Copilot uses AI. Check for mistakes.
DeleteDC(cropDC);
m_bitmap.reset(cropBitmap);

// Resize our window
RECT dest{ 0, 0, cropWidth, cropHeight };
LONG_PTR exStyle = GetWindowLongPtrW(m_window, GWL_EXSTYLE);
LONG_PTR style = GetWindowLongPtrW(m_window, GWL_STYLE);

winrt::check_bool(AdjustWindowRectEx(&dest, static_cast<DWORD>(style), FALSE, static_cast<DWORD>(exStyle)));

winrt::check_bool(SetWindowPos(
m_window, HWND_TOPMOST, 0, 0, dest.right - dest.left, dest.bottom - dest.top, SWP_NOMOVE | SWP_SHOWWINDOW));

m_destRect = { 0, 0, cropWidth, cropHeight };
m_captured = true;
InvalidateRect(m_window, nullptr, FALSE);
}

void ScreenshotCropAndLockWindow::Hide()
{
ShowWindow(m_window, SW_HIDE);
}
29 changes: 29 additions & 0 deletions src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once
#include <robmikh.common/DesktopWindow.h>
#include "CropAndLockWindow.h"

struct ScreenshotCropAndLockWindow : robmikh::common::desktop::DesktopWindow<ScreenshotCropAndLockWindow>, CropAndLockWindow
{
static const std::wstring ClassName;
ScreenshotCropAndLockWindow(std::wstring const& titleString, int width, int height);
~ScreenshotCropAndLockWindow() override;
LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam);

HWND Handle() override { return m_window; }
void CropAndLock(HWND windowToCrop, RECT cropRect) override;
void OnClosed(std::function<void(HWND)> callback) override { m_closedCallback = callback; }

private:
static void RegisterWindowClass();

void Hide();

private:
std::unique_ptr<void, decltype(&DeleteObject)> m_bitmap{ nullptr, &DeleteObject };
RECT m_destRect = {};
RECT m_sourceRect = {};

bool m_captured = false;
bool m_destroyed = false;
std::function<void(HWND)> m_closedCallback;
};
1 change: 1 addition & 0 deletions src/modules/CropAndLock/CropAndLock/SettingsWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ enum class CropAndLockType
{
Reparent,
Thumbnail,
Screenshot,
};
29 changes: 25 additions & 4 deletions src/modules/CropAndLock/CropAndLock/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "SettingsWindow.h"
#include "OverlayWindow.h"
#include "CropAndLockWindow.h"
#include "ScreenshotCropAndLockWindow.h"
#include "ThumbnailCropAndLockWindow.h"
#include "ReparentCropAndLockWindow.h"
#include "ModuleConstants.h"
Expand Down Expand Up @@ -133,6 +134,7 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
// Handles and thread for the events sent from runner
HANDLE m_reparent_event_handle;
HANDLE m_thumbnail_event_handle;
HANDLE m_screenshot_event_handle;
HANDLE m_exit_event_handle;
std::thread m_event_triggers_thread;

Expand Down Expand Up @@ -181,6 +183,11 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
Logger::trace(L"Creating a thumbnail window");
Trace::CropAndLock::CreateThumbnailWindow();
break;
case CropAndLockType::Screenshot:
croppedWindow = std::make_shared<ScreenshotCropAndLockWindow>(title, 800, 600);
Logger::trace(L"Creating a screenshot window");
Trace::CropAndLock::CreateScreenshotWindow();
break;
default:
return;
}
Expand Down Expand Up @@ -215,19 +222,20 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
// Start a thread to listen on the events.
m_reparent_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT);
m_thumbnail_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT);
m_screenshot_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_SCREENSHOT_EVENT);
m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT);
if (!m_reparent_event_handle || !m_thumbnail_event_handle || !m_exit_event_handle)
if (!m_reparent_event_handle || !m_thumbnail_event_handle || !m_screenshot_event_handle || !m_exit_event_handle)
{
Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError()));
return 1;
}

m_event_triggers_thread = std::thread([&]() {
MSG msg;
HANDLE event_handles[3] = { m_reparent_event_handle, m_thumbnail_event_handle, m_exit_event_handle };
HANDLE event_handles[4] = { m_reparent_event_handle, m_thumbnail_event_handle, m_screenshot_event_handle, m_exit_event_handle };
while (m_running)
{
DWORD dwEvt = MsgWaitForMultipleObjects(3, event_handles, false, INFINITE, QS_ALLINPUT);
DWORD dwEvt = MsgWaitForMultipleObjects(4, event_handles, false, INFINITE, QS_ALLINPUT);
if (!m_running)
{
break;
Expand Down Expand Up @@ -259,13 +267,25 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
break;
}
case WAIT_OBJECT_0 + 2:
{
// Screenshot Event
bool enqueueSucceeded = controller.DispatcherQueue().TryEnqueue([&]() {
ProcessCommand(CropAndLockType::Screenshot);
});
if (!enqueueSucceeded)
{
Logger::error("Couldn't enqueue message to screenshot a window.");
}
break;
}
case WAIT_OBJECT_0 + 3:
{
// Exit Event
Logger::trace(L"Received an exit event.");
PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
break;
}
case WAIT_OBJECT_0 + 3:
case WAIT_OBJECT_0 + 4:
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
Expand Down Expand Up @@ -295,6 +315,7 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
SetEvent(m_reparent_event_handle);
CloseHandle(m_reparent_event_handle);
CloseHandle(m_thumbnail_event_handle);
CloseHandle(m_screenshot_event_handle);
CloseHandle(m_exit_event_handle);
m_event_triggers_thread.join();

Expand Down
Loading