[Screen Ruler] Better error handling and reuse D3D device (#20223)

* [Screen Ruler] simplify lines calculation

* [Screen Ruler] Add inches and centimeters support

* [Chore] prefer x64 toolset to avoid hitting C1076

* [Screen Ruler] Allow making screenshots in non-continuous mode

* [Screen Ruler] Use single d3d device for all ops

* [Screen Ruler] remove gpu mutex and clean up screen capturing

* [Screen Ruler] handle and log DXGI initialization failure

* [Screen Ruler] Add unhandled exception handler

* [Screen Ruler] comment out Units of Measure setting

* [Screen Ruler] introduce a separate device dedicated for capturing
This commit is contained in:
Andrey Nekrasov
2022-09-05 15:39:56 +03:00
committed by GitHub
parent feead9c68b
commit 9d7c9c1746
40 changed files with 958 additions and 429 deletions

View File

@@ -346,7 +346,6 @@ CSIDL
csignal
cso
CSRW
cstddef
cstdint
cstdlib
cstring
@@ -402,6 +401,7 @@ DBLEPSILON
DCapture
DCBA
DCOM
dcommon
dcomp
dcompi
DComposition
@@ -490,7 +490,6 @@ dreamsofameaningfullife
drivedetectionwarning
dshow
dst
DState
DTo
dutil
DVASPECT
@@ -522,6 +521,7 @@ DWORDLONG
dworigin
dwrite
dxgi
dxgidebug
dxgiformat
dxguid
ecount
@@ -811,6 +811,7 @@ ICapture
icase
ICEBLUE
IClass
IClosable
ICollection
IColor
ICommand
@@ -881,7 +882,6 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
TEXTEXTRACTOR
imagingdevices
IMain
IMarkdown
@@ -1278,6 +1278,7 @@ Moq
MOUSEACTIVATE
MOUSEHWHEEL
MOUSEINPUT
MOUSELEAVE
MOUSEMOVE
MOUSEWHEEL
MOVESIZEEND
@@ -1632,7 +1633,6 @@ ptd
PTOKEN
PToy
ptr
ptrdiff
ptstr
PVOID
pwa
@@ -1746,6 +1746,7 @@ RIGHTSCROLLBAR
riid
riverar
RKey
RLO
RMENU
RNumber
roadmap
@@ -2063,6 +2064,7 @@ testhost
testprocess
TEXCOORD
textblock
TEXTEXTRACTOR
TEXTINCLUDE
THH
THICKFRAME
@@ -2272,7 +2274,6 @@ wchar
WClass
wcout
wcscat
wcschr
wcscmp
wcscpy
wcslen

View File

@@ -32,6 +32,7 @@
<!-- C++ source compile-specific things for all configurations -->
<PropertyGroup>
<PreferredToolArchitecture>x64</PreferredToolArchitecture>
<VcpkgEnabled>false</VcpkgEnabled>
<ExternalIncludePath>$(MSBuildThisFileFullPath)\..\deps\;$(ExternalIncludePath)</ExternalIncludePath>
</PropertyGroup>

View File

@@ -119,7 +119,7 @@
<?define ImageResizerSparsePackageAssets=LargeTile.png;SmallTile.png;SplashScreen.png;Square150x150Logo.png;Square44x44Logo.png;storelogo.png;Wide310x150Logo.png?>
<?define MeasureToolFiles=CoreMessagingXP.dll;dcompi.dll;dwmcorei.dll;DwmSceneI.dll;DWriteCore.dll;marshal.dll;Microsoft.DirectManipulation.dll;Microsoft.InputStateManager.dll;Microsoft.InteractiveExperiences.Projection.dll;Microsoft.Internal.FrameworkUdk.dll;Microsoft.UI.Composition.OSSupport.dll;Microsoft.UI.Input.dll;Microsoft.UI.Windowing.Core.dll;Microsoft.UI.Xaml.Controls.dll;Microsoft.UI.Xaml.Controls.pri;Microsoft.ui.xaml.dll;Microsoft.UI.Xaml.Internal.dll;Microsoft.UI.Xaml.Phone.dll;Microsoft.ui.xaml.resources.19h1.dll;Microsoft.ui.xaml.resources.common.dll;Microsoft.Web.WebView2.Core.dll;Microsoft.Windows.ApplicationModel.Resources.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.AppNotifications.Projection.dll;Microsoft.Windows.PushNotifications.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WindowsAppRuntime.dll;Microsoft.WindowsAppRuntime.Insights.Resource.dll;Microsoft.WindowsAppRuntime.Release.Net.dll;Microsoft.WinUI.dll;MRM.dll;PowerToys.ManagedCommon.dll;PowerToys.ManagedTelemetry.dll;PowerToys.MeasureToolCore.dll;PowerToys.MeasureToolUI.deps.json;PowerToys.MeasureToolUI.dll;PowerToys.MeasureToolUI.exe;PowerToys.MeasureToolUI.runtimeconfig.json;PushNotificationsLongRunningTask.ProxyStub.dll;resources.pri;System.CodeDom.dll;System.Management.dll;WindowsAppRuntime.png;WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll;WinRT.Runtime.dll;WinUIEdit.dll;WinUIEx.dll;wuceffectsi.dll?>
<?define MeasureToolFiles=CoreMessagingXP.dll;dcompi.dll;dwmcorei.dll;DwmSceneI.dll;DWriteCore.dll;marshal.dll;Microsoft.DirectManipulation.dll;Microsoft.InputStateManager.dll;Microsoft.InteractiveExperiences.Projection.dll;Microsoft.Internal.FrameworkUdk.dll;Microsoft.UI.Composition.OSSupport.dll;Microsoft.UI.Input.dll;Microsoft.UI.Windowing.Core.dll;Microsoft.UI.Xaml.Controls.dll;Microsoft.UI.Xaml.Controls.pri;Microsoft.ui.xaml.dll;Microsoft.UI.Xaml.Internal.dll;Microsoft.UI.Xaml.Phone.dll;Microsoft.ui.xaml.resources.19h1.dll;Microsoft.ui.xaml.resources.common.dll;Microsoft.Web.WebView2.Core.dll;Microsoft.Windows.ApplicationModel.Resources.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.AppNotifications.Projection.dll;Microsoft.Windows.PushNotifications.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WindowsAppRuntime.dll;Microsoft.WindowsAppRuntime.Insights.Resource.dll;Microsoft.WindowsAppRuntime.Release.Net.dll;Microsoft.WinUI.dll;MRM.dll;PowerToys.ManagedCommon.dll;PowerToys.Interop.dll;PowerToys.ManagedTelemetry.dll;PowerToys.MeasureToolCore.dll;PowerToys.MeasureToolUI.deps.json;PowerToys.MeasureToolUI.dll;PowerToys.MeasureToolUI.exe;PowerToys.MeasureToolUI.runtimeconfig.json;PushNotificationsLongRunningTask.ProxyStub.dll;resources.pri;System.CodeDom.dll;System.Management.dll;WindowsAppRuntime.png;WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll;WinRT.Runtime.dll;WinUIEdit.dll;WinUIEx.dll;wuceffectsi.dll?>
<?define PowerRenameMicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?>

View File

@@ -156,7 +156,7 @@ inline void LogStackTrace()
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
return;
}
STACKFRAME64 stack;
memset(&stack, 0, sizeof(STACKFRAME64));
@@ -238,14 +238,14 @@ inline LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
}
/* Handler to trap abort() calls */
inline void AbortHandler(int signal_number)
inline void AbortHandler(int /*signal_number*/)
{
Logger::error("--- ABORT");
try
{
LogStackTrace();
}
catch(...)
catch (...)
{
Logger::error("Failed to log stack trace on abort");
Logger::flush();
@@ -271,9 +271,9 @@ inline void InitUnhandledExceptionHandler(void)
// Global handler for unhandled exceptions
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
// Handler for abort()
signal(SIGABRT, &AbortHandler);
signal(SIGABRT, &AbortHandler);
}
catch(...)
catch (...)
{
Logger::error("Failed to init global unhandled exception handler");
}

View File

@@ -30,7 +30,7 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
auto toolState = GetWindowParam<BoundsToolState*>(window);
if (!toolState)
break;
const POINT cursorPos = convert::FromSystemToRelativeForDirect2D(window, toolState->commonState->cursorPosSystemSpace);
const POINT cursorPos = convert::FromSystemToWindow(window, toolState->commonState->cursorPosSystemSpace);
D2D_POINT_2F newRegionStart = { .x = static_cast<float>(cursorPos.x), .y = static_cast<float>(cursorPos.y) };
toolState->perScreen[window].currentRegionStart = newRegionStart;
@@ -38,6 +38,8 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
}
case WM_CURSOR_LEFT_MONITOR:
{
for (; ShowCursor(true) < 0;)
;
auto toolState = GetWindowParam<BoundsToolState*>(window);
if (!toolState)
break;
@@ -59,12 +61,12 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
if (const bool shiftPress = GetKeyState(VK_SHIFT) & 0x8000; shiftPress)
{
const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, toolState->commonState->cursorPosSystemSpace);
const auto cursorPos = convert::FromSystemToWindow(window, toolState->commonState->cursorPosSystemSpace);
D2D1_RECT_F rect;
std::tie(rect.left, rect.right) = std::minmax(static_cast<float>(cursorPos.x), toolState->perScreen[window].currentRegionStart->x);
std::tie(rect.top, rect.bottom) = std::minmax(static_cast<float>(cursorPos.y), toolState->perScreen[window].currentRegionStart->y);
toolState->perScreen[window].measurements.push_back(rect);
toolState->perScreen[window].measurements.push_back(Measurement{ rect });
}
toolState->perScreen[window].currentRegionStart = std::nullopt;
@@ -97,45 +99,42 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
namespace
{
void DrawMeasurement(const D2D1_RECT_F rect,
void DrawMeasurement(const Measurement& measurement,
const bool alignTextBoxToCenter,
const CommonState& commonState,
HWND window,
const D2DState& d2dState)
const D2DState& d2dState,
float mouseX,
float mouseY)
{
const bool screenQuadrantAware = !alignTextBoxToCenter;
const auto prevMode = d2dState.rt->GetAntialiasMode();
d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
d2dState.rt->DrawRectangle(rect, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->SetAntialiasMode(prevMode);
d2dState.ToggleAliasedLinesMode(true);
d2dState.dxgiWindowState.rt->DrawRectangle(measurement.rect, d2dState.solidBrushes[Brush::line].get());
d2dState.ToggleAliasedLinesMode(false);
OverlayBoxText text;
const auto width = std::abs(rect.right - rect.left + 1);
const auto height = std::abs(rect.top - rect.bottom + 1);
const uint32_t textLen = swprintf_s(text.buffer.data(),
text.buffer.size(),
L"%.0f × %.0f",
width,
height);
std::optional<size_t> crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1;
const auto [crossSymbolPos, measureStringBufLen] =
measurement.Print(text.buffer.data(),
text.buffer.size(),
true,
true,
commonState.units);
commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
v = text;
});
float cornerX = rect.right;
float cornerY = rect.bottom;
if (alignTextBoxToCenter)
{
cornerX = rect.left + width / 2;
cornerY = rect.top + height / 2;
mouseX = measurement.rect.left + measurement.Width(Measurement::Unit::Pixel) / 2;
mouseY = measurement.rect.top + measurement.Height(Measurement::Unit::Pixel) / 2;
}
d2dState.DrawTextBox(text.buffer.data(),
textLen,
measureStringBufLen,
crossSymbolPos,
cornerX,
cornerY,
mouseX,
mouseY,
screenQuadrantAware,
window);
}
@@ -150,20 +149,21 @@ void DrawBoundsToolTick(const CommonState& commonState,
if (it == end(toolState.perScreen))
return;
d2dState.rt->Clear();
d2dState.dxgiWindowState.rt->Clear();
const auto& perScreen = it->second;
for (const auto& measure : perScreen.measurements)
DrawMeasurement(measure, true, commonState, window, d2dState);
DrawMeasurement(measure, true, commonState, window, d2dState, measure.rect.right, measure.rect.bottom);
if (!perScreen.currentRegionStart.has_value())
return;
const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, commonState.cursorPosSystemSpace);
const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
const D2D1_RECT_F rect{ .left = perScreen.currentRegionStart->x,
.top = perScreen.currentRegionStart->y,
.right = static_cast<float>(cursorPos.x),
.bottom = static_cast<float>(cursorPos.y) };
DrawMeasurement(rect, false, commonState, window, d2dState);
D2D1_RECT_F rect;
const float cursorX = static_cast<float>(cursorPos.x);
const float cursorY = static_cast<float>(cursorPos.y);
std::tie(rect.left, rect.right) = std::minmax(cursorX, perScreen.currentRegionStart->x);
std::tie(rect.top, rect.bottom) = std::minmax(cursorY, perScreen.currentRegionStart->y);
DrawMeasurement(Measurement{ rect }, false, commonState, window, d2dState, cursorX, cursorY);
}

