Skip to content

Commit e753069

Browse files
authored
优化主窗口位置 (#762)
* feat: 保存 DPI 无关的窗口尺寸 * feat: 启动时窗口必须在屏幕工作区内
1 parent c32a7c9 commit e753069

File tree

8 files changed

+204
-76
lines changed

8 files changed

+204
-76
lines changed

src/Magpie.App/App.cpp

+2-7
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,8 @@ StartUpOptions App::Initialize(int) {
8282
}
8383

8484
result.IsError = false;
85-
const RECT& windowRect = settings.WindowRect();
86-
result.MainWndRect = {
87-
(float)windowRect.left,
88-
(float)windowRect.top,
89-
(float)windowRect.right,
90-
(float)windowRect.bottom
91-
};
85+
result.MainWindowCenter = settings.MainWindowCenter();
86+
result.MainWindowSizeInDips = settings.MainWindowSizeInDips();
9287
result.IsWndMaximized= settings.IsWindowMaximized();
9388
result.IsNeedElevated = settings.IsAlwaysRunAsAdmin();
9489

src/Magpie.App/App.idl

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ namespace Magpie.App {
4343
};
4444

4545
struct StartUpOptions {
46-
Windows.Foundation.Rect MainWndRect;
46+
Windows.Foundation.Point MainWindowCenter;
47+
Windows.Foundation.Size MainWindowSizeInDips;
4748
Boolean IsError;
4849
Boolean IsWndMaximized;
4950
Boolean IsNeedElevated;

src/Magpie.App/AppSettings.cpp

+63-31
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
#include "JsonHelper.h"
1414
#include "ScalingMode.h"
1515
#include "LocalizationService.h"
16+
#include <ShellScalingApi.h>
17+
18+
#pragma comment(lib, "Shcore.lib")
1619

1720
using namespace ::Magpie::Core;
1821

@@ -449,13 +452,18 @@ void AppSettings::_UpdateWindowPlacement() noexcept {
449452
return;
450453
}
451454

452-
_windowRect = {
453-
wp.rcNormalPosition.left,
454-
wp.rcNormalPosition.top,
455-
wp.rcNormalPosition.right - wp.rcNormalPosition.left,
456-
wp.rcNormalPosition.bottom - wp.rcNormalPosition.top
455+
_mainWindowCenter = {
456+
(wp.rcNormalPosition.left + wp.rcNormalPosition.right) / 2.0f,
457+
(wp.rcNormalPosition.top + wp.rcNormalPosition.bottom) / 2.0f
458+
};
459+
460+
const float dpiFactor = GetDpiForWindow(hwndMain) / float(USER_DEFAULT_SCREEN_DPI);
461+
_mainWindowSizeInDips = {
462+
(wp.rcNormalPosition.right - wp.rcNormalPosition.left) / dpiFactor,
463+
(wp.rcNormalPosition.bottom - wp.rcNormalPosition.top) / dpiFactor,
457464
};
458-
_isWindowMaximized = wp.showCmd == SW_MAXIMIZE;
465+
466+
_isMainWindowMaximized = wp.showCmd == SW_MAXIMIZE;
459467
}
460468

461469
bool AppSettings::_Save(const _AppSettingsData& data) noexcept {
@@ -484,16 +492,16 @@ bool AppSettings::_Save(const _AppSettingsData& data) noexcept {
484492

485493
writer.Key("windowPos");
486494
writer.StartObject();
487-
writer.Key("x");
488-
writer.Int(data._windowRect.left);
489-
writer.Key("y");
490-
writer.Int(data._windowRect.top);
495+
writer.Key("centerX");
496+
writer.Double(data._mainWindowCenter.X);
497+
writer.Key("centerY");
498+
writer.Double(data._mainWindowCenter.Y);
491499
writer.Key("width");
492-
writer.Uint((uint32_t)data._windowRect.right);
500+
writer.Double(data._mainWindowSizeInDips.Width);
493501
writer.Key("height");
494-
writer.Uint((uint32_t)data._windowRect.bottom);
502+
writer.Double(data._mainWindowSizeInDips.Height);
495503
writer.Key("maximized");
496-
writer.Bool(data._isWindowMaximized);
504+
writer.Bool(data._isMainWindowMaximized);
497505
writer.EndObject();
498506

499507
writer.Key("shortcuts");
@@ -608,25 +616,49 @@ void AppSettings::_LoadSettings(const rapidjson::GenericObject<true, rapidjson::
608616

609617
auto windowPosNode = root.FindMember("windowPos");
610618
if (windowPosNode != root.MemberEnd() && windowPosNode->value.IsObject()) {
611-
const auto& windowRectObj = windowPosNode->value.GetObj();
612-
613-
int x = 0;
614-
int y = 0;
615-
if (JsonHelper::ReadInt(windowRectObj, "x", x, true)
616-
&& JsonHelper::ReadInt(windowRectObj, "y", y, true)) {
617-
_windowRect.left = x;
618-
_windowRect.top = y;
619-
}
620-
621-
uint32_t width = 0;
622-
uint32_t height = 0;
623-
if (JsonHelper::ReadUInt(windowRectObj, "width", width, true)
624-
&& JsonHelper::ReadUInt(windowRectObj, "height", height, true)) {
625-
_windowRect.right = (LONG)width;
626-
_windowRect.bottom = (LONG)height;
619+
const auto& windowPosObj = windowPosNode->value.GetObj();
620+
621+
Point center{};
622+
Size size{};
623+
if (JsonHelper::ReadFloat(windowPosObj, "centerX", center.X, true) &&
624+
JsonHelper::ReadFloat(windowPosObj, "centerY", center.Y, true) &&
625+
JsonHelper::ReadFloat(windowPosObj, "width", size.Width, true) &&
626+
JsonHelper::ReadFloat(windowPosObj, "height", size.Height, true)) {
627+
_mainWindowCenter = center;
628+
_mainWindowSizeInDips = size;
629+
} else {
630+
// 尽最大努力和旧版本兼容
631+
int x = 0;
632+
int y = 0;
633+
uint32_t width = 0;
634+
uint32_t height = 0;
635+
if (JsonHelper::ReadInt(windowPosObj, "x", x, true) &&
636+
JsonHelper::ReadInt(windowPosObj, "y", y, true) &&
637+
JsonHelper::ReadUInt(windowPosObj, "width", width, true) &&
638+
JsonHelper::ReadUInt(windowPosObj, "height", height, true)) {
639+
_mainWindowCenter = {
640+
x + width / 2.0f,
641+
y + height / 2.0f
642+
};
643+
644+
// 如果窗口位置不存在屏幕则使用主屏幕的缩放,猜错的后果仅是窗口尺寸错误,
645+
// 无论如何原始缩放信息已经丢失。
646+
const HMONITOR hMon = MonitorFromPoint(
647+
{ std::lroundf(_mainWindowCenter.X), std::lroundf(_mainWindowCenter.Y) },
648+
MONITOR_DEFAULTTOPRIMARY
649+
);
650+
651+
UINT dpi = USER_DEFAULT_SCREEN_DPI;
652+
GetDpiForMonitor(hMon, MDT_EFFECTIVE_DPI, &dpi, &dpi);
653+
const float dpiFactor = dpi / float(USER_DEFAULT_SCREEN_DPI);
654+
_mainWindowSizeInDips = {
655+
width / dpiFactor,
656+
height / dpiFactor
657+
};
658+
}
627659
}
628660

629-
JsonHelper::ReadBool(windowRectObj, "maximized", _isWindowMaximized);
661+
JsonHelper::ReadBool(windowPosObj, "maximized", _isMainWindowMaximized);
630662
}
631663

632664
auto shortcutsNode = root.FindMember("shortcuts");
@@ -742,7 +774,7 @@ bool AppSettings::_LoadProfile(
742774
const rapidjson::GenericObject<true, rapidjson::Value>& profileObj,
743775
Profile& profile,
744776
bool isDefault
745-
) {
777+
) const {
746778
if (!isDefault) {
747779
if (!JsonHelper::ReadString(profileObj, "name", profile.name, true)) {
748780
return false;

src/Magpie.App/AppSettings.h

+13-7
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ struct _AppSettingsData {
3636
// -1 表示使用系统设置
3737
int _language = -1;
3838

39-
// X, Y, 长, 高
40-
RECT _windowRect{ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT };
39+
// 保存窗口中心点和 DPI 无关的窗口尺寸
40+
Point _mainWindowCenter{};
41+
// 小于零表示默认位置和尺寸
42+
Size _mainWindowSizeInDips{ -1.0f,-1.0f };
4143

4244
Theme _theme = Theme::System;
4345
// 必须在 1~5 之间
@@ -59,7 +61,7 @@ struct _AppSettingsData {
5961
bool _isInlineParams = false;
6062
bool _isShowTrayIcon = true;
6163
bool _isAutoRestore = false;
62-
bool _isWindowMaximized = false;
64+
bool _isMainWindowMaximized = false;
6365
bool _isAutoCheckForUpdates = true;
6466
bool _isCheckForPreviewUpdates = false;
6567
};
@@ -115,12 +117,16 @@ class AppSettings : private _AppSettingsData {
115117
_themeChangedEvent.remove(token);
116118
}
117119

118-
const RECT& WindowRect() const noexcept {
119-
return _windowRect;
120+
Point MainWindowCenter() const noexcept {
121+
return _mainWindowCenter;
122+
}
123+
124+
Size MainWindowSizeInDips() const noexcept {
125+
return _mainWindowSizeInDips;
120126
}
121127

122128
bool IsWindowMaximized() const noexcept {
123-
return _isWindowMaximized;
129+
return _isMainWindowMaximized;
124130
}
125131

126132
const Shortcut& GetShortcut(ShortcutAction action) const {
@@ -363,7 +369,7 @@ class AppSettings : private _AppSettingsData {
363369
const rapidjson::GenericObject<true, rapidjson::Value>& profileObj,
364370
Profile& profile,
365371
bool isDefault = false
366-
);
372+
) const;
367373
bool _SetDefaultShortcuts();
368374
void _SetDefaultScalingModes();
369375

src/Magpie/MainWindow.cpp

+106-19
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
#include "Win32Utils.h"
55
#include "ThemeHelper.h"
66
#include "XamlApp.h"
7+
#include <ShellScalingApi.h>
8+
9+
#pragma comment(lib, "Shcore.lib")
710

811
namespace Magpie {
912

10-
bool MainWindow::Create(HINSTANCE hInstance, const RECT& windowRect, bool isMaximized) noexcept {
13+
bool MainWindow::Create(HINSTANCE hInstance, winrt::Point windowCenter, winrt::Size windowSizeInDips, bool isMaximized) noexcept {
1114
static const int _ = [](HINSTANCE hInstance) {
1215
WNDCLASSEXW wcex{};
1316
wcex.cbSize = sizeof(wcex);
@@ -27,7 +30,7 @@ bool MainWindow::Create(HINSTANCE hInstance, const RECT& windowRect, bool isMaxi
2730
return 0;
2831
}(hInstance);
2932

30-
_CreateWindow(hInstance, windowRect);
33+
const auto& [posToSet, sizeToSet] = _CreateWindow(hInstance, windowCenter, windowSizeInDips);
3134

3235
if (!_hWnd) {
3336
return false;
@@ -46,8 +49,10 @@ bool MainWindow::Create(HINSTANCE hInstance, const RECT& windowRect, bool isMaxi
4649

4750
// 1. 设置初始 XAML Islands 窗口的尺寸
4851
// 2. 刷新窗口边框
49-
// 3. 防止窗口显示时背景闪烁: https://stackoverflow.com/questions/69715610/how-to-initialize-the-background-color-of-win32-app-to-something-other-than-whit
50-
SetWindowPos(_hWnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
52+
// 3. 无法获知 DPI 的情况下 _CreateWindow 创建的窗口尺寸为零,在这里延后设置窗口位置
53+
// 4. 防止窗口显示时背景闪烁: https://stackoverflow.com/questions/69715610/how-to-initialize-the-background-color-of-win32-app-to-something-other-than-whit
54+
SetWindowPos(_hWnd, NULL, posToSet.x, posToSet.y, sizeToSet.cx, sizeToSet.cy,
55+
(sizeToSet.cx == 0 ? (SWP_NOMOVE | SWP_NOSIZE) : 0) | SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOCOPYBITS);
5156

5257
// Xaml 控件加载完成后显示主窗口
5358
_content.Loaded([this, isMaximized](winrt::IInspectable const&, winrt::RoutedEventArgs const&) {
@@ -155,8 +160,8 @@ LRESULT MainWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) noex
155160
// 设置窗口最小尺寸
156161
MINMAXINFO* mmi = (MINMAXINFO*)lParam;
157162
mmi->ptMinTrackSize = {
158-
std::lround(550 * _currentDpi / double(USER_DEFAULT_SCREEN_DPI)),
159-
std::lround(300 * _currentDpi / double(USER_DEFAULT_SCREEN_DPI))
163+
std::lroundf(550 * _currentDpi / float(USER_DEFAULT_SCREEN_DPI)),
164+
std::lroundf(300 * _currentDpi / float(USER_DEFAULT_SCREEN_DPI))
160165
};
161166
return 0;
162167
}
@@ -217,31 +222,113 @@ LRESULT MainWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) noex
217222
return base_type::_MessageHandler(msg, wParam, lParam);
218223
}
219224

220-
void MainWindow::_CreateWindow(HINSTANCE hInstance, const RECT& windowRect) noexcept {
221-
// 防止窗口启动时不在可见区域,Windows 不会自动处理。
222-
// 检查两个点的位置是否存在屏幕:窗口的中心点和上边框中心点。前者确保大部分窗口内容可见,后者确保大部分标题栏可见。
223-
const POINT windowCenter{
224-
(windowRect.left + windowRect.right) / 2,
225-
(windowRect.top + windowRect.bottom) / 2
226-
};
227-
const bool isValidPosition = MonitorFromPoint(windowCenter, MONITOR_DEFAULTTONULL)
228-
&& MonitorFromPoint({ windowCenter.x, windowRect.top }, MONITOR_DEFAULTTONULL);
225+
std::pair<POINT, SIZE> MainWindow::_CreateWindow(HINSTANCE hInstance, winrt::Point windowCenter, winrt::Size windowSizeInDips) noexcept {
226+
POINT windowPos = { CW_USEDEFAULT,CW_USEDEFAULT };
227+
SIZE windowSize{};
228+
229+
// windowSizeInDips 小于零表示默认位置和尺寸
230+
if (windowSizeInDips.Width > 0) {
231+
// 检查窗口中心点的 DPI,根据我的测试,创建窗口时 Windows 使用窗口中心点确定 DPI。
232+
// 如果窗口中心点不在任何屏幕上,则在默认位置启动,让调用者设置窗口尺寸。
233+
const HMONITOR hMon = MonitorFromPoint(
234+
{ std::lroundf(windowCenter.X),std::lroundf(windowCenter.Y) },
235+
MONITOR_DEFAULTTONULL
236+
);
237+
if (hMon) {
238+
UINT dpi = USER_DEFAULT_SCREEN_DPI;
239+
GetDpiForMonitor(hMon, MDT_EFFECTIVE_DPI, &dpi, &dpi);
240+
241+
const float dpiFactor = dpi / float(USER_DEFAULT_SCREEN_DPI);
242+
const winrt::Size windowSizeInPixels = {
243+
windowSizeInDips.Width * dpiFactor,
244+
windowSizeInDips.Height * dpiFactor
245+
};
246+
247+
windowSize.cx = std::lroundf(windowSizeInPixels.Width);
248+
windowSize.cy = std::lroundf(windowSizeInPixels.Height);
249+
250+
MONITORINFO mi{ sizeof(mi) };
251+
GetMonitorInfo(hMon, &mi);
252+
253+
// 确保启动位置在屏幕工作区内。不允许启动时跨越多个屏幕。
254+
if (windowSize.cx <= mi.rcWork.right - mi.rcWork.left && windowSize.cy <= mi.rcWork.bottom - mi.rcWork.top) {
255+
windowPos.x = std::lroundf(windowCenter.X - windowSizeInPixels.Width / 2);
256+
windowPos.x = std::clamp(windowPos.x, mi.rcWork.left, mi.rcWork.right - windowSize.cx);
257+
258+
windowPos.y = std::lroundf(windowCenter.Y - windowSizeInPixels.Height / 2);
259+
windowPos.y = std::clamp(windowPos.y, mi.rcWork.top, mi.rcWork.bottom - windowSize.cy);
260+
} else {
261+
// 屏幕工作区无法容纳窗口则使用默认窗口尺寸
262+
windowSize = {};
263+
windowSizeInDips.Width = -1.0f;
264+
}
265+
}
266+
}
229267

230268
// Win11 22H2 中为了使用 Mica 背景需指定 WS_EX_NOREDIRECTIONBITMAP
269+
// windowSize 可能为零,并返回窗口尺寸给调用者
231270
CreateWindowEx(
232271
Win32Utils::GetOSVersion().Is22H2OrNewer() ? WS_EX_NOREDIRECTIONBITMAP : 0,
233272
CommonSharedConstants::MAIN_WINDOW_CLASS_NAME,
234273
L"Magpie",
235274
WS_OVERLAPPEDWINDOW,
236-
isValidPosition ? windowRect.left : CW_USEDEFAULT,
237-
isValidPosition ? windowRect.top : CW_USEDEFAULT,
238-
windowRect.right - windowRect.left,
239-
windowRect.bottom - windowRect.top,
275+
windowPos.x,
276+
windowPos.y,
277+
windowSize.cx,
278+
windowSize.cy,
240279
NULL,
241280
NULL,
242281
hInstance,
243282
this
244283
);
284+
285+
if (windowSize.cx == 0) {
286+
const HMONITOR hMon = MonitorFromWindow(_hWnd, MONITOR_DEFAULTTONEAREST);
287+
288+
MONITORINFO mi{ sizeof(mi) };
289+
GetMonitorInfo(hMon, &mi);
290+
291+
const float dpiFactor = _currentDpi / float(USER_DEFAULT_SCREEN_DPI);
292+
const winrt::Size workingAreaSizeInDips = {
293+
(mi.rcWork.right - mi.rcWork.left) / dpiFactor,
294+
(mi.rcWork.bottom - mi.rcWork.top) / dpiFactor
295+
};
296+
297+
// 确保启动尺寸小于屏幕工作区
298+
if (windowSizeInDips.Width <= 0 ||
299+
windowSizeInDips.Width > workingAreaSizeInDips.Width ||
300+
windowSizeInDips.Height > workingAreaSizeInDips.Height) {
301+
// 默认尺寸
302+
static constexpr winrt::Size DEFAULT_SIZE{ 980.0f, 690.0f };
303+
304+
windowSizeInDips = DEFAULT_SIZE;
305+
306+
if (windowSizeInDips.Width > workingAreaSizeInDips.Width ||
307+
windowSizeInDips.Height > workingAreaSizeInDips.Height) {
308+
// 屏幕太小无法容纳默认尺寸
309+
windowSizeInDips.Width = workingAreaSizeInDips.Width * 0.8f;
310+
windowSizeInDips.Height = windowSizeInDips.Width * DEFAULT_SIZE.Height / DEFAULT_SIZE.Width;
311+
312+
if (windowSizeInDips.Height > workingAreaSizeInDips.Height) {
313+
windowSizeInDips.Height = workingAreaSizeInDips.Height * 0.8f;
314+
windowSizeInDips.Width = windowSizeInDips.Height * DEFAULT_SIZE.Width / DEFAULT_SIZE.Height;
315+
}
316+
}
317+
}
318+
319+
windowSize.cx = std::lroundf(windowSizeInDips.Width * dpiFactor);
320+
windowSize.cy = std::lroundf(windowSizeInDips.Height * dpiFactor);
321+
322+
// 确保启动位置在屏幕工作区内
323+
RECT targetRect;
324+
GetWindowRect(_hWnd, &targetRect);
325+
windowPos.x = std::clamp(targetRect.left, mi.rcWork.left, mi.rcWork.right - windowSize.cx);
326+
windowPos.y = std::clamp(targetRect.top, mi.rcWork.top, mi.rcWork.bottom - windowSize.cy);
327+
328+
return std::make_pair(windowPos, windowSize);
329+
} else {
330+
return {};
331+
}
245332
}
246333

247334
void MainWindow::_UpdateTheme() {

0 commit comments

Comments
 (0)