Skip to content

Commit

Permalink
优化主窗口位置 (#762)
Browse files Browse the repository at this point in the history
* feat: 保存 DPI 无关的窗口尺寸

* feat: 启动时窗口必须在屏幕工作区内
  • Loading branch information
Blinue committed Nov 26, 2023
1 parent c32a7c9 commit e753069
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 76 deletions.
9 changes: 2 additions & 7 deletions src/Magpie.App/App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,8 @@ StartUpOptions App::Initialize(int) {
}

result.IsError = false;
const RECT& windowRect = settings.WindowRect();
result.MainWndRect = {
(float)windowRect.left,
(float)windowRect.top,
(float)windowRect.right,
(float)windowRect.bottom
};
result.MainWindowCenter = settings.MainWindowCenter();
result.MainWindowSizeInDips = settings.MainWindowSizeInDips();
result.IsWndMaximized= settings.IsWindowMaximized();
result.IsNeedElevated = settings.IsAlwaysRunAsAdmin();

Expand Down
3 changes: 2 additions & 1 deletion src/Magpie.App/App.idl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ namespace Magpie.App {
};

struct StartUpOptions {
Windows.Foundation.Rect MainWndRect;
Windows.Foundation.Point MainWindowCenter;
Windows.Foundation.Size MainWindowSizeInDips;
Boolean IsError;
Boolean IsWndMaximized;
Boolean IsNeedElevated;
Expand Down
94 changes: 63 additions & 31 deletions src/Magpie.App/AppSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
#include "JsonHelper.h"
#include "ScalingMode.h"
#include "LocalizationService.h"
#include <ShellScalingApi.h>

#pragma comment(lib, "Shcore.lib")

using namespace ::Magpie::Core;

Expand Down Expand Up @@ -449,13 +452,18 @@ void AppSettings::_UpdateWindowPlacement() noexcept {
return;
}

_windowRect = {
wp.rcNormalPosition.left,
wp.rcNormalPosition.top,
wp.rcNormalPosition.right - wp.rcNormalPosition.left,
wp.rcNormalPosition.bottom - wp.rcNormalPosition.top
_mainWindowCenter = {
(wp.rcNormalPosition.left + wp.rcNormalPosition.right) / 2.0f,
(wp.rcNormalPosition.top + wp.rcNormalPosition.bottom) / 2.0f
};

const float dpiFactor = GetDpiForWindow(hwndMain) / float(USER_DEFAULT_SCREEN_DPI);
_mainWindowSizeInDips = {
(wp.rcNormalPosition.right - wp.rcNormalPosition.left) / dpiFactor,
(wp.rcNormalPosition.bottom - wp.rcNormalPosition.top) / dpiFactor,
};
_isWindowMaximized = wp.showCmd == SW_MAXIMIZE;

_isMainWindowMaximized = wp.showCmd == SW_MAXIMIZE;
}

bool AppSettings::_Save(const _AppSettingsData& data) noexcept {
Expand Down Expand Up @@ -484,16 +492,16 @@ bool AppSettings::_Save(const _AppSettingsData& data) noexcept {

writer.Key("windowPos");
writer.StartObject();
writer.Key("x");
writer.Int(data._windowRect.left);
writer.Key("y");
writer.Int(data._windowRect.top);
writer.Key("centerX");
writer.Double(data._mainWindowCenter.X);
writer.Key("centerY");
writer.Double(data._mainWindowCenter.Y);
writer.Key("width");
writer.Uint((uint32_t)data._windowRect.right);
writer.Double(data._mainWindowSizeInDips.Width);
writer.Key("height");
writer.Uint((uint32_t)data._windowRect.bottom);
writer.Double(data._mainWindowSizeInDips.Height);
writer.Key("maximized");
writer.Bool(data._isWindowMaximized);
writer.Bool(data._isMainWindowMaximized);
writer.EndObject();

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

auto windowPosNode = root.FindMember("windowPos");
if (windowPosNode != root.MemberEnd() && windowPosNode->value.IsObject()) {
const auto& windowRectObj = windowPosNode->value.GetObj();

int x = 0;
int y = 0;
if (JsonHelper::ReadInt(windowRectObj, "x", x, true)
&& JsonHelper::ReadInt(windowRectObj, "y", y, true)) {
_windowRect.left = x;
_windowRect.top = y;
}

uint32_t width = 0;
uint32_t height = 0;
if (JsonHelper::ReadUInt(windowRectObj, "width", width, true)
&& JsonHelper::ReadUInt(windowRectObj, "height", height, true)) {
_windowRect.right = (LONG)width;
_windowRect.bottom = (LONG)height;
const auto& windowPosObj = windowPosNode->value.GetObj();

Point center{};
Size size{};
if (JsonHelper::ReadFloat(windowPosObj, "centerX", center.X, true) &&
JsonHelper::ReadFloat(windowPosObj, "centerY", center.Y, true) &&
JsonHelper::ReadFloat(windowPosObj, "width", size.Width, true) &&
JsonHelper::ReadFloat(windowPosObj, "height", size.Height, true)) {
_mainWindowCenter = center;
_mainWindowSizeInDips = size;
} else {
// 尽最大努力和旧版本兼容
int x = 0;
int y = 0;
uint32_t width = 0;
uint32_t height = 0;
if (JsonHelper::ReadInt(windowPosObj, "x", x, true) &&
JsonHelper::ReadInt(windowPosObj, "y", y, true) &&
JsonHelper::ReadUInt(windowPosObj, "width", width, true) &&
JsonHelper::ReadUInt(windowPosObj, "height", height, true)) {
_mainWindowCenter = {
x + width / 2.0f,
y + height / 2.0f
};

// 如果窗口位置不存在屏幕则使用主屏幕的缩放,猜错的后果仅是窗口尺寸错误,
// 无论如何原始缩放信息已经丢失。
const HMONITOR hMon = MonitorFromPoint(
{ std::lroundf(_mainWindowCenter.X), std::lroundf(_mainWindowCenter.Y) },
MONITOR_DEFAULTTOPRIMARY
);

UINT dpi = USER_DEFAULT_SCREEN_DPI;
GetDpiForMonitor(hMon, MDT_EFFECTIVE_DPI, &dpi, &dpi);
const float dpiFactor = dpi / float(USER_DEFAULT_SCREEN_DPI);
_mainWindowSizeInDips = {
width / dpiFactor,
height / dpiFactor
};
}
}

JsonHelper::ReadBool(windowRectObj, "maximized", _isWindowMaximized);
JsonHelper::ReadBool(windowPosObj, "maximized", _isMainWindowMaximized);
}

auto shortcutsNode = root.FindMember("shortcuts");
Expand Down Expand Up @@ -742,7 +774,7 @@ bool AppSettings::_LoadProfile(
const rapidjson::GenericObject<true, rapidjson::Value>& profileObj,
Profile& profile,
bool isDefault
) {
) const {
if (!isDefault) {
if (!JsonHelper::ReadString(profileObj, "name", profile.name, true)) {
return false;
Expand Down
20 changes: 13 additions & 7 deletions src/Magpie.App/AppSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ struct _AppSettingsData {
// -1 表示使用系统设置
int _language = -1;

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

Theme _theme = Theme::System;
// 必须在 1~5 之间
Expand All @@ -59,7 +61,7 @@ struct _AppSettingsData {
bool _isInlineParams = false;
bool _isShowTrayIcon = true;
bool _isAutoRestore = false;
bool _isWindowMaximized = false;
bool _isMainWindowMaximized = false;
bool _isAutoCheckForUpdates = true;
bool _isCheckForPreviewUpdates = false;
};
Expand Down Expand Up @@ -115,12 +117,16 @@ class AppSettings : private _AppSettingsData {
_themeChangedEvent.remove(token);
}

const RECT& WindowRect() const noexcept {
return _windowRect;
Point MainWindowCenter() const noexcept {
return _mainWindowCenter;
}

Size MainWindowSizeInDips() const noexcept {
return _mainWindowSizeInDips;
}

bool IsWindowMaximized() const noexcept {
return _isWindowMaximized;
return _isMainWindowMaximized;
}

const Shortcut& GetShortcut(ShortcutAction action) const {
Expand Down Expand Up @@ -363,7 +369,7 @@ class AppSettings : private _AppSettingsData {
const rapidjson::GenericObject<true, rapidjson::Value>& profileObj,
Profile& profile,
bool isDefault = false
);
) const;
bool _SetDefaultShortcuts();
void _SetDefaultScalingModes();

Expand Down
125 changes: 106 additions & 19 deletions src/Magpie/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
#include "Win32Utils.h"
#include "ThemeHelper.h"
#include "XamlApp.h"
#include <ShellScalingApi.h>

#pragma comment(lib, "Shcore.lib")

namespace Magpie {

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

_CreateWindow(hInstance, windowRect);
const auto& [posToSet, sizeToSet] = _CreateWindow(hInstance, windowCenter, windowSizeInDips);

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

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

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

void MainWindow::_CreateWindow(HINSTANCE hInstance, const RECT& windowRect) noexcept {
// 防止窗口启动时不在可见区域,Windows 不会自动处理。
// 检查两个点的位置是否存在屏幕:窗口的中心点和上边框中心点。前者确保大部分窗口内容可见,后者确保大部分标题栏可见。
const POINT windowCenter{
(windowRect.left + windowRect.right) / 2,
(windowRect.top + windowRect.bottom) / 2
};
const bool isValidPosition = MonitorFromPoint(windowCenter, MONITOR_DEFAULTTONULL)
&& MonitorFromPoint({ windowCenter.x, windowRect.top }, MONITOR_DEFAULTTONULL);
std::pair<POINT, SIZE> MainWindow::_CreateWindow(HINSTANCE hInstance, winrt::Point windowCenter, winrt::Size windowSizeInDips) noexcept {
POINT windowPos = { CW_USEDEFAULT,CW_USEDEFAULT };
SIZE windowSize{};

// windowSizeInDips 小于零表示默认位置和尺寸
if (windowSizeInDips.Width > 0) {
// 检查窗口中心点的 DPI,根据我的测试,创建窗口时 Windows 使用窗口中心点确定 DPI。
// 如果窗口中心点不在任何屏幕上,则在默认位置启动,让调用者设置窗口尺寸。
const HMONITOR hMon = MonitorFromPoint(
{ std::lroundf(windowCenter.X),std::lroundf(windowCenter.Y) },
MONITOR_DEFAULTTONULL
);
if (hMon) {
UINT dpi = USER_DEFAULT_SCREEN_DPI;
GetDpiForMonitor(hMon, MDT_EFFECTIVE_DPI, &dpi, &dpi);

const float dpiFactor = dpi / float(USER_DEFAULT_SCREEN_DPI);
const winrt::Size windowSizeInPixels = {
windowSizeInDips.Width * dpiFactor,
windowSizeInDips.Height * dpiFactor
};

windowSize.cx = std::lroundf(windowSizeInPixels.Width);
windowSize.cy = std::lroundf(windowSizeInPixels.Height);

MONITORINFO mi{ sizeof(mi) };
GetMonitorInfo(hMon, &mi);

// 确保启动位置在屏幕工作区内。不允许启动时跨越多个屏幕。
if (windowSize.cx <= mi.rcWork.right - mi.rcWork.left && windowSize.cy <= mi.rcWork.bottom - mi.rcWork.top) {
windowPos.x = std::lroundf(windowCenter.X - windowSizeInPixels.Width / 2);
windowPos.x = std::clamp(windowPos.x, mi.rcWork.left, mi.rcWork.right - windowSize.cx);

windowPos.y = std::lroundf(windowCenter.Y - windowSizeInPixels.Height / 2);
windowPos.y = std::clamp(windowPos.y, mi.rcWork.top, mi.rcWork.bottom - windowSize.cy);
} else {
// 屏幕工作区无法容纳窗口则使用默认窗口尺寸
windowSize = {};
windowSizeInDips.Width = -1.0f;
}
}
}

// Win11 22H2 中为了使用 Mica 背景需指定 WS_EX_NOREDIRECTIONBITMAP
// windowSize 可能为零,并返回窗口尺寸给调用者
CreateWindowEx(
Win32Utils::GetOSVersion().Is22H2OrNewer() ? WS_EX_NOREDIRECTIONBITMAP : 0,
CommonSharedConstants::MAIN_WINDOW_CLASS_NAME,
L"Magpie",
WS_OVERLAPPEDWINDOW,
isValidPosition ? windowRect.left : CW_USEDEFAULT,
isValidPosition ? windowRect.top : CW_USEDEFAULT,
windowRect.right - windowRect.left,
windowRect.bottom - windowRect.top,
windowPos.x,
windowPos.y,
windowSize.cx,
windowSize.cy,
NULL,
NULL,
hInstance,
this
);

if (windowSize.cx == 0) {
const HMONITOR hMon = MonitorFromWindow(_hWnd, MONITOR_DEFAULTTONEAREST);

MONITORINFO mi{ sizeof(mi) };
GetMonitorInfo(hMon, &mi);

const float dpiFactor = _currentDpi / float(USER_DEFAULT_SCREEN_DPI);
const winrt::Size workingAreaSizeInDips = {
(mi.rcWork.right - mi.rcWork.left) / dpiFactor,
(mi.rcWork.bottom - mi.rcWork.top) / dpiFactor
};

// 确保启动尺寸小于屏幕工作区
if (windowSizeInDips.Width <= 0 ||
windowSizeInDips.Width > workingAreaSizeInDips.Width ||
windowSizeInDips.Height > workingAreaSizeInDips.Height) {
// 默认尺寸
static constexpr winrt::Size DEFAULT_SIZE{ 980.0f, 690.0f };

windowSizeInDips = DEFAULT_SIZE;

if (windowSizeInDips.Width > workingAreaSizeInDips.Width ||
windowSizeInDips.Height > workingAreaSizeInDips.Height) {
// 屏幕太小无法容纳默认尺寸
windowSizeInDips.Width = workingAreaSizeInDips.Width * 0.8f;
windowSizeInDips.Height = windowSizeInDips.Width * DEFAULT_SIZE.Height / DEFAULT_SIZE.Width;

if (windowSizeInDips.Height > workingAreaSizeInDips.Height) {
windowSizeInDips.Height = workingAreaSizeInDips.Height * 0.8f;
windowSizeInDips.Width = windowSizeInDips.Height * DEFAULT_SIZE.Width / DEFAULT_SIZE.Height;
}
}
}

windowSize.cx = std::lroundf(windowSizeInDips.Width * dpiFactor);
windowSize.cy = std::lroundf(windowSizeInDips.Height * dpiFactor);

// 确保启动位置在屏幕工作区内
RECT targetRect;
GetWindowRect(_hWnd, &targetRect);
windowPos.x = std::clamp(targetRect.left, mi.rcWork.left, mi.rcWork.right - windowSize.cx);
windowPos.y = std::clamp(targetRect.top, mi.rcWork.top, mi.rcWork.bottom - windowSize.cy);

return std::make_pair(windowPos, windowSize);
} else {
return {};
}
}

void MainWindow::_UpdateTheme() {
Expand Down
Loading

0 comments on commit e753069

Please sign in to comment.