View File

@@ -6,21 +6,9 @@
namespace convert
{
// Converts a given point from multi-monitor coordinate system to the one relative to HWND
inline POINT FromSystemToRelative(HWND window, POINT p)
inline POINT FromSystemToWindow(HWND window, POINT p)
{
ScreenToClient(window, &p);
return p;
}
// Converts a given point from multi-monitor coordinate system to the one relative to HWND and also ready
// to be used in Direct2D calls with AA mode set to aliased
inline POINT FromSystemToRelativeForDirect2D(HWND window, POINT p)
{
ScreenToClient(window, &p);
// Submitting DrawLine calls to Direct2D with thickness == 1.f and AA mode set to aliased causes
// them to be drawn offset by [1,1] toward upper-left corner, so we must to compensate for that.
++p.x;
++p.y;
return p;
}
}

View File

@@ -2,6 +2,7 @@
#include "constants.h"
#include "D2DState.h"
#include "DxgiAPI.h"
#include <common/Display/dpi_aware.h>
#include <ToolState.h>
@@ -19,43 +20,28 @@ namespace
}
}
D2DState::D2DState(const HWND overlayWindow, std::vector<D2D1::ColorF> solidBrushesColors)
D2DState::D2DState(const DxgiAPI* dxgi,
HWND window,
std::vector<D2D1::ColorF> solidBrushesColors)
{
std::lock_guard guard{ gpuAccessLock };
RECT clientRect = {};
winrt::check_bool(GetClientRect(overlayWindow, &clientRect));
winrt::check_hresult(D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, &d2dFactory));
// We should always use DPIAware::DEFAULT_DPI, since it's the correct thing to do in DPI-Aware mode
auto renderTargetProperties = D2D1::RenderTargetProperties(
D2D1_RENDER_TARGET_TYPE_DEFAULT,
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
DPIAware::DEFAULT_DPI,
DPIAware::DEFAULT_DPI,
D2D1_RENDER_TARGET_USAGE_NONE,
D2D1_FEATURE_LEVEL_DEFAULT);
auto renderTargetSize = D2D1::SizeU(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
auto hwndRenderTargetProperties = D2D1::HwndRenderTargetProperties(overlayWindow, renderTargetSize);
winrt::check_hresult(d2dFactory->CreateHwndRenderTarget(renderTargetProperties, hwndRenderTargetProperties, &rt));
winrt::check_hresult(rt->CreateCompatibleRenderTarget(&bitmapRt));
dxgiAPI = dxgi;
unsigned dpi = DPIAware::DEFAULT_DPI;
DPIAware::GetScreenDPIForWindow(overlayWindow, dpi);
DPIAware::GetScreenDPIForWindow(window, dpi);
dpiScale = dpi / static_cast<float>(DPIAware::DEFAULT_DPI);
winrt::check_hresult(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), writeFactory.put_unknown()));
winrt::check_hresult(writeFactory->CreateTextFormat(L"Segoe UI Variable Text",
nullptr,
DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
consts::FONT_SIZE * dpiScale,
L"en-US",
&textFormat));
dxgiWindowState = dxgiAPI->CreateD2D1RenderTarget(window);
winrt::check_hresult(dxgiWindowState.rt->CreateCompatibleRenderTarget(bitmapRt.put()));
winrt::check_hresult(dxgiAPI->writeFactory->CreateTextFormat(L"Segoe UI Variable Text",
nullptr,
DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
consts::FONT_SIZE * dpiScale,
L"en-US",
textFormat.put()));
winrt::check_hresult(textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER));
winrt::check_hresult(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER));
winrt::check_hresult(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));
@@ -63,22 +49,22 @@ D2DState::D2DState(const HWND overlayWindow, std::vector<D2D1::ColorF> solidBrus
solidBrushes.resize(solidBrushesColors.size());
for (size_t i = 0; i < solidBrushes.size(); ++i)
{
winrt::check_hresult(rt->CreateSolidColorBrush(solidBrushesColors[i], &solidBrushes[i]));
winrt::check_hresult(dxgiWindowState.rt->CreateSolidColorBrush(solidBrushesColors[i], solidBrushes[i].put()));
}
const auto deviceContext = rt.query<ID2D1DeviceContext>();
winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D1Shadow, &shadowEffect));
const auto deviceContext = dxgiWindowState.rt.as<ID2D1DeviceContext>();
winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D1Shadow, shadowEffect.put()));
winrt::check_hresult(shadowEffect->SetValue(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, consts::SHADOW_RADIUS));
winrt::check_hresult(shadowEffect->SetValue(D2D1_SHADOW_PROP_COLOR, D2D1::ColorF(0.f, 0.f, 0.f, consts::SHADOW_OPACITY)));
winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D12DAffineTransform, &affineTransformEffect));
winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D12DAffineTransform, affineTransformEffect.put()));
affineTransformEffect->SetInputEffect(0, shadowEffect.get());
textRenderer = winrt::make_self<PerGlyphOpacityTextRender>(d2dFactory, rt, solidBrushes[Brush::foreground]);
textRenderer = winrt::make_self<PerGlyphOpacityTextRender>(dxgi->d2dFactory2, dxgiWindowState.rt, solidBrushes[Brush::foreground]);
}
void D2DState::DrawTextBox(const wchar_t* text,
const uint32_t textLen,
const size_t textLen,
const std::optional<size_t> halfOpaqueSymbolPos,
const float centerX,
const float centerY,
@@ -86,16 +72,19 @@ void D2DState::DrawTextBox(const wchar_t* text,
const HWND window) const
{
wil::com_ptr<IDWriteTextLayout> textLayout;
winrt::check_hresult(writeFactory->CreateTextLayout(text,
textLen,
textFormat.get(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(),
&textLayout));
winrt::check_hresult(
dxgiAPI->writeFactory->CreateTextLayout(text,
static_cast<uint32_t>(textLen),
textFormat.get(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(),
&textLayout));
DWRITE_TEXT_METRICS textMetrics = {};
winrt::check_hresult(textLayout->GetMetrics(&textMetrics));
textMetrics.width *= consts::TEXT_BOX_MARGIN_COEFF;
textMetrics.height *= consts::TEXT_BOX_MARGIN_COEFF;
// Assumes text doesn't contain new lines
const float lineHeight = textMetrics.height;
textMetrics.width += lineHeight;
textMetrics.height += lineHeight * .5f;
winrt::check_hresult(textLayout->SetMaxWidth(textMetrics.width));
winrt::check_hresult(textLayout->SetMaxHeight(textMetrics.height));
@@ -103,6 +92,8 @@ void D2DState::DrawTextBox(const wchar_t* text,
.top = centerY - textMetrics.height / 2.f,
.right = centerX + textMetrics.width / 2.f,
.bottom = centerY + textMetrics.height / 2.f };
const float SHADOW_OFFSET = consts::SHADOW_OFFSET * dpiScale;
if (screenQuadrantAware)
{
bool cursorInLeftScreenHalf = false;
@@ -112,8 +103,8 @@ void D2DState::DrawTextBox(const wchar_t* text,
static_cast<long>(centerY),
cursorInLeftScreenHalf,
cursorInTopScreenHalf);
float textQuadrantOffsetX = textMetrics.width * dpiScale;
float textQuadrantOffsetY = textMetrics.height * dpiScale;
float textQuadrantOffsetX = textMetrics.width / 2.f + SHADOW_OFFSET;
float textQuadrantOffsetY = textMetrics.height / 2.f + SHADOW_OFFSET;
if (!cursorInLeftScreenHalf)
textQuadrantOffsetX *= -1.f;
if (!cursorInTopScreenHalf)
@@ -140,15 +131,14 @@ void D2DState::DrawTextBox(const wchar_t* text,
bitmapRt->GetBitmap(&rtBitmap);
shadowEffect->SetInput(0, rtBitmap.get());
const auto shadowMatrix = D2D1::Matrix3x2F::Translation(consts::SHADOW_OFFSET * dpiScale,
consts::SHADOW_OFFSET * dpiScale);
const auto shadowMatrix = D2D1::Matrix3x2F::Translation(SHADOW_OFFSET, SHADOW_OFFSET);
winrt::check_hresult(affineTransformEffect->SetValue(D2D1_2DAFFINETRANSFORM_PROP_TRANSFORM_MATRIX,
shadowMatrix));
auto deviceContext = rt.query<ID2D1DeviceContext>();
auto deviceContext = dxgiWindowState.rt.as<ID2D1DeviceContext>();
deviceContext->DrawImage(affineTransformEffect.get(), D2D1_INTERPOLATION_MODE_LINEAR);
// Draw text box border rectangle
rt->DrawRoundedRectangle(textBoxRect, solidBrushes[Brush::border].get());
dxgiWindowState.rt->DrawRoundedRectangle(textBoxRect, solidBrushes[Brush::border].get());
const float TEXT_BOX_PADDING = 1.f * dpiScale;
textBoxRect.rect.bottom -= TEXT_BOX_PADDING;
textBoxRect.rect.top += TEXT_BOX_PADDING;
@@ -156,7 +146,7 @@ void D2DState::DrawTextBox(const wchar_t* text,
textBoxRect.rect.right -= TEXT_BOX_PADDING;
// Draw text & its box
rt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::background].get());
dxgiWindowState.rt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::background].get());
if (halfOpaqueSymbolPos.has_value())
{
@@ -167,3 +157,19 @@ void D2DState::DrawTextBox(const wchar_t* text,
}
winrt::check_hresult(textLayout->Draw(nullptr, textRenderer.get(), textRect.left, textRect.top));
}
void D2DState::ToggleAliasedLinesMode(const bool enabled) const
{
if (enabled)
{
// Draw lines in the middle of a pixel to avoid bleeding, since [0,0] pixel is
// a rectangle filled from (0,0) to (1,1) and the lines use thickness = 1.
dxgiWindowState.rt->SetTransform(D2D1::Matrix3x2F::Translation(.5f, .5f));
dxgiWindowState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
}
else
{
dxgiWindowState.rt->SetTransform(D2D1::Matrix3x2F::Identity());
dxgiWindowState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
}
}

View File

@@ -3,10 +3,9 @@
#include <optional>
#include <vector>
#include <d2d1_3.h>
#include <wil/com.h>
#include <windef.h>
#include "DxgiAPI.h"
#include "PerGlyphOpacityTextRender.h"
enum Brush : size_t
@@ -19,24 +18,27 @@ enum Brush : size_t
struct D2DState
{
wil::com_ptr<ID2D1Factory> d2dFactory;
wil::com_ptr<IDWriteFactory> writeFactory;
wil::com_ptr<ID2D1HwndRenderTarget> rt;
wil::com_ptr<ID2D1BitmapRenderTarget> bitmapRt;
wil::com_ptr<IDWriteTextFormat> textFormat;
winrt::com_ptr<PerGlyphOpacityTextRender> textRenderer;
std::vector<wil::com_ptr<ID2D1SolidColorBrush>> solidBrushes;
wil::com_ptr<ID2D1Effect> shadowEffect;
wil::com_ptr<ID2D1Effect> affineTransformEffect;
const DxgiAPI* dxgiAPI = nullptr;
DxgiWindowState dxgiWindowState;
winrt::com_ptr<ID2D1BitmapRenderTarget> bitmapRt;
winrt::com_ptr<IDWriteTextFormat> textFormat;
winrt::com_ptr<PerGlyphOpacityTextRender> textRenderer;
std::vector<winrt::com_ptr<ID2D1SolidColorBrush>> solidBrushes;
winrt::com_ptr<ID2D1Effect> shadowEffect;
winrt::com_ptr<ID2D1Effect> affineTransformEffect;
float dpiScale = 1.f;
D2DState(const HWND window, std::vector<D2D1::ColorF> solidBrushesColors);
D2DState(const DxgiAPI*,
HWND window,
std::vector<D2D1::ColorF> solidBrushesColors);
void DrawTextBox(const wchar_t* text,
const uint32_t textLen,
const size_t textLen,
const std::optional<size_t> halfOpaqueSymbolPos,
const float centerX,
const float centerY,
const bool screenQuadrantAware,
const HWND window) const;
void ToggleAliasedLinesMode(const bool enabled) const;
};

View File

@@ -0,0 +1,145 @@
#include "pch.h"
#include "DxgiAPI.h"
#include <common/Display/dpi_aware.h>
//#define DEBUG_DEVICES
#define SEPARATE_D3D_FOR_CAPTURE
namespace
{
DxgiAPI::D3D CreateD3D()
{
DxgiAPI::D3D d3d;
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if defined(DEBUG_DEVICES)
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
HRESULT hr =
D3D11CreateDevice(nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3d.d3dDevice.put(),
nullptr,
nullptr);
if (hr == DXGI_ERROR_UNSUPPORTED)
{
hr = D3D11CreateDevice(nullptr,
D3D_DRIVER_TYPE_WARP,
nullptr,
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3d.d3dDevice.put(),
nullptr,
nullptr);
}
winrt::check_hresult(hr);
d3d.dxgiDevice = d3d.d3dDevice.as<IDXGIDevice>();
winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(d3d.dxgiDevice.get(), d3d.d3dDeviceInspectable.put()));
winrt::com_ptr<IDXGIAdapter> adapter;
winrt::check_hresult(d3d.dxgiDevice->GetParent(winrt::guid_of<IDXGIAdapter>(), adapter.put_void()));
winrt::check_hresult(adapter->GetParent(winrt::guid_of<IDXGIFactory2>(), d3d.dxgiFactory2.put_void()));
d3d.d3dDevice->GetImmediateContext(d3d.d3dContext.put());
winrt::check_bool(d3d.d3dContext);
auto contextMultithread = d3d.d3dContext.as<ID3D11Multithread>();
contextMultithread->SetMultithreadProtected(true);
return d3d;
}
}
DxgiAPI::DxgiAPI()
{
const D2D1_FACTORY_OPTIONS d2dFactoryOptions = {
#if defined(DEBUG_DEVICES)
D2D1_DEBUG_LEVEL_INFORMATION
#else
D2D1_DEBUG_LEVEL_NONE
#endif
};
winrt::check_hresult(D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, d2dFactoryOptions, d2dFactory2.put()));
winrt::check_hresult(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
winrt::guid_of<IDWriteFactory>(),
reinterpret_cast<IUnknown**>(writeFactory.put())));
auto d3d = CreateD3D();
d3dDevice = d3d.d3dDevice;
dxgiDevice = d3d.dxgiDevice;
d3dDeviceInspectable = d3d.d3dDeviceInspectable;
dxgiFactory2 = d3d.dxgiFactory2;
d3dContext = d3d.d3dContext;
#if defined(SEPARATE_D3D_FOR_CAPTURE)
auto d3dFC = CreateD3D();
d3dForCapture = d3dFC;
#else
d3dForCapture = d3d;
#endif
winrt::check_hresult(d2dFactory2->CreateDevice(dxgiDevice.get(), d2dDevice1.put()));
winrt::check_hresult(DCompositionCreateDevice(
dxgiDevice.get(),
winrt::guid_of<IDCompositionDevice>(),
compositionDevice.put_void()));
}
DxgiWindowState DxgiAPI::CreateD2D1RenderTarget(HWND window) const
{
RECT rect = {};
winrt::check_bool(GetClientRect(window, &rect));
const DXGI_SWAP_CHAIN_DESC1 desc = {
.Width = static_cast<UINT>(rect.right - rect.left),
.Height = static_cast<UINT>(rect.bottom - rect.top),
.Format = static_cast<DXGI_FORMAT>(winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized),
.SampleDesc = { .Count = 1, .Quality = 0 },
.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
.BufferCount = 2,
.Scaling = DXGI_SCALING_STRETCH,
.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD,
.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED,
};
DxgiWindowState state;
winrt::com_ptr<ID2D1DeviceContext> rt;
d2dDevice1->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, rt.put());
state.rt = rt;
winrt::check_hresult(dxgiFactory2->CreateSwapChainForComposition(d3dDevice.get(),
&desc,
nullptr,
state.swapChain.put()));
winrt::com_ptr<IDXGISurface> surface;
winrt::check_hresult(state.swapChain->GetBuffer(0, winrt::guid_of<IDXGISurface>(), surface.put_void()));
const D2D1_BITMAP_PROPERTIES1 properties = {
.pixelFormat = { .format = DXGI_FORMAT_B8G8R8A8_UNORM, .alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED },
.bitmapOptions = D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW
};
winrt::com_ptr<ID2D1Bitmap1> bitmap;
winrt::check_hresult(rt->CreateBitmapFromDxgiSurface(surface.get(),
properties,
bitmap.put()));
rt->SetTarget(bitmap.get());
winrt::check_hresult(compositionDevice->CreateTargetForHwnd(window,
true,
state.compositionTarget.put()));
winrt::com_ptr<IDCompositionVisual> visual;
winrt::check_hresult(compositionDevice->CreateVisual(visual.put()));
winrt::check_hresult(visual->SetContent(state.swapChain.get()));
winrt::check_hresult(state.compositionTarget->SetRoot(visual.get()));
winrt::check_hresult(compositionDevice->Commit());
return state;
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <d2d1_3.h>
#include <d3d11_4.h>
#include <dcomp.h>
#include <dxgi1_3.h>
#include <inspectable.h>
#include <winrt/base.h>
struct DxgiWindowState
{
winrt::com_ptr<ID2D1RenderTarget> rt;
winrt::com_ptr<IDXGISwapChain1> swapChain;
winrt::com_ptr<IDCompositionTarget> compositionTarget;
};
struct DxgiAPI final
{
struct D3D
{
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::com_ptr<IDXGIDevice> dxgiDevice;
winrt::com_ptr<IInspectable> d3dDeviceInspectable;
winrt::com_ptr<IDXGIFactory2> dxgiFactory2;
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
};
winrt::com_ptr<ID2D1Factory2> d2dFactory2;
winrt::com_ptr<IDWriteFactory> writeFactory;
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::com_ptr<IDXGIDevice> dxgiDevice;
winrt::com_ptr<IInspectable> d3dDeviceInspectable;
winrt::com_ptr<IDXGIFactory2> dxgiFactory2;
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
D3D d3dForCapture;
winrt::com_ptr<ID2D1Device1> d2dDevice1;
winrt::com_ptr<IDCompositionDevice> compositionDevice;
DxgiAPI();
enum class Uninitialized
{
};
explicit inline DxgiAPI(Uninitialized) {}
DxgiWindowState CreateD2D1RenderTarget(HWND window) const;
};

View File

@@ -2,8 +2,8 @@
#include "constants.h"
#include "EdgeDetection.h"
template<bool PerChannel,
bool ContinuousCapture,
bool IsX,
bool Increment>
inline long FindEdge(const BGRATextureView& texture, const POINT centerPoint, const uint8_t tolerance)
@@ -57,25 +57,21 @@ inline long FindEdge(const BGRATextureView& texture, const POINT centerPoint, co
return Increment ? static_cast<long>(IsX ? texture.width : texture.height) - 1 : 0;
}
template<bool PerChannel, bool ContinuousCapture>
template<bool PerChannel>
inline RECT DetectEdgesInternal(const BGRATextureView& texture,
const POINT centerPoint,
const uint8_t tolerance)
{
return RECT{ .left = FindEdge<PerChannel,
ContinuousCapture,
true,
false>(texture, centerPoint, tolerance),
.top = FindEdge<PerChannel,
ContinuousCapture,
false,
false>(texture, centerPoint, tolerance),
.right = FindEdge<PerChannel,
ContinuousCapture,
true,
true>(texture, centerPoint, tolerance),
.bottom = FindEdge<PerChannel,
ContinuousCapture,
false,
true>(texture, centerPoint, tolerance) };
}
@@ -83,12 +79,9 @@ inline RECT DetectEdgesInternal(const BGRATextureView& texture,
RECT DetectEdges(const BGRATextureView& texture,
const POINT centerPoint,
const bool perChannel,
const uint8_t tolerance,
const bool continuousCapture)
const uint8_t tolerance)
{
auto function = perChannel ? &DetectEdgesInternal<true, false> : DetectEdgesInternal<false, false>;
if (continuousCapture)
function = perChannel ? &DetectEdgesInternal<true, true> : &DetectEdgesInternal<false, true>;
auto function = perChannel ? &DetectEdgesInternal<true> : DetectEdgesInternal<false>;
return function(texture, centerPoint, tolerance);
}

View File

@@ -5,5 +5,4 @@
RECT DetectEdges(const BGRATextureView& texture,
const POINT centerPoint,
const bool perChannel,
const uint8_t tolerance,
const bool continuousCapture);
const uint8_t tolerance);

View File

@@ -16,30 +16,22 @@ namespace
// Computing in this way to achieve pixel-perfect axial symmetry of aliased D2D lines
if (horizontal)
{
start.x -= consts::FEET_HALF_LENGTH + 1.f;
end.x += consts::FEET_HALF_LENGTH;
start.y += 1.f;
end.y += 1.f;
start.x -= consts::FEET_HALF_LENGTH;
end.x += consts::FEET_HALF_LENGTH + 1.f;
}
else
{
start.y -= consts::FEET_HALF_LENGTH + 1.f;
end.y += consts::FEET_HALF_LENGTH;
start.x += 1.f;
end.x += 1.f;
start.y -= consts::FEET_HALF_LENGTH;
end.y += consts::FEET_HALF_LENGTH + 1.f;
}
return { start, end };
}
}
winrt::com_ptr<ID2D1Bitmap> ConvertID3D11Texture2DToD2D1Bitmap(wil::com_ptr<ID2D1HwndRenderTarget> rt,
winrt::com_ptr<ID2D1Bitmap> ConvertID3D11Texture2DToD2D1Bitmap(winrt::com_ptr<ID2D1RenderTarget> rt,
const MappedTextureView* capturedScreenTexture)
{
std::lock_guard guard{ gpuAccessLock };
capturedScreenTexture->view.pixels;
D2D1_BITMAP_PROPERTIES props = { .pixelFormat = rt->GetPixelFormat() };
@@ -62,6 +54,7 @@ LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LP
{
switch (message)
{
case WM_MOUSELEAVE:
case WM_CURSOR_LEFT_MONITOR:
{
if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
@@ -130,20 +123,27 @@ void DrawMeasureToolTick(const CommonState& commonState,
bool drawFeetOnCross = {};
bool drawHorizontalCrossLine = true;
bool drawVerticalCrossLine = true;
RECT measuredEdges{};
Measurement measuredEdges{};
MeasureToolState::Mode mode = {};
winrt::com_ptr<ID2D1Bitmap> backgroundBitmap;
const MappedTextureView* backgroundTextureToConvert = nullptr;
bool gotMeasurement = false;
toolState.Read([&](const MeasureToolState& state) {
continuousCapture = state.global.continuousCapture;
drawFeetOnCross = state.global.drawFeetOnCross;
mode = state.global.mode;
if (auto it = state.perScreen.find(window); it != end(state.perScreen))
{
const auto& perScreen = it->second;
measuredEdges = perScreen.measuredEdges;
if (!perScreen.measuredEdges)
{
return;
}
gotMeasurement = true;
measuredEdges = *perScreen.measuredEdges;
if (continuousCapture)
return;
@@ -158,6 +158,10 @@ void DrawMeasureToolTick(const CommonState& commonState,
}
}
});
if (!gotMeasurement)
return;
switch (mode)
{
case MeasureToolState::Mode::Cross:
@@ -176,7 +180,7 @@ void DrawMeasureToolTick(const CommonState& commonState,
if (!continuousCapture && !backgroundBitmap && backgroundTextureToConvert)
{
backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.rt, backgroundTextureToConvert);
backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.dxgiWindowState.rt, backgroundTextureToConvert);
if (backgroundBitmap)
{
toolState.Access([&](MeasureToolState& state) {
@@ -187,28 +191,24 @@ void DrawMeasureToolTick(const CommonState& commonState,
}
if (continuousCapture || !backgroundBitmap)
d2dState.rt->Clear();
d2dState.dxgiWindowState.rt->Clear();
// Add 1px to each dim, since the range we obtain from measuredEdges is inclusive.
const float hMeasure = static_cast<float>(measuredEdges.right - measuredEdges.left + 1);
const float vMeasure = static_cast<float>(measuredEdges.bottom - measuredEdges.top + 1);
const float hMeasure = measuredEdges.Width(Measurement::Unit::Pixel);
const float vMeasure = measuredEdges.Height(Measurement::Unit::Pixel);
if (!continuousCapture && backgroundBitmap)
{
d2dState.rt->DrawBitmap(backgroundBitmap.get());
d2dState.dxgiWindowState.rt->DrawBitmap(backgroundBitmap.get());
}
const auto previousAliasingMode = d2dState.rt->GetAntialiasMode();
// Anti-aliasing is creating artifacts. Aliasing is for drawing straight lines.
d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, commonState.cursorPosSystemSpace);
const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
d2dState.ToggleAliasedLinesMode(true);
if (drawHorizontalCrossLine)
{
const D2D_POINT_2F hLineStart{ .x = static_cast<float>(measuredEdges.left), .y = static_cast<float>(cursorPos.y) };
const D2D_POINT_2F hLineStart{ .x = measuredEdges.rect.left, .y = static_cast<float>(cursorPos.y) };
D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y };
d2dState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get());
d2dState.dxgiWindowState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get());
if (drawFeetOnCross)
{
@@ -218,57 +218,37 @@ void DrawMeasureToolTick(const CommonState& commonState,
hLineEnd.x -= 1.f;
auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false);
auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false);
d2dState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get());
d2dState.dxgiWindowState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get());
d2dState.dxgiWindowState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get());
}
}
if (drawVerticalCrossLine)
{
const D2D_POINT_2F vLineStart{ .x = static_cast<float>(cursorPos.x), .y = static_cast<float>(measuredEdges.top) };
const D2D_POINT_2F vLineStart{ .x = static_cast<float>(cursorPos.x), .y = measuredEdges.rect.top };
D2D_POINT_2F vLineEnd{ .x = vLineStart.x, .y = vLineStart.y + vMeasure };
d2dState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get());
d2dState.dxgiWindowState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get());
if (drawFeetOnCross)
{
vLineEnd.y -= 1.f;
auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true);
auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true);
d2dState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get());
d2dState.dxgiWindowState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get());
d2dState.dxgiWindowState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get());
}
}
// After drawing the lines, restore anti aliasing to draw the measurement tooltip.
d2dState.rt->SetAntialiasMode(previousAliasingMode);
uint32_t measureStringBufLen = 0;
d2dState.ToggleAliasedLinesMode(false);
OverlayBoxText text;
std::optional<size_t> crossSymbolPos;
switch (mode)
{
case MeasureToolState::Mode::Cross:
measureStringBufLen = swprintf_s(text.buffer.data(),
text.buffer.size(),
L"%.0f × %.0f",
hMeasure,
vMeasure);
crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1;
break;
case MeasureToolState::Mode::Vertical:
measureStringBufLen = swprintf_s(text.buffer.data(),
text.buffer.size(),
L"%.0f",
vMeasure);
break;
case MeasureToolState::Mode::Horizontal:
measureStringBufLen = swprintf_s(text.buffer.data(),
text.buffer.size(),
L"%.0f",
hMeasure);
break;
}
const auto [crossSymbolPos, measureStringBufLen] =
measuredEdges.Print(text.buffer.data(),
text.buffer.size(),
drawHorizontalCrossLine,
drawVerticalCrossLine,
commonState.units);
commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
v = text;

View File

@@ -0,0 +1,91 @@
#include "pch.h"
#include "Measurement.h"
Measurement::Measurement(RECT winRect)
{
rect.left = static_cast<float>(winRect.left);
rect.right = static_cast<float>(winRect.right);
rect.top = static_cast<float>(winRect.top);
rect.bottom = static_cast<float>(winRect.bottom);
}
Measurement::Measurement(D2D1_RECT_F d2dRect) :
rect{ d2dRect }
{
}
namespace
{
inline float Convert(const float pixels, const Measurement::Unit units)
{
switch (units)
{
case Measurement::Unit::Pixel:
return pixels;
case Measurement::Unit::Inch:
return pixels / 96.f;
case Measurement::Unit::Centimetre:
return pixels / 96.f * 2.54f;
default:
return pixels;
}
}
}
inline float Measurement::Width(const Unit units) const
{
return Convert(rect.right - rect.left + 1.f, units);
}
inline float Measurement::Height(const Unit units) const
{
return Convert(rect.bottom - rect.top + 1.f, units);
}
Measurement::PrintResult Measurement::Print(wchar_t* buf,
const size_t bufSize,
const bool printWidth,
const bool printHeight,
const Unit units) const
{
PrintResult result;
if (printWidth)
{
result.strLen += swprintf_s(buf,
bufSize,
L"%g",
Width(units));
if (printHeight)
{
result.crossSymbolPos = result.strLen + 1;
result.strLen += swprintf_s(buf + result.strLen,
bufSize - result.strLen,
L" \x00D7 ");
}
}
if (printHeight)
{
result.strLen += swprintf_s(buf + result.strLen,
bufSize - result.strLen,
L"%g",
Height(units));
}
switch (units)
{
case Measurement::Unit::Inch:
result.strLen += swprintf_s(buf + result.strLen,
bufSize - result.strLen,
L" in");
break;
case Measurement::Unit::Centimetre:
result.strLen += swprintf_s(buf + result.strLen,
bufSize - result.strLen,
L" cm");
break;
}
return result;
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <dcommon.h>
#include <windef.h>
struct Measurement
{
enum Unit
{
Pixel,
Inch,
Centimetre
};
D2D1_RECT_F rect = {}; // corners are inclusive
Measurement() = default;
Measurement(const Measurement&) = default;
Measurement& operator=(const Measurement&) = default;
explicit Measurement(D2D1_RECT_F d2dRect);
explicit Measurement(RECT winRect);
float Width(const Unit units) const;
float Height(const Unit units) const;
struct PrintResult
{
std::optional<size_t> crossSymbolPos;
size_t strLen = {};
};
PrintResult Print(wchar_t* buf,
const size_t bufSize,
const bool printWidth,
const bool printHeight,
const Unit units) const;
};

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "BoundsToolOverlayUI.h"
#include "constants.h"
#include "MeasureToolOverlayUI.h"
#include "OverlayUI.h"
@@ -22,16 +23,17 @@ void CreateOverlayWindowClasses()
wcex.lpfnWndProc = MeasureToolWndProc;
wcex.lpszClassName = NonLocalizable::MeasureToolOverlayWindowName;
wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS);
RegisterClassExW(&wcex);
wcex.lpfnWndProc = BoundsToolWndProc;
wcex.lpszClassName = NonLocalizable::BoundsToolOverlayWindowName;
wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS);
RegisterClassExW(&wcex);
}
HWND CreateOverlayUIWindow(const CommonState& commonState,
const MonitorInfo& monitor,
const bool excludeFromCapture,
const wchar_t* windowClass,
void* extraParam)
{
@@ -39,7 +41,7 @@ HWND CreateOverlayUIWindow(const CommonState& commonState,
std::call_once(windowClassesCreatedFlag, CreateOverlayWindowClasses);
const auto screenArea = monitor.GetScreenSize(true);
DWORD windowStyle = WS_EX_TOOLWINDOW;
DWORD windowStyle = WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW;
#if !defined(DEBUG_OVERLAY)
windowStyle |= WS_EX_TOPMOST;
#endif
@@ -47,7 +49,7 @@ HWND CreateOverlayUIWindow(const CommonState& commonState,
CreateWindowExW(windowStyle,
windowClass,
L"PowerToys.MeasureToolOverlay",
WS_POPUP,
WS_POPUP | CS_HREDRAW | CS_VREDRAW,
screenArea.left(),
screenArea.top(),
screenArea.width(),
@@ -59,7 +61,11 @@ HWND CreateOverlayUIWindow(const CommonState& commonState,
};
winrt::check_bool(window);
ShowWindow(window, SW_SHOWNORMAL);
SetWindowDisplayAffinity(window, WDA_EXCLUDEFROMCAPTURE);
UpdateWindow(window);
if (excludeFromCapture)
{
SetWindowDisplayAffinity(window, WDA_EXCLUDEFROMCAPTURE);
}
#if !defined(DEBUG_OVERLAY)
SetWindowPos(window, HWND_TOPMOST, {}, {}, {}, {}, SWP_NOMOVE | SWP_NOSIZE);
#else
@@ -109,51 +115,60 @@ std::vector<D2D1::ColorF> AppendCommonOverlayUIColors(const D2D1::ColorF& lineCo
void OverlayUIState::RunUILoop()
{
bool cursorOnScreen = true;
while (IsWindow(_window) && !_commonState.closeOnOtherMonitors)
{
const auto now = std::chrono::high_resolution_clock::now();
const auto cursor = _commonState.cursorPosSystemSpace;
const bool cursorOnScreen = _monitorArea.inside(cursor);
const bool cursorOverToolbar = _commonState.toolbarBoundingBox.inside(cursor);
if (cursorOnScreen != _cursorOnScreen)
auto& dxgi = _d2dState.dxgiWindowState;
if (_monitorArea.inside(cursor) != cursorOnScreen)
{
_cursorOnScreen = cursorOnScreen;
cursorOnScreen = !cursorOnScreen;
if (!cursorOnScreen)
{
if (_clearOnCursorLeavingScreen)
{
_d2dState.rt->BeginDraw();
_d2dState.rt->Clear();
_d2dState.rt->EndDraw();
}
PostMessageW(_window, WM_CURSOR_LEFT_MONITOR, {}, {});
}
}
run_message_loop(true, 1);
dxgi.rt->BeginDraw();
dxgi.rt->Clear();
if (!cursorOverToolbar)
_tickFunc();
dxgi.rt->EndDraw();
dxgi.swapChain->Present(0, 0);
if (cursorOnScreen)
{
_d2dState.rt->BeginDraw();
if (!cursorOverToolbar)
_tickFunc();
else
_d2dState.rt->Clear();
_d2dState.rt->EndDraw();
const auto frameTime = std::chrono::high_resolution_clock::now() - now;
if (frameTime < consts::TARGET_FRAME_DURATION)
{
std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION - frameTime);
}
}
else
{
// Don't consume resources while nothing could be updated
std::this_thread::sleep_for(std::chrono::milliseconds{ 200 });
}
run_message_loop(true, 1);
}
DestroyWindow(_window);
}
template<typename StateT, typename TickFuncT>
OverlayUIState::OverlayUIState(StateT& toolState,
OverlayUIState::OverlayUIState(const DxgiAPI* dxgiAPI,
StateT& toolState,
TickFuncT tickFunc,
const CommonState& commonState,
HWND window) :
_window{ window },
_commonState{ commonState },
_d2dState{ window, AppendCommonOverlayUIColors(commonState.lineColor) },
_d2dState{ dxgiAPI, window, AppendCommonOverlayUIColors(commonState.lineColor) },
_tickFunc{ [this, tickFunc, &toolState] {
tickFunc(_commonState, toolState, _window, _d2dState);
} }
@@ -175,25 +190,30 @@ OverlayUIState::~OverlayUIState()
// Returning unique_ptr, since we need to pin ui state in memory
template<typename ToolT, typename TickFuncT>
inline std::unique_ptr<OverlayUIState> OverlayUIState::CreateInternal(ToolT& toolState,
inline std::unique_ptr<OverlayUIState> OverlayUIState::CreateInternal(const DxgiAPI* dxgi,
ToolT& toolState,
TickFuncT tickFunc,
CommonState& commonState,
const wchar_t* toolWindowClassName,
void* windowParam,
const MonitorInfo& monitor,
const bool clearOnCursorLeavingScreen)
const bool excludeFromCapture)
{
wil::shared_event uiCreatedEvent(wil::EventOptions::ManualReset);
std::unique_ptr<OverlayUIState> uiState;
auto threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] {
const HWND window = CreateOverlayUIWindow(commonState, monitor, toolWindowClassName, windowParam);
uiState = std::unique_ptr<OverlayUIState>{ new OverlayUIState{ toolState, tickFunc, commonState, window } };
uiState->_monitorArea = monitor.GetScreenSize(true);
uiState->_clearOnCursorLeavingScreen = clearOnCursorLeavingScreen;
// we must create window + d2d state in the same thread, then store thread handle in uiState, thus
// lifetime is ok here, since we join the thread in destructor
auto* state = uiState.get();
uiCreatedEvent.SetEvent();
std::thread threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] {
OverlayUIState* state = nullptr;
{
auto sinalUICreatedEvent = wil::scope_exit([&] { uiCreatedEvent.SetEvent(); });
const HWND window = CreateOverlayUIWindow(commonState, monitor, excludeFromCapture, toolWindowClassName, windowParam);
uiState = std::unique_ptr<OverlayUIState>{ new OverlayUIState{ dxgi, toolState, tickFunc, commonState, window } };
uiState->_monitorArea = monitor.GetScreenSize(true);
// we must create window + d2d state in the same thread, then store thread handle in uiState, thus
// lifetime is ok here, since we join the thread in destructor
state = uiState.get();
}
state->RunUILoop();
@@ -202,28 +222,40 @@ inline std::unique_ptr<OverlayUIState> OverlayUIState::CreateInternal(ToolT& too
});
uiCreatedEvent.wait();
uiState->_uiThread = std::move(threadHandle);
if (uiState)
uiState->_uiThread = std::move(threadHandle);
else if (threadHandle.joinable())
threadHandle.join();
return uiState;
}
std::unique_ptr<OverlayUIState> OverlayUIState::Create(Serialized<MeasureToolState>& toolState,
std::unique_ptr<OverlayUIState> OverlayUIState::Create(const DxgiAPI* dxgi,
Serialized<MeasureToolState>& toolState,
CommonState& commonState,
const MonitorInfo& monitor)
{
return OverlayUIState::CreateInternal(toolState,
bool excludeFromCapture = false;
toolState.Read([&](const MeasureToolState& s) {
excludeFromCapture = s.global.continuousCapture;
});
return OverlayUIState::CreateInternal(dxgi,
toolState,
DrawMeasureToolTick,
commonState,
NonLocalizable::MeasureToolOverlayWindowName,
&toolState,
monitor,
true);
excludeFromCapture);
}
std::unique_ptr<OverlayUIState> OverlayUIState::Create(BoundsToolState& toolState,
std::unique_ptr<OverlayUIState> OverlayUIState::Create(const DxgiAPI* dxgi,
BoundsToolState& toolState,
CommonState& commonState,
const MonitorInfo& monitor)
{
return OverlayUIState::CreateInternal(toolState,
return OverlayUIState::CreateInternal(dxgi,
toolState,
DrawBoundsToolTick,
commonState,
NonLocalizable::BoundsToolOverlayWindowName,

View File

@@ -1,6 +1,8 @@
#pragma once
#include "DxgiAPI.h"
#include "D2DState.h"
#include "ToolState.h"
#include <common/display/monitors.h>
@@ -9,7 +11,8 @@
class OverlayUIState final
{
template<typename StateT, typename TickFuncT>
OverlayUIState(StateT& toolState,
OverlayUIState(const DxgiAPI* dxgiAPI,
StateT& toolState,
TickFuncT tickFunc,
const CommonState& commonState,
HWND window);
@@ -20,26 +23,27 @@ class OverlayUIState final
D2DState _d2dState;
std::function<void()> _tickFunc;
std::thread _uiThread;
bool _cursorOnScreen = true;
bool _clearOnCursorLeavingScreen = false;
template<typename ToolT, typename TickFuncT>
static std::unique_ptr<OverlayUIState> CreateInternal(ToolT& toolState,
static std::unique_ptr<OverlayUIState> CreateInternal(const DxgiAPI* dxgi,
ToolT& toolState,
TickFuncT tickFunc,
CommonState& commonState,
const wchar_t* toolWindowClassName,
void* windowParam,
const MonitorInfo& monitor,
const bool clearOnCursorLeavingScreen);
const bool excludeFromCapture);
public:
OverlayUIState(OverlayUIState&&) noexcept = default;
~OverlayUIState();
static std::unique_ptr<OverlayUIState> Create(BoundsToolState& toolState,
static std::unique_ptr<OverlayUIState> Create(const DxgiAPI* dxgi,
BoundsToolState& toolState,
CommonState& commonState,
const MonitorInfo& monitor);
static std::unique_ptr<OverlayUIState> Create(Serialized<MeasureToolState>& toolState,
static std::unique_ptr<OverlayUIState> Create(const DxgiAPI* dxgi,
Serialized<MeasureToolState>& toolState,
CommonState& commonState,
const MonitorInfo& monitor);
inline HWND overlayWindowHandle() const

View File

@@ -3,9 +3,9 @@
#include "PerGlyphOpacityTextRender.h"
PerGlyphOpacityTextRender::PerGlyphOpacityTextRender(
wil::com_ptr<ID2D1Factory> pD2DFactory,
wil::com_ptr<ID2D1HwndRenderTarget> rt,
wil::com_ptr<ID2D1SolidColorBrush> baseBrush) :
winrt::com_ptr<ID2D1Factory> pD2DFactory,
winrt::com_ptr<ID2D1RenderTarget> rt,
winrt::com_ptr<ID2D1SolidColorBrush> baseBrush) :
_pD2DFactory{ pD2DFactory.get() },
_rt{ rt.get() },
_baseBrush{ baseBrush.get() }

View File

@@ -17,13 +17,13 @@ struct OpacityEffect : winrt::implements<OpacityEffect, IDrawingEffect>
struct PerGlyphOpacityTextRender : winrt::implements<PerGlyphOpacityTextRender, IDWriteTextRenderer>
{
ID2D1Factory* _pD2DFactory = nullptr;
ID2D1HwndRenderTarget* _rt = nullptr;
ID2D1RenderTarget* _rt = nullptr;
ID2D1SolidColorBrush* _baseBrush = nullptr;
PerGlyphOpacityTextRender(
wil::com_ptr<ID2D1Factory> pD2DFactory,
wil::com_ptr<ID2D1HwndRenderTarget> rt,
wil::com_ptr<ID2D1SolidColorBrush> baseBrush);
winrt::com_ptr<ID2D1Factory> pD2DFactory,
winrt::com_ptr<ID2D1RenderTarget> rt,
winrt::com_ptr<ID2D1SolidColorBrush> baseBrush);
HRESULT __stdcall DrawGlyphRun(void* clientDrawingContext,
FLOAT baselineOriginX,

View File

@@ -3,6 +3,7 @@
#include <common/display/dpi_aware.h>
#include <common/display/monitors.h>
#include <common/utils/logger_helper.h>
#include <common/utils/UnhandledExceptionHandler.h>
#include <common/logger/logger.h>
#include "../MeasureToolModuleInterface/trace.h"
@@ -14,8 +15,6 @@
//#define DEBUG_PRIMARY_MONITOR_ONLY
std::recursive_mutex gpuAccessLock;
namespace winrt::PowerToys::MeasureToolCore::implementation
{
void Core::MouseCaptureThread()
@@ -34,17 +33,33 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
_stopMouseCaptureThreadSignal{ wil::EventOptions::ManualReset },
_mouseCaptureThread{ [this] { MouseCaptureThread(); } }
{
Trace::RegisterProvider();
LoggerHelpers::init_logger(L"Measure Tool", L"Core", "Measure Tool");
}
Core::~Core()
{
_stopMouseCaptureThreadSignal.SetEvent();
_mouseCaptureThread.join();
Close();
}
void Core::Close()
{
ResetState();
Trace::UnregisterProvider();
// avoid triggering d2d debug layer leak on shutdown
dxgiAPI = DxgiAPI{ DxgiAPI::Uninitialized{} };
#if 0
winrt::com_ptr<IDXGIDebug> dxgiDebug;
winrt::check_hresult(DXGIGetDebugInterface1({},
winrt::guid_of<IDXGIDebug>(),
dxgiDebug.put_void()));
dxgiDebug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_ALL);
#endif
if (!_stopMouseCaptureThreadSignal.is_signaled())
_stopMouseCaptureThreadSignal.SetEvent();
if (_mouseCaptureThread.joinable())
_mouseCaptureThread.join();
}
void Core::ResetState()
@@ -67,6 +82,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
_settings = Settings::LoadFromFile();
_commonState.units = _settings.units;
_commonState.lineColor.r = _settings.lineColor[0] / 255.f;
_commonState.lineColor.g = _settings.lineColor[1] / 255.f;
_commonState.lineColor.b = _settings.lineColor[2] / 255.f;
@@ -78,13 +94,15 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
ResetState();
#if defined(DEBUG_PRIMARY_MONITOR_ONLY)
const auto& monitorInfo = MonitorInfo::GetPrimaryMonitor();
std::vector<MonitorInfo> monitors = { MonitorInfo::GetPrimaryMonitor() };
const auto& monitorInfo = monitors[0];
#else
const auto monitors = MonitorInfo::GetMonitors(true);
for (const auto& monitorInfo : monitors)
#endif
{
auto overlayUI = OverlayUIState::Create(_boundsToolState,
auto overlayUI = OverlayUIState::Create(&dxgiAPI,
_boundsToolState,
_commonState,
monitorInfo);
#if !defined(DEBUG_PRIMARY_MONITOR_ONLY)
@@ -120,7 +138,8 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
for (const auto& monitorInfo : monitors)
#endif
{
auto overlayUI = OverlayUIState::Create(_measureToolState,
auto overlayUI = OverlayUIState::Create(&dxgiAPI,
_measureToolState,
_commonState,
monitorInfo);
#if !defined(DEBUG_PRIMARY_MONITOR_ONLY)
@@ -133,7 +152,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
for (size_t i = 0; i < monitors.size(); ++i)
{
auto thread = StartCapturingThread(
&_d3dState,
&dxgiAPI,
_commonState,
_measureToolState,
_overlayUIStates[i]->overlayWindowHandle(),

View File

@@ -8,12 +8,29 @@
#include <common/utils/serialized.h>
#include "ScreenCapturing.h"
struct PowerToysMisc
{
PowerToysMisc()
{
Trace::RegisterProvider();
LoggerHelpers::init_logger(L"Measure Tool", L"Core", "Measure Tool");
InitUnhandledExceptionHandler();
}
~PowerToysMisc()
{
Trace::UnregisterProvider();
}
};
namespace winrt::PowerToys::MeasureToolCore::implementation
{
struct Core : CoreT<Core>
struct Core : PowerToysMisc, CoreT<Core>
{
Core();
~Core();
void Close();
void StartBoundsTool();
void StartMeasureTool(const bool horizontal, const bool vertical);
void SetToolCompletionEvent(ToolSessionCompleted sessionCompletedTrigger);
@@ -22,7 +39,7 @@ namespace winrt::PowerToys::MeasureToolCore::implementation
float GetDPIScaleForWindow(uint64_t windowHandle);
void MouseCaptureThread();
D3DState _d3dState;
DxgiAPI dxgiAPI;
wil::shared_event _stopMouseCaptureThreadSignal;
std::thread _mouseCaptureThread;

View File

@@ -11,7 +11,7 @@ namespace PowerToys
delegate void ToolSessionCompleted();
[default_interface]
runtimeclass Core
runtimeclass Core : Windows.Foundation.IClosable
{
Core();
void SetToolCompletionEvent(event ToolSessionCompleted completionTrigger);

View File

@@ -55,7 +55,7 @@
<SubSystem>Windows</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<ModuleDefinitionFile>MeasureTool.def</ModuleDefinitionFile>
<AdditionalDependencies>Shell32.lib;Shcore.lib;Dwmapi.lib;Gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Dbghelp.lib;Shell32.lib;Shcore.lib;dcomp.lib;DXGI.lib;Dwmapi.lib;Gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
@@ -74,8 +74,11 @@
<ClInclude Include="..\MeasureToolModuleInterface\trace.h" />
<ClInclude Include="BoundsToolOverlayUI.h" />
<ClInclude Include="Clipboard.h" />
<ClInclude Include="CoordinateSystemConversion.h" />
<ClInclude Include="constants.h" />
<ClInclude Include="D2DState.h" />
<ClInclude Include="DxgiAPI.h" />
<ClInclude Include="Measurement.h" />
<ClInclude Include="MeasureToolOverlayUI.h" />
<ClInclude Include="PerGlyphOpacityTextRender.h" />
<ClInclude Include="resource.h" />
@@ -96,7 +99,9 @@
<ClCompile Include="BoundsToolOverlayUI.cpp" />
<ClCompile Include="Clipboard.cpp" />
<ClCompile Include="D2DState.cpp" />
<ClCompile Include="DxgiAPI.cpp" />
<ClCompile Include="EdgeDetection.cpp" />
<ClCompile Include="Measurement.cpp" />
<ClCompile Include="MeasureToolOverlayUI.cpp" />
<ClCompile Include="OverlayUI.cpp" />
<ClCompile Include="PerGlyphOpacityTextRender.cpp" />

View File

@@ -17,6 +17,8 @@
<ClCompile Include="..\MeasureToolModuleInterface\trace.cpp" />
<ClCompile Include="Clipboard.cpp" />
<ClCompile Include="PerGlyphOpacityTextRender.cpp" />
<ClCompile Include="Measurement.cpp" />
<ClCompile Include="DxgiAPI.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
@@ -34,6 +36,9 @@
<ClInclude Include="Clipboard.h" />
<ClInclude Include="PerGlyphOpacityTextRender.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="CoordinateSystemConversion.h" />
<ClInclude Include="Measurement.h" />
<ClInclude Include="DxgiAPI.h" />
</ItemGroup>
<ItemGroup>
<Filter Include="Assets">

View File

@@ -10,65 +10,47 @@
//#define DEBUG_EDGES
D3DState::D3DState()
namespace
{
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifndef NDEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
HRESULT hr =
D3D11CreateDevice(nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3dDevice.put(),
nullptr,
nullptr);
if (hr == DXGI_ERROR_UNSUPPORTED)
winrt::GraphicsCaptureItem CreateCaptureItemForMonitor(HMONITOR monitor)
{
hr = D3D11CreateDevice(nullptr,
D3D_DRIVER_TYPE_WARP,
nullptr,
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3dDevice.put(),
nullptr,
nullptr);
}
winrt::check_hresult(hr);
auto captureInterop = winrt::get_activation_factory<
winrt::GraphicsCaptureItem,
IGraphicsCaptureItemInterop>();
dxgiDevice = d3dDevice.as<IDXGIDevice>();
winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), d3dDeviceInspectable.put()));
winrt::GraphicsCaptureItem item = nullptr;
winrt::check_hresult(captureInterop->CreateForMonitor(
monitor,
winrt::guid_of<winrt::GraphicsCaptureItem>(),
winrt::put_abi(item)));
return item;
}
}
class D3DCaptureState final
{
D3DState* d3dState = nullptr;
DxgiAPI* dxgiAPI = nullptr;
winrt::IDirect3DDevice device;
winrt::com_ptr<IDXGISwapChain1> swapChain;
winrt::com_ptr<ID3D11DeviceContext> context;
winrt::SizeInt32 frameSize;
winrt::SizeInt32 frameSize;
HMONITOR monitor = {};
winrt::DirectXPixelFormat pixelFormat;
winrt::Direct3D11CaptureFramePool framePool;
winrt::GraphicsCaptureSession session;
winrt::Direct3D11CaptureFramePool framePool = nullptr;
winrt::GraphicsCaptureSession session = nullptr;
std::function<void(MappedTextureView)> frameCallback;
Box monitorArea;
bool captureOutsideOfMonitor = false;
bool continuousCapture = false;
D3DCaptureState(D3DState* d3dState,
winrt::com_ptr<IDXGISwapChain1> _swapChain,
winrt::com_ptr<ID3D11DeviceContext> _context,
const winrt::GraphicsCaptureItem& item,
winrt::DirectXPixelFormat _pixelFormat,
Box monitorArea,
const bool captureOutsideOfMonitor);
D3DCaptureState(DxgiAPI* dxgiAPI,
winrt::com_ptr<IDXGISwapChain1> swapChain,
winrt::DirectXPixelFormat pixelFormat,
MonitorInfo monitorInfo,
const bool continuousCapture);
winrt::com_ptr<ID3D11Texture2D> CopyFrameToCPU(const winrt::com_ptr<ID3D11Texture2D>& texture);
@@ -76,14 +58,13 @@ class D3DCaptureState final
void StartSessionInPreferredMode();
std::mutex destructorMutex;
std::mutex frameArrivedMutex;
public:
static std::unique_ptr<D3DCaptureState> Create(D3DState* d3dState,
winrt::GraphicsCaptureItem item,
static std::unique_ptr<D3DCaptureState> Create(DxgiAPI* dxgiAPI,
MonitorInfo monitorInfo,
const winrt::DirectXPixelFormat pixelFormat,
Box monitorSize,
const bool captureOutsideOfMonitor);
const bool continuousCapture);
~D3DCaptureState();
@@ -93,25 +74,19 @@ public:
void StopCapture();
};
D3DCaptureState::D3DCaptureState(D3DState* _d3dState,
D3DCaptureState::D3DCaptureState(DxgiAPI* dxgiAPI,
winrt::com_ptr<IDXGISwapChain1> _swapChain,
winrt::com_ptr<ID3D11DeviceContext> _context,
const winrt::GraphicsCaptureItem& item,
winrt::DirectXPixelFormat _pixelFormat,
Box _monitorArea,
const bool _captureOutsideOfMonitor) :
d3dState{ _d3dState },
device{ _d3dState->d3dDeviceInspectable.as<winrt::IDirect3DDevice>() },
winrt::DirectXPixelFormat pixelFormat_,
MonitorInfo monitorInfo,
const bool continuousCapture_) :
dxgiAPI{ dxgiAPI },
device{ dxgiAPI->d3dForCapture.d3dDeviceInspectable.as<winrt::IDirect3DDevice>() },
swapChain{ std::move(_swapChain) },
context{ std::move(_context) },
frameSize{ item.Size() },
pixelFormat{ std::move(_pixelFormat) },
framePool{ winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(device, pixelFormat, 1, item.Size()) },
session{ framePool.CreateCaptureSession(item) },
monitorArea{ _monitorArea },
captureOutsideOfMonitor{ _captureOutsideOfMonitor }
pixelFormat{ std::move(pixelFormat_) },
monitor{ monitorInfo.GetHandle() },
monitorArea{ monitorInfo.GetScreenSize(true) },
continuousCapture{ continuousCapture_ }
{
framePool.FrameArrived({ this, &D3DCaptureState::OnFrameArrived });
}
winrt::com_ptr<ID3D11Texture2D> D3DCaptureState::CopyFrameToCPU(const winrt::com_ptr<ID3D11Texture2D>& frameTexture)
@@ -124,8 +99,8 @@ winrt::com_ptr<ID3D11Texture2D> D3DCaptureState::CopyFrameToCPU(const winrt::com
desc.BindFlags = 0;
winrt::com_ptr<ID3D11Texture2D> cpuTexture;
winrt::check_hresult(d3dState->d3dDevice->CreateTexture2D(&desc, nullptr, cpuTexture.put()));
context->CopyResource(cpuTexture.get(), frameTexture.get());
winrt::check_hresult(dxgiAPI->d3dForCapture.d3dDevice->CreateTexture2D(&desc, nullptr, cpuTexture.put()));
dxgiAPI->d3dForCapture.d3dContext->CopyResource(cpuTexture.get(), frameTexture.get());
return cpuTexture;
}
@@ -142,15 +117,25 @@ auto GetDXGIInterfaceFromObject(winrt::IInspectable const& object)
void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& sender, const winrt::IInspectable&)
{
// Prevent calling a callback on a partially destroyed state
std::unique_lock callbackLock{ destructorMutex };
std::lock_guard callbackLock{ frameArrivedMutex };
bool resized = false;
POINT cursorPos = {};
GetCursorPos(&cursorPos);
auto frame = sender.TryGetNextFrame();
winrt::check_bool(frame);
if (monitorArea.inside(cursorPos) || captureOutsideOfMonitor)
winrt::Direct3D11CaptureFrame frame = nullptr;
try
{
frame = sender.TryGetNextFrame();
}
catch (...)
{
}
if (!frame)
return;
if (monitorArea.inside(cursorPos) || !continuousCapture)
{
winrt::com_ptr<ID3D11Texture2D> texture;
{
@@ -170,7 +155,10 @@ void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& se
auto gpuTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(surface);
texture = CopyFrameToCPU(gpuTexture);
surface.Close();
MappedTextureView textureView{ texture, context, static_cast<size_t>(frameSize.Width), static_cast<size_t>(frameSize.Height) };
MappedTextureView textureView{ texture,
dxgiAPI->d3dForCapture.d3dContext,
static_cast<size_t>(frameSize.Width),
static_cast<size_t>(frameSize.Height) };
frameCallback(std::move(textureView));
}
@@ -178,69 +166,60 @@ void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& se
frame.Close();
DXGI_PRESENT_PARAMETERS presentParameters = {};
swapChain->Present1(1, 0, &presentParameters);
if (resized)
{
framePool.Recreate(device, pixelFormat, 2, frameSize);
}
}
std::unique_ptr<D3DCaptureState> D3DCaptureState::Create(D3DState* d3dState,
winrt::GraphicsCaptureItem item,
std::unique_ptr<D3DCaptureState> D3DCaptureState::Create(DxgiAPI* dxgiAPI,
MonitorInfo monitorInfo,
const winrt::DirectXPixelFormat pixelFormat,
Box monitorArea,
const bool captureOutsideOfMonitor)
const bool continuousCapture)
{
std::lock_guard guard{ gpuAccessLock };
const auto dims = monitorInfo.GetScreenSize(true);
const DXGI_SWAP_CHAIN_DESC1 desc = {
.Width = static_cast<uint32_t>(item.Size().Width),
.Height = static_cast<uint32_t>(item.Size().Height),
.Width = static_cast<uint32_t>(dims.width()),
.Height = static_cast<uint32_t>(dims.height()),
.Format = static_cast<DXGI_FORMAT>(pixelFormat),
.SampleDesc = { .Count = 1, .Quality = 0 },
.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
.BufferCount = 2,
.Scaling = DXGI_SCALING_STRETCH,
.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL,
.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD,
.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED,
};
winrt::com_ptr<IDXGIAdapter> adapter;
winrt::check_hresult(d3dState->dxgiDevice->GetParent(winrt::guid_of<IDXGIAdapter>(), adapter.put_void()));
winrt::com_ptr<IDXGIFactory2> factory;
winrt::check_hresult(adapter->GetParent(winrt::guid_of<IDXGIFactory2>(), factory.put_void()));
winrt::com_ptr<IDXGISwapChain1> swapChain;
winrt::check_hresult(factory->CreateSwapChainForComposition(d3dState->d3dDevice.get(), &desc, nullptr, swapChain.put()));
winrt::com_ptr<ID3D11DeviceContext> context;
d3dState->d3dDevice->GetImmediateContext(context.put());
winrt::check_bool(context);
auto contextMultithread = context.as<ID3D11Multithread>();
contextMultithread->SetMultithreadProtected(true);
winrt::check_hresult(dxgiAPI->d3dForCapture.dxgiFactory2->CreateSwapChainForComposition(dxgiAPI->d3dForCapture.d3dDevice.get(),
&desc,
nullptr,
swapChain.put()));
// We must create the object in a heap, since we need to pin it in memory to receive callbacks
auto statePtr = new D3DCaptureState{ d3dState,
auto statePtr = new D3DCaptureState{ dxgiAPI,
std::move(swapChain),
std::move(context),
item,
pixelFormat,
monitorArea,
captureOutsideOfMonitor };
std::move(monitorInfo),
continuousCapture };
return std::unique_ptr<D3DCaptureState>{ statePtr };
}
D3DCaptureState::~D3DCaptureState()
{
std::unique_lock callbackLock{ destructorMutex };
std::unique_lock callbackLock{ frameArrivedMutex };
StopCapture();
framePool.Close();
}
void D3DCaptureState::StartSessionInPreferredMode()
{
auto item = CreateCaptureItemForMonitor(monitor);
frameSize = item.Size();
framePool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(device, pixelFormat, 2, item.Size());
session = framePool.CreateCaptureSession(item);
framePool.FrameArrived({ this, &D3DCaptureState::OnFrameArrived });
// Try disable border if possible (available on Windows ver >= 20348)
if (auto session3 = session.try_as<winrt::IGraphicsCaptureSession3>())
{
@@ -270,27 +249,35 @@ MappedTextureView D3DCaptureState::CaptureSingleFrame()
result.emplace(std::move(tex));
frameArrivedEvent.SetEvent();
};
std::lock_guard guard{ gpuAccessLock };
StartSessionInPreferredMode();
frameArrivedEvent.wait();
assert(result.has_value());
return std::move(*result);
}
void D3DCaptureState::StopCapture()
{
session.Close();
try
{
if (session)
session.Close();
if (framePool)
framePool.Close();
}
catch (...)
{
// RPC call might fail here
}
}
void UpdateCaptureState(const CommonState& commonState,
Serialized<MeasureToolState>& state,
HWND window,
const MappedTextureView& textureView,
const bool continuousCapture)
const MappedTextureView& textureView)
{
const auto cursorPos = convert::FromSystemToRelative(window, commonState.cursorPosSystemSpace);
const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
const bool cursorInLeftScreenHalf = cursorPos.x < textureView.view.width / 2;
const bool cursorInTopScreenHalf = cursorPos.y < textureView.view.height / 2;
uint8_t pixelTolerance = {};
@@ -310,8 +297,7 @@ void UpdateCaptureState(const CommonState& commonState,
const RECT bounds = DetectEdges(textureView.view,
cursorPos,
perColorChannelEdgeDetection,
pixelTolerance,
continuousCapture);
pixelTolerance);
#if defined(DEBUG_EDGES)
char buffer[256];
@@ -328,50 +314,54 @@ void UpdateCaptureState(const CommonState& commonState,
OutputDebugStringA(buffer);
#endif
state.Access([&](MeasureToolState& state) {
state.perScreen[window].measuredEdges = bounds;
state.perScreen[window].measuredEdges = Measurement{ bounds };
});
}
std::thread StartCapturingThread(D3DState* d3dState,
std::thread StartCapturingThread(DxgiAPI* dxgiAPI,
const CommonState& commonState,
Serialized<MeasureToolState>& state,
HWND window,
MonitorInfo monitor)
{
return SpawnLoggedThread(L"Screen Capture thread", [&state, &commonState, monitor, window, d3dState] {
auto captureInterop = winrt::get_activation_factory<
winrt::GraphicsCaptureItem,
IGraphicsCaptureItemInterop>();
winrt::GraphicsCaptureItem item = nullptr;
winrt::check_hresult(captureInterop->CreateForMonitor(
monitor.GetHandle(),
winrt::guid_of<winrt::GraphicsCaptureItem>(),
winrt::put_abi(item)));
return SpawnLoggedThread(L"Screen Capture thread", [&state, &commonState, monitor, window, dxgiAPI] {
bool continuousCapture = {};
state.Read([&](const MeasureToolState& state) {
continuousCapture = state.global.continuousCapture;
});
const auto monitorArea = monitor.GetScreenSize(true);
auto captureState = D3DCaptureState::Create(d3dState,
item,
auto captureState = D3DCaptureState::Create(dxgiAPI,
monitor,
winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized,
monitorArea,
!continuousCapture);
continuousCapture);
const auto monitorArea = monitor.GetScreenSize(true);
bool mouseOnMonitor = false;
if (continuousCapture)
{
captureState->StartCapture([&, window](MappedTextureView textureView) {
UpdateCaptureState(commonState, state, window, textureView, continuousCapture);
});
while (IsWindow(window) && !commonState.closeOnOtherMonitors)
{
std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION);
if (mouseOnMonitor == monitorArea.inside(commonState.cursorPosSystemSpace))
{
std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION);
continue;
}
mouseOnMonitor = !mouseOnMonitor;
if (mouseOnMonitor)
{
captureState->StartCapture([&, window](MappedTextureView textureView) {
UpdateCaptureState(commonState, state, window, textureView);
});
}
else
{
state.Access([&](MeasureToolState& state) {
state.perScreen[window].measuredEdges = {};
});
captureState->StopCapture();
}
}
captureState->StopCapture();
}
else
{
@@ -394,7 +384,15 @@ std::thread StartCapturingThread(D3DState* d3dState,
auto path = std::filesystem::temp_directory_path() / buf;
textureView.view.SaveAsBitmap(path.string().c_str());
#endif
UpdateCaptureState(commonState, state, window, textureView, continuousCapture);
UpdateCaptureState(commonState, state, window, textureView);
mouseOnMonitor = true;
}
else if (mouseOnMonitor)
{
state.Access([&](MeasureToolState& state) {
state.perScreen[window].measuredEdges = {};
});
mouseOnMonitor = false;
}
const auto frameTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - now);
@@ -404,5 +402,7 @@ std::thread StartCapturingThread(D3DState* d3dState,
}
}
}
captureState->StopCapture();
});
}

View File

@@ -1,19 +1,11 @@
#pragma once
#include "DxgiAPI.h"
#include "ToolState.h"
#include <common/utils/serialized.h>
struct D3DState
{
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::com_ptr<IDXGIDevice> dxgiDevice;
winrt::com_ptr<IInspectable> d3dDeviceInspectable;
D3DState();
};
std::thread StartCapturingThread(D3DState* d3dState,
std::thread StartCapturingThread(DxgiAPI* dxgiAPI,
const CommonState& commonState,
Serialized<MeasureToolState>& state,
HWND targetWindow,

View File

@@ -15,6 +15,7 @@ namespace
const wchar_t JSON_KEY_PIXEL_TOLERANCE[] = L"PixelTolerance";
const wchar_t JSON_KEY_PER_COLOR_CHANNEL_EDGE_DETECTION[] = L"PerColorChannelEdgeDetection";
const wchar_t JSON_KEY_MEASURE_CROSS_COLOR[] = L"MeasureCrossColor";
const wchar_t JSON_KEY_UNITS_OF_MEASURE[] = L"UnitsOfMeasure";
}
Settings Settings::LoadFromFile()
@@ -65,6 +66,14 @@ Settings Settings::LoadFromFile()
catch (...)
{
}
try
{
result.units = static_cast<Measurement::Unit>(props.GetNamedObject(JSON_KEY_UNITS_OF_MEASURE).GetNamedNumber(JSON_KEY_VALUE));
}
catch (...)
{
}
}
catch (...)
{

View File

@@ -3,6 +3,8 @@
#include <array>
#include <cinttypes>
#include "Measurement.h"
struct Settings
{
uint8_t pixelTolerance = 30;
@@ -10,6 +12,7 @@ struct Settings
bool drawFeetOnCross = true;
bool perColorChannelEdgeDetection = false;
std::array<uint8_t, 3> lineColor = {255, 69, 0};
Measurement::Unit units = Measurement::Unit::Pixel;
static Settings LoadFromFile();
};

View File

@@ -16,10 +16,11 @@
//#define DEBUG_OVERLAY
#include "BGRATextureView.h"
#include "Measurement.h"
struct OverlayBoxText
{
std::array<wchar_t, 32> buffer = {};
std::array<wchar_t, 128> buffer = {};
};
struct CommonState
@@ -28,6 +29,8 @@ struct CommonState
D2D1::ColorF lineColor = D2D1::ColorF::OrangeRed;
Box toolbarBoundingBox;
Measurement::Unit units = Measurement::Unit::Pixel;
mutable Serialized<OverlayBoxText> overlayBoxText;
POINT cursorPosSystemSpace = {}; // updated atomically
std::atomic_bool closeOnOtherMonitors = false;
@@ -38,7 +41,7 @@ struct BoundsToolState
struct PerScreen
{
std::optional<D2D_POINT_2F> currentRegionStart;
std::vector<D2D1_RECT_F> measurements;
std::vector<Measurement> measurements;
};
std::unordered_map<HWND, PerScreen> perScreen;
@@ -67,7 +70,7 @@ struct MeasureToolState
{
bool cursorInLeftScreenHalf = false;
bool cursorInTopScreenHalf = false;
RECT measuredEdges = {};
std::optional<Measurement> measuredEdges;
// While not in a continuous capturing mode, we need to draw captured backgrounds. These are passed
// directly from a capturing thread.
const MappedTextureView* capturedScreenTexture = nullptr;
@@ -79,6 +82,3 @@ struct MeasureToolState
CommonState* commonState = nullptr; // required for WndProc
};
// Concurrently accessing Direct2D and Direct3D APIs make the driver go boom
extern std::recursive_mutex gpuAccessLock;

View File

@@ -4,12 +4,11 @@
namespace consts
{
constexpr inline size_t TARGET_FRAME_RATE = 120;
constexpr inline auto TARGET_FRAME_DURATION = std::chrono::milliseconds{ 1000 } / TARGET_FRAME_RATE;
constexpr inline size_t TARGET_FRAME_RATE = 90;
constexpr inline auto TARGET_FRAME_DURATION = std::chrono::microseconds{ 1000000 } / TARGET_FRAME_RATE;
constexpr inline float FONT_SIZE = 14.f;
constexpr inline float TEXT_BOX_CORNER_RADIUS = 4.f;
constexpr inline float TEXT_BOX_MARGIN_COEFF = 1.25f;
constexpr inline float FEET_HALF_LENGTH = 2.f;
constexpr inline float SHADOW_OPACITY = .4f;
constexpr inline float SHADOW_RADIUS = 6.f;

View File

@@ -11,6 +11,7 @@
#include <d3d11.h>
#include <d3d11_4.h>
#include <dxgi1_6.h>
#include <dxgidebug.h>
#include <d2d1_3.h>
#include <dwrite.h>
#include <dwmapi.h>

View File

@@ -4,6 +4,7 @@
using System;
using ManagedCommon;
using MeasureToolUI.Helpers;
using Microsoft.UI.Xaml;
namespace MeasureToolUI
@@ -42,7 +43,18 @@ namespace MeasureToolUI
}
}
_window = new MainWindow();
PowerToys.MeasureToolCore.Core core = null;
try
{
core = new PowerToys.MeasureToolCore.Core();
}
catch (Exception ex)
{
Logger.LogError($"MeasureToolCore failed to initialize: {ex}");
Environment.Exit(1);
}
_window = new MainWindow(core);
_window.Activate();
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using interop;
using Microsoft.VisualBasic;
using Windows.Storage;
namespace MeasureToolUI.Helpers
{
public static class Logger
{
private static readonly string ApplicationLogPath = Path.Combine(interop.Constants.AppDataPath(), "Measure Tool\\MeasureToolUI\\Logs");
static Logger()
{
if (!Directory.Exists(ApplicationLogPath))
{
Directory.CreateDirectory(ApplicationLogPath);
}
// Using InvariantCulture since this is used for a log file name
var logFilePath = Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
Trace.AutoFlush = true;
}
public static void LogError(string message)
{
Log(message, "ERROR");
}
public static void LogError(string message, Exception ex)
{
Log(
message + Environment.NewLine +
ex?.Message + Environment.NewLine +
"Inner exception: " + Environment.NewLine +
ex?.InnerException?.Message + Environment.NewLine +
"Stack trace: " + Environment.NewLine +
ex?.StackTrace,
"ERROR");
}
public static void LogWarning(string message)
{
Log(message, "WARNING");
}
public static void LogInfo(string message)
{
Log(message, "INFO");
}
private static void Log(string message, string type)
{
Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay);
Trace.Indent();
Trace.WriteLine(GetCallerInfo());
Trace.WriteLine(message);
Trace.Unindent();
}
private static string GetCallerInfo()
{
StackTrace stackTrace = new StackTrace();
var methodName = stackTrace.GetFrame(3)?.GetMethod();
var className = methodName?.DeclaringType.Name;
return "[Method]: " + methodName?.Name + " [Class]: " + className;
}
}
}

View File

@@ -18,12 +18,12 @@ namespace MeasureToolUI
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx
public sealed partial class MainWindow : WindowEx, IDisposable
{
private const int WindowWidth = 216;
private const int WindowHeight = 50;
private PowerToys.MeasureToolCore.Core _coreLogic = new PowerToys.MeasureToolCore.Core();
private PowerToys.MeasureToolCore.Core _coreLogic;
private AppWindow _appWindow;
private PointInt32 _initialPosition;
@@ -34,14 +34,14 @@ namespace MeasureToolUI
this.SetWindowSize(WindowWidth, WindowHeight);
}
public MainWindow()
public MainWindow(PowerToys.MeasureToolCore.Core core)
{
InitializeComponent();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
_appWindow = AppWindow.GetFromWindowId(windowId);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
var presenter = _appWindow.Presenter as OverlappedPresenter;
presenter.IsAlwaysOnTop = true;
this.SetIsAlwaysOnTop(true);
@@ -51,6 +51,8 @@ namespace MeasureToolUI
this.SetIsMaximizable(false);
IsTitleBarVisible = false;
_coreLogic = core;
Closed += MainWindow_Closed;
DisplayArea displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest);
float dpiScale = _coreLogic.GetDPIScaleForWindow((int)hwnd);
@@ -64,6 +66,12 @@ namespace MeasureToolUI
OnPositionChanged(_initialPosition);
}
private void MainWindow_Closed(object sender, WindowEventArgs args)
{
_coreLogic?.Dispose();
_coreLogic = null;
}
private void UpdateToolUsageCompletionEvent(object sender)
{
_coreLogic.SetToolCompletionEvent(new PowerToys.MeasureToolCore.ToolSessionCompleted(() =>
@@ -136,5 +144,10 @@ namespace MeasureToolUI
_coreLogic.ResetState();
this.Close();
}
public void Dispose()
{
_coreLogic?.Dispose();
}
}
}

View File

@@ -79,6 +79,7 @@
<Folder Include="Assets\Icons\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj" />
</ItemGroup>

View File

@@ -13,5 +13,6 @@ internal static class NativeMethods
internal static readonly IntPtr HWND_TOPMOST = new System.IntPtr(-1);
internal const uint SWP_NOSIZE = 0x0001;
internal const uint SWP_NOMOVE = 0x0002;
internal const uint SWP_NOACTIVATE = 0x0010;
internal const uint SWP_SHOWWINDOW = 0x0040;
}

View File

@@ -14,6 +14,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public MeasureToolProperties()
{
ActivationShortcut = new HotkeySettings(true, false, false, true, 0x4D);
UnitsOfMeasure = new IntProperty(0);
PixelTolerance = new IntProperty(30);
ContinuousCapture = true;
DrawFeetOnCross = true;
@@ -32,6 +33,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool PerColorChannelEdgeDetection { get; set; }
public IntProperty UnitsOfMeasure { get; set; }
public IntProperty PixelTolerance { get; set; }
public StringProperty MeasureCrossColor { get; set; }

View File

@@ -129,6 +129,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels
}
}
public int UnitsOfMeasure
{
get
{
return Settings.Properties.UnitsOfMeasure.Value;
}
set
{
if (Settings.Properties.UnitsOfMeasure.Value != value)
{
Settings.Properties.UnitsOfMeasure.Value = value;
NotifyPropertyChanged();
}
}
}
public int PixelTolerance
{
get

View File

@@ -155,6 +155,18 @@
<value>Customize the shortcut to bring up the command bar</value>
<comment>"Screen Ruler" is the name of the utility</comment>
</data>
<data name="MeasureTool_UnitsOfMeasure.Header" xml:space="preserve">
<value>Units of measurement</value>
</data>
<data name="MeasureTool_UnitsOfMeasure_Pixels.Content" xml:space="preserve">
<value>Pixels</value>
</data>
<data name="MeasureTool_UnitsOfMeasure_Inches.Content" xml:space="preserve">
<value>Inches</value>
</data>
<data name="MeasureTool_UnitsOfMeasure_Centimeters.Content" xml:space="preserve">
<value>Centimeters</value>
</data>
<data name="MeasureTool_PixelTolerance.Header" xml:space="preserve">
<value>Pixel tolerance for edge detection</value>
</data>

View File

@@ -49,6 +49,7 @@
IsOpen="{x:Bind Mode=OneWay, Path=ViewModel.ShowContinuousCaptureWarning}"
IsTabStop="{x:Bind Mode=OneWay, Path=ViewModel.ShowContinuousCaptureWarning}"
IsClosable="False" />
<controls:Setting x:Uid="MeasureTool_PerColorChannelEdgeDetection" Icon="&#xE7FB;">
<controls:Setting.ActionContent>
<ToggleSwitch
@@ -56,6 +57,7 @@
x:Uid="MeasureTool_PerColorChannelEdgeDetection_ToggleSwitch" />
</controls:Setting.ActionContent>
</controls:Setting>
<controls:Setting x:Uid="MeasureTool_PixelTolerance">
<controls:Setting.ActionContent>
<Slider Minimum="0"
@@ -65,6 +67,16 @@
</controls:Setting.ActionContent>
</controls:Setting>
<!--<controls:Setting x:Uid="MeasureTool_UnitsOfMeasure" Icon="&#xECC6;">
<controls:Setting.ActionContent>
<ComboBox SelectedIndex="{x:Bind Path=ViewModel.UnitsOfMeasure, Mode=TwoWay}" MinWidth="{StaticResource SettingActionControlMinWidth}">
<ComboBoxItem x:Uid="MeasureTool_UnitsOfMeasure_Pixels" />
<ComboBoxItem x:Uid="MeasureTool_UnitsOfMeasure_Inches" />
<ComboBoxItem x:Uid="MeasureTool_UnitsOfMeasure_Centimeters" />
</ComboBox>
</controls:Setting.ActionContent>
</controls:Setting>-->
<controls:Setting x:Uid="MeasureTool_DrawFeetOnCross">
<controls:Setting.ActionContent>
<ToggleSwitch IsOn="{x:Bind ViewModel.DrawFeetOnCross, Mode=TwoWay}" x:Uid="MeasureTool_DrawFeetOnCross_ToggleSwitch" />