diff --git a/img/Main window.png b/img/Main window.png
index 84b6b118e..7c4a997f8 100644
Binary files a/img/Main window.png and b/img/Main window.png differ
diff --git "a/img/\344\270\273\347\252\227\345\217\243.png" "b/img/\344\270\273\347\252\227\345\217\243.png"
index f97dcc2c0..a26c96306 100644
Binary files "a/img/\344\270\273\347\252\227\345\217\243.png" and "b/img/\344\270\273\347\252\227\345\217\243.png" differ
diff --git a/src/Magpie.App/App.idl b/src/Magpie.App/App.idl
index f5c2f28bc..0fc83cb99 100644
--- a/src/Magpie.App/App.idl
+++ b/src/Magpie.App/App.idl
@@ -26,6 +26,8 @@
#include "ScalingConfigurationPage.idl"
#include "ProfilePage.idl"
#include "SettingsPage.idl"
+#include "CaptionButtonsControl.idl"
+#include "TitleBarControl.idl"
namespace Magpie.App {
enum ShortcutAction {
diff --git a/src/Magpie.App/App.xaml b/src/Magpie.App/App.xaml
index 3138f8bc0..b8c040b81 100644
--- a/src/Magpie.App/App.xaml
+++ b/src/Magpie.App/App.xaml
@@ -17,6 +17,628 @@
+
+
12
+
+
+
+
+
+
+
diff --git a/src/Magpie.App/IconHelper.cpp b/src/Magpie.App/IconHelper.cpp
index 7700d782b..6292f91db 100644
--- a/src/Magpie.App/IconHelper.cpp
+++ b/src/Magpie.App/IconHelper.cpp
@@ -185,7 +185,6 @@ SoftwareBitmap IconHelper::ExtractIconFormWnd(HWND hWnd, uint32_t preferredSize,
}
SoftwareBitmap IconHelper::ExtractIconFromExe(const wchar_t* fileName, uint32_t preferredSize, uint32_t dpi) {
- preferredSize = (preferredSize + 15) / 16 * 16;
preferredSize = (uint32_t)std::lround(preferredSize * dpi / double(USER_DEFAULT_SCREEN_DPI));
{
diff --git a/src/Magpie.App/Magpie.App.vcxproj b/src/Magpie.App/Magpie.App.vcxproj
index b64cac855..aaa27625c 100644
--- a/src/Magpie.App/Magpie.App.vcxproj
+++ b/src/Magpie.App/Magpie.App.vcxproj
@@ -128,6 +128,10 @@
CandidateWindowItem.idl
Code
+
+ CaptionButtonsControl.xaml
+ Code
+
@@ -235,6 +239,10 @@
TextBlockHelper.idl
Code
+
+ TitleBarControl.xaml
+ Code
+
WrapPanel.idl
@@ -275,6 +283,10 @@
CandidateWindowItem.idl
Code
+
+ CaptionButtonsControl.xaml
+ Code
+
EffectParametersViewModel.idl
@@ -381,6 +393,10 @@
TextBlockHelper.idl
Code
+
+ TitleBarControl.xaml
+ Code
+
WrapPanel.idl
@@ -388,6 +404,14 @@
+
+ CaptionButtonsControl.xaml
+ Code
+
+
+ TitleBarControl.xaml
+ Code
+
Designer
@@ -500,6 +524,9 @@
Designer
+
+ Designer
+
Designer
@@ -533,7 +560,7 @@
Designer
-
+
Designer
diff --git a/src/Magpie.App/Magpie.App.vcxproj.filters b/src/Magpie.App/Magpie.App.vcxproj.filters
index 0bbbf1677..02d401c0a 100644
--- a/src/Magpie.App/Magpie.App.vcxproj.filters
+++ b/src/Magpie.App/Magpie.App.vcxproj.filters
@@ -224,9 +224,6 @@
Controls
-
- Styles
-
Controls
@@ -245,6 +242,12 @@
Styles
+
+ Controls
+
+
+ Controls
+
diff --git a/src/Magpie.App/MainPage.cpp b/src/Magpie.App/MainPage.cpp
index 5e30cf6fb..9664e9fd6 100644
--- a/src/Magpie.App/MainPage.cpp
+++ b/src/Magpie.App/MainPage.cpp
@@ -76,7 +76,7 @@ void MainPage::InitializeComponent() {
MUXC::BackdropMaterial::SetApplyToRootOrPageBackground(*this, true);
}
- IVector navMenuItems = __super::RootNavigationView().MenuItems();
+ IVector navMenuItems = RootNavigationView().MenuItems();
for (const Profile& profile : AppSettings::Get().Profiles()) {
MUXC::NavigationViewItem item;
item.Content(box_value(profile.name));
@@ -86,28 +86,14 @@ void MainPage::InitializeComponent() {
navMenuItems.InsertAt(navMenuItems.Size() - 1, item);
}
-
- // Win10 里启动时有一个 ToggleSwitch 的动画 bug,这里展示页面切换动画掩盖
- if (!osVersion.IsWin11()) {
- ContentFrame().Navigate(winrt::xaml_typename());
- }
}
void MainPage::Loaded(IInspectable const&, RoutedEventArgs const&) {
- MUXC::NavigationView nv = __super::RootNavigationView();
-
- if (nv.DisplayMode() == MUXC::NavigationViewDisplayMode::Minimal) {
- nv.IsPaneOpen(true);
- }
+ MUXC::NavigationView nv = RootNavigationView();
// 修复 WinUI 的汉堡菜单的尺寸 bug
nv.PaneDisplayMode(MUXC::NavigationViewPaneDisplayMode::Auto);
- // 消除焦点框
- IsTabStop(true);
- Focus(FocusState::Programmatic);
- IsTabStop(false);
-
// 设置 NavigationView 内的 Tooltip 的主题
XamlUtils::UpdateThemeOfTooltips(*this, ActualTheme());
}
@@ -119,7 +105,7 @@ void MainPage::NavigationView_SelectionChanged(
auto contentFrame = ContentFrame();
if (args.IsSettingsSelected()) {
- contentFrame.Navigate(winrt::xaml_typename());
+ contentFrame.Navigate(xaml_typename());
} else {
IInspectable selectedItem = args.SelectedItem();
if (!selectedItem) {
@@ -132,22 +118,22 @@ void MainPage::NavigationView_SelectionChanged(
hstring tagStr = unbox_value(tag);
Interop::TypeName typeName;
if (tagStr == L"Home") {
- typeName = winrt::xaml_typename();
+ typeName = xaml_typename();
} else if (tagStr == L"ScalingConfiguration") {
- typeName = winrt::xaml_typename();
+ typeName = xaml_typename();
} else if (tagStr == L"About") {
- typeName = winrt::xaml_typename();
+ typeName = xaml_typename();
} else {
- typeName = winrt::xaml_typename();
+ typeName = xaml_typename();
}
contentFrame.Navigate(typeName);
} else {
// 缩放配置页面
- MUXC::NavigationView nv = __super::RootNavigationView();
+ MUXC::NavigationView nv = RootNavigationView();
uint32_t index;
if (nv.MenuItems().IndexOf(nv.SelectedItem(), index)) {
- contentFrame.Navigate(winrt::xaml_typename(), box_value((int)index - 4));
+ contentFrame.Navigate(xaml_typename(), box_value((int)index - 4));
}
}
}
@@ -163,7 +149,7 @@ void MainPage::NavigationView_PaneOpening(MUXC::NavigationView const&, IInspecta
// UpdateThemeOfTooltips 中使用的 hack 会使 NavigationViewItem 在展开时不会自动删除 Tooltip
// 因此这里手动删除
- const MUXC::NavigationView& nv = __super::RootNavigationView();
+ const MUXC::NavigationView& nv = RootNavigationView();
for (const IInspectable& item : nv.MenuItems()) {
ToolTipService::SetToolTip(item.as(), nullptr);
}
@@ -176,7 +162,18 @@ void MainPage::NavigationView_PaneClosing(MUXC::NavigationView const&, MUXC::Nav
XamlUtils::UpdateThemeOfTooltips(*this, ActualTheme());
}
-void MainPage::NavigationView_DisplayModeChanged(MUXC::NavigationView const&, MUXC::NavigationViewDisplayModeChangedEventArgs const&) {
+void MainPage::NavigationView_DisplayModeChanged(MUXC::NavigationView const& nv, MUXC::NavigationViewDisplayModeChangedEventArgs const&) {
+ bool isExpanded = nv.DisplayMode() == MUXC::NavigationViewDisplayMode::Expanded;
+ nv.IsPaneToggleButtonVisible(!isExpanded);
+ if (isExpanded) {
+ nv.IsPaneOpen(true);
+ }
+
+ // HACK!
+ // 使导航栏的可滚动区域不会覆盖标题栏
+ FrameworkElement menuItemsScrollViewer = nv.GetTemplateChild(L"MenuItemsScrollViewer").as();
+ menuItemsScrollViewer.Margin({ 0,isExpanded ? TitleBar().ActualHeight() : 0.0,0,0});
+
XamlUtils::UpdateThemeOfTooltips(*this, ActualTheme());
}
@@ -188,8 +185,6 @@ fire_and_forget MainPage::NavigationView_ItemInvoked(MUXC::NavigationView const&
// 同步调用 ShowAt 有时会失败
co_await Dispatcher().TryRunAsync(CoreDispatcherPriority::Normal, [this]() {
- // 仅限 Win10:导航栏处于 Minimal 状态时会导致 Flyout 不在正确位置弹出
- // 有一个修复方法,但会导致性能损失
NewProfileFlyout().ShowAt(NewProfileNavigationViewItem());
});
}
@@ -354,7 +349,7 @@ void MainPage::_ProfileService_ProfileAdded(Profile& profile) {
item.Icon(FontIcon());
_LoadIcon(item, profile);
- IVector navMenuItems = __super::RootNavigationView().MenuItems();
+ IVector navMenuItems = RootNavigationView().MenuItems();
navMenuItems.InsertAt(navMenuItems.Size() - 1, item);
RootNavigationView().SelectedItem(item);
}
diff --git a/src/Magpie.App/MainPage.h b/src/Magpie.App/MainPage.h
index fc2b803d2..cc3c3ee53 100644
--- a/src/Magpie.App/MainPage.h
+++ b/src/Magpie.App/MainPage.h
@@ -22,7 +22,7 @@ struct MainPage : MainPageT {
void NavigationView_PaneClosing(MUXC::NavigationView const&, MUXC::NavigationViewPaneClosingEventArgs const&);
- void NavigationView_DisplayModeChanged(MUXC::NavigationView const&, MUXC::NavigationViewDisplayModeChangedEventArgs const&);
+ void NavigationView_DisplayModeChanged(MUXC::NavigationView const& nv, MUXC::NavigationViewDisplayModeChangedEventArgs const&);
fire_and_forget NavigationView_ItemInvoked(MUXC::NavigationView const&, MUXC::NavigationViewItemInvokedEventArgs const& args);
diff --git a/src/Magpie.App/MainPage.idl b/src/Magpie.App/MainPage.idl
index 2fa3f00f9..a252bc721 100644
--- a/src/Magpie.App/MainPage.idl
+++ b/src/Magpie.App/MainPage.idl
@@ -3,6 +3,8 @@ namespace Magpie.App {
MainPage();
Microsoft.UI.Xaml.Controls.NavigationView RootNavigationView { get; };
+ TitleBarControl TitleBar { get; };
+
NewProfileViewModel NewProfileViewModel { get; };
void NavigateToAboutPage();
diff --git a/src/Magpie.App/MainPage.xaml b/src/Magpie.App/MainPage.xaml
index 0f85f2a58..73d057332 100644
--- a/src/Magpie.App/MainPage.xaml
+++ b/src/Magpie.App/MainPage.xaml
@@ -7,151 +7,155 @@
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
Loaded="Loaded"
mc:Ignorable="d">
-
-
-
-
-
+
+
+
+
+ 0
+ 1,0,0,0
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ ItemsSource="{x:Bind NewProfileViewModel.CandidateWindows, Mode=OneWay}"
+ SelectedIndex="{x:Bind NewProfileViewModel.CandidateWindowIndex, Mode=TwoWay}">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
diff --git a/src/Magpie.App/PageFrame.cpp b/src/Magpie.App/PageFrame.cpp
index eb951318f..dcdaa50d0 100644
--- a/src/Magpie.App/PageFrame.cpp
+++ b/src/Magpie.App/PageFrame.cpp
@@ -4,7 +4,6 @@
#include "PageFrame.g.cpp"
#endif
#include "XamlUtils.h"
-#include "Win32Utils.h"
using namespace winrt;
using namespace Windows::UI::Xaml::Controls;
@@ -44,14 +43,6 @@ const DependencyProperty PageFrame::MainContentProperty = DependencyProperty::Re
void PageFrame::Loading(FrameworkElement const&, IInspectable const&) {
_Update();
-
- MainPage mainPage = XamlRoot().Content().as();
- _rootNavigationView = mainPage.RootNavigationView();
- _displayModeChangedRevoker = _rootNavigationView.DisplayModeChanged(
- auto_revoke,
- [&](auto const&, auto const&) { _UpdateHeaderStyle(); }
- );
- _UpdateHeaderStyle();
}
void PageFrame::Loaded(IInspectable const&, RoutedEventArgs const&) {
@@ -68,34 +59,15 @@ void PageFrame::ScrollViewer_ViewChanging(IInspectable const&, ScrollViewerViewC
}
void PageFrame::_Update() {
- TitleTextBlock().Visibility(Title().empty() ? Visibility::Collapsed : Visibility::Visible);
HeaderActionPresenter().Visibility(HeaderAction() ? Visibility::Visible : Visibility::Collapsed);
- if (_rootNavigationView) {
- _UpdateHeaderStyle();
- }
-}
-
-void PageFrame::_UpdateHeaderStyle() {
- TextBlock textBlock = TitleTextBlock();
-
IconElement icon = Icon();
if (icon) {
icon.Width(28);
icon.Height(28);
}
- if (_rootNavigationView.DisplayMode() == MUXC::NavigationViewDisplayMode::Minimal) {
- HeaderGrid().Margin({ 28, 8, 0, 0 });
- IconContainer().Visibility(Visibility::Collapsed);
- textBlock.FontSize(20);
- HeaderActionPresenter().Margin({ 0,-3,0,-3 });
- } else {
- HeaderGrid().Margin({ 0, Win32Utils::GetOSVersion().Is22H2OrNewer() ? 22.0 : 42.0, 0, 0});
- IconContainer().Visibility(icon ? Visibility::Visible : Visibility::Collapsed);
- textBlock.FontSize(30);
- HeaderActionPresenter().Margin({ 0,0,0,-4 });
- }
+ IconContainer().Visibility(icon ? Visibility::Visible : Visibility::Collapsed);
}
void PageFrame::_OnTitleChanged(DependencyObject const& sender, DependencyPropertyChangedEventArgs const&) {
diff --git a/src/Magpie.App/PageFrame.h b/src/Magpie.App/PageFrame.h
index ce5da42c6..c1c251b12 100644
--- a/src/Magpie.App/PageFrame.h
+++ b/src/Magpie.App/PageFrame.h
@@ -43,7 +43,7 @@ struct PageFrame : PageFrameT {
void ScrollViewer_PointerPressed(IInspectable const&, Input::PointerRoutedEventArgs const&);
void ScrollViewer_ViewChanging(IInspectable const&, Controls::ScrollViewerViewChangingEventArgs const&);
- event_token PropertyChanged(Data::PropertyChangedEventHandler const& value) {
+ event_token PropertyChanged(PropertyChangedEventHandler const& value) {
return _propertyChangedEvent.add(value);
}
@@ -64,12 +64,7 @@ struct PageFrame : PageFrameT {
void _Update();
- void _UpdateHeaderStyle();
-
- event _propertyChangedEvent;
-
- Microsoft::UI::Xaml::Controls::NavigationView _rootNavigationView{ nullptr };
- Microsoft::UI::Xaml::Controls::NavigationView::DisplayModeChanged_revoker _displayModeChangedRevoker{};
+ event _propertyChangedEvent;
};
}
diff --git a/src/Magpie.App/PageFrame.xaml b/src/Magpie.App/PageFrame.xaml
index 9ae08ae27..100204e9e 100644
--- a/src/Magpie.App/PageFrame.xaml
+++ b/src/Magpie.App/PageFrame.xaml
@@ -12,18 +12,16 @@
1000
-
+
-
+
+ Margin="0,0,40,16">
@@ -31,12 +29,12 @@
-
@@ -86,10 +84,10 @@
PointerPressed="ScrollViewer_PointerPressed"
VerticalScrollBarVisibility="Auto"
ViewChanging="ScrollViewer_ViewChanging">
-
+
{
void IsEnabledChanged(IInspectable const&, DependencyPropertyChangedEventArgs const&);
void Loading(FrameworkElement const&, IInspectable const&);
- event_token PropertyChanged(Data::PropertyChangedEventHandler const& value) {
+ event_token PropertyChanged(PropertyChangedEventHandler const& value) {
return _propertyChangedEvent.add(value);
}
@@ -72,7 +72,7 @@ struct SettingsCard : SettingsCardT {
void _SetEnabledState();
- event _propertyChangedEvent;
+ event _propertyChangedEvent;
};
}
diff --git a/src/Magpie.App/SettingsCard.idl b/src/Magpie.App/SettingsCard.idl
index ab0e5915e..1f2a17056 100644
--- a/src/Magpie.App/SettingsCard.idl
+++ b/src/Magpie.App/SettingsCard.idl
@@ -1,7 +1,5 @@
namespace Magpie.App {
[Windows.UI.Xaml.Markup.ContentProperty("RawTitle")]
- [Windows.UI.Xaml.TemplateVisualState("Normal", "CommonStates")]
- [Windows.UI.Xaml.TemplateVisualState("Disabled", "CommonStates")]
runtimeclass SettingsCard : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged
{
SettingsCard();
diff --git a/src/Magpie.App/SettingsGroup.h b/src/Magpie.App/SettingsGroup.h
index f81854642..e4ecd89bf 100644
--- a/src/Magpie.App/SettingsGroup.h
+++ b/src/Magpie.App/SettingsGroup.h
@@ -33,7 +33,7 @@ struct SettingsGroup : SettingsGroupT {
void IsEnabledChanged(IInspectable const&, DependencyPropertyChangedEventArgs const&);
void Loading(FrameworkElement const&, IInspectable const&);
- event_token PropertyChanged(Data::PropertyChangedEventHandler const& value) {
+ event_token PropertyChanged(PropertyChangedEventHandler const& value) {
return _propertyChangedEvent.add(value);
}
@@ -53,7 +53,7 @@ struct SettingsGroup : SettingsGroupT {
void _SetEnabledState();
- event _propertyChangedEvent;
+ event _propertyChangedEvent;
};
}
diff --git a/src/Magpie.App/ShortcutControl.idl b/src/Magpie.App/ShortcutControl.idl
index e65c1efa6..947dbcfd4 100644
--- a/src/Magpie.App/ShortcutControl.idl
+++ b/src/Magpie.App/ShortcutControl.idl
@@ -1,5 +1,5 @@
namespace Magpie.App {
- runtimeclass ShortcutControl : Windows.UI.Xaml.Controls.UserControl {
+ runtimeclass ShortcutControl : Windows.UI.Xaml.Controls.Grid {
ShortcutControl();
ShortcutAction Action;
diff --git a/src/Magpie.App/ShortcutControl.xaml b/src/Magpie.App/ShortcutControl.xaml
index b3f5d0433..0d4e6d2ac 100644
--- a/src/Magpie.App/ShortcutControl.xaml
+++ b/src/Magpie.App/ShortcutControl.xaml
@@ -1,66 +1,62 @@
-
-
-
-
-
+
+
+
diff --git a/src/Magpie.App/ShortcutDialog.idl b/src/Magpie.App/ShortcutDialog.idl
index bd94b6468..a4f25c1fa 100644
--- a/src/Magpie.App/ShortcutDialog.idl
+++ b/src/Magpie.App/ShortcutDialog.idl
@@ -1,5 +1,5 @@
namespace Magpie.App {
- runtimeclass ShortcutDialog : Windows.UI.Xaml.Controls.UserControl {
+ runtimeclass ShortcutDialog : Windows.UI.Xaml.Controls.Grid {
ShortcutDialog();
ShortcutError Error;
diff --git a/src/Magpie.App/ShortcutDialog.xaml b/src/Magpie.App/ShortcutDialog.xaml
index 9321b490b..be25efd8b 100644
--- a/src/Magpie.App/ShortcutDialog.xaml
+++ b/src/Magpie.App/ShortcutDialog.xaml
@@ -1,82 +1,82 @@
-
-
-
-
-
-
-
+
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/Magpie.App/TitlebarControl.cpp b/src/Magpie.App/TitlebarControl.cpp
new file mode 100644
index 000000000..dfdeb4e32
--- /dev/null
+++ b/src/Magpie.App/TitlebarControl.cpp
@@ -0,0 +1,41 @@
+#include "pch.h"
+#include "TitleBarControl.h"
+#if __has_include("TitleBarControl.g.cpp")
+#include "TitleBarControl.g.cpp"
+#endif
+#include "IconHelper.h"
+
+using namespace winrt;
+using namespace Windows::UI::Xaml::Media::Imaging;
+
+namespace winrt::Magpie::App::implementation {
+
+TitleBarControl::TitleBarControl() {
+ // 异步加载 Logo
+ [](TitleBarControl* that)->fire_and_forget {
+ wchar_t exePath[MAX_PATH];
+ GetModuleFileName(NULL, exePath, MAX_PATH);
+
+ auto weakThis = that->get_weak();
+
+ SoftwareBitmapSource bitmap;
+ co_await bitmap.SetBitmapAsync(IconHelper::ExtractIconFromExe(exePath, 40, USER_DEFAULT_SCREEN_DPI));
+
+ if (!weakThis.get()) {
+ co_return;
+ }
+
+ that->_logo = std::move(bitmap);
+ that->_propertyChangedEvent(*that, PropertyChangedEventArgs(L"Logo"));
+ }(this);
+}
+
+void TitleBarControl::Loading(FrameworkElement const&, IInspectable const&) {
+ MUXC::NavigationView rootNavigationView = Application::Current().as().MainPage().RootNavigationView();
+ rootNavigationView.DisplayModeChanged([this](const auto&, const auto& args) {
+ bool expanded = args.DisplayMode() == MUXC::NavigationViewDisplayMode::Expanded;
+ VisualStateManager::GoToState(*this, expanded ? L"Expanded" : L"Compact", true);
+ });
+}
+
+}
diff --git a/src/Magpie.App/TitlebarControl.h b/src/Magpie.App/TitlebarControl.h
new file mode 100644
index 000000000..8504a8f39
--- /dev/null
+++ b/src/Magpie.App/TitlebarControl.h
@@ -0,0 +1,33 @@
+#pragma once
+#include "TitleBarControl.g.h"
+
+namespace winrt::Magpie::App::implementation {
+struct TitleBarControl : TitleBarControlT {
+ TitleBarControl();
+
+ void Loading(FrameworkElement const&, IInspectable const&);
+
+ Imaging::SoftwareBitmapSource Logo() const noexcept {
+ return _logo;
+ }
+
+ event_token PropertyChanged(PropertyChangedEventHandler const& value) {
+ return _propertyChangedEvent.add(value);
+ }
+
+ void PropertyChanged(event_token const& token) {
+ _propertyChangedEvent.remove(token);
+ }
+
+private:
+ Imaging::SoftwareBitmapSource _logo{ nullptr };
+ event _propertyChangedEvent;
+};
+}
+
+namespace winrt::Magpie::App::factory_implementation {
+
+struct TitleBarControl : TitleBarControlT {
+};
+
+}
diff --git a/src/Magpie.App/TitlebarControl.idl b/src/Magpie.App/TitlebarControl.idl
new file mode 100644
index 000000000..5e0f00c73
--- /dev/null
+++ b/src/Magpie.App/TitlebarControl.idl
@@ -0,0 +1,8 @@
+namespace Magpie.App {
+ runtimeclass TitleBarControl : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged {
+ TitleBarControl();
+
+ Windows.UI.Xaml.Media.Imaging.SoftwareBitmapSource Logo { get; };
+ CaptionButtonsControl CaptionButtons { get; };
+ }
+}
diff --git a/src/Magpie.App/TitlebarControl.xaml b/src/Magpie.App/TitlebarControl.xaml
new file mode 100644
index 000000000..21e2e7c96
--- /dev/null
+++ b/src/Magpie.App/TitlebarControl.xaml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Magpie.App/ToggleSwitch.xaml b/src/Magpie.App/ToggleSwitch.xaml
deleted file mode 100644
index 6d765bbc9..000000000
--- a/src/Magpie.App/ToggleSwitch.xaml
+++ /dev/null
@@ -1,654 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/Magpie.Core/MagApp.cpp b/src/Magpie.Core/MagApp.cpp
index 696d6457b..6e3c4aa41 100644
--- a/src/Magpie.Core/MagApp.cpp
+++ b/src/Magpie.Core/MagApp.cpp
@@ -425,8 +425,10 @@ bool MagApp::_CreateHostWnd() {
}
}
+ // WS_EX_NOREDIRECTIONBITMAP 可以避免 WS_EX_LAYERED 导致的额外内存开销
_hwndHost = CreateWindowEx(
- (_options.IsDebugMode() ? 0 : WS_EX_TOPMOST) | WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW,
+ (_options.IsDebugMode() ? 0 : WS_EX_TOPMOST) | WS_EX_NOACTIVATE
+ | WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW,
HOST_WINDOW_CLASS_NAME,
NULL, // 标题为空,否则会被添加新配置页面列为候选窗口
WS_POPUP,
diff --git a/src/Magpie/MainWindow.cpp b/src/Magpie/MainWindow.cpp
index 851acc216..e52087c3a 100644
--- a/src/Magpie/MainWindow.cpp
+++ b/src/Magpie/MainWindow.cpp
@@ -8,15 +8,24 @@
namespace Magpie {
bool MainWindow::Create(HINSTANCE hInstance, const RECT& windowRect, bool isMaximized) noexcept {
- WNDCLASSEXW wcex{};
- wcex.cbSize = sizeof(wcex);
- wcex.lpfnWndProc = _WndProc;
- wcex.hInstance = hInstance;
- wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(CommonSharedConstants::IDI_APP));
- wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
- wcex.lpszClassName = CommonSharedConstants::MAIN_WINDOW_CLASS_NAME;
+ static const int _ = [](HINSTANCE hInstance) {
+ WNDCLASSEXW wcex{};
+ wcex.cbSize = sizeof(wcex);
+ wcex.lpfnWndProc = _WndProc;
+ wcex.hInstance = hInstance;
+ wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(CommonSharedConstants::IDI_APP));
+ wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ wcex.lpszClassName = CommonSharedConstants::MAIN_WINDOW_CLASS_NAME;
+ RegisterClassEx(&wcex);
- RegisterClassEx(&wcex);
+ wcex.style = CS_DBLCLKS;
+ wcex.lpfnWndProc = _TitleBarWndProc;
+ wcex.hIcon = NULL;
+ wcex.lpszClassName = CommonSharedConstants::TITLE_BAR_WINDOW_CLASS_NAME;
+ RegisterClassEx(&wcex);
+
+ return 0;
+ }(hInstance);
// Win11 22H2 中为了使用 Mica 背景需指定 WS_EX_NOREDIRECTIONBITMAP
CreateWindowEx(
@@ -25,8 +34,8 @@ bool MainWindow::Create(HINSTANCE hInstance, const RECT& windowRect, bool isMaxi
L"Magpie",
WS_OVERLAPPEDWINDOW,
windowRect.left, windowRect.top, windowRect.right, windowRect.bottom,
- nullptr,
- nullptr,
+ NULL,
+ NULL,
hInstance,
this
);
@@ -37,22 +46,87 @@ bool MainWindow::Create(HINSTANCE hInstance, const RECT& windowRect, bool isMaxi
_SetContent(winrt::Magpie::App::MainPage());
- // Xaml 控件加载完成后显示主窗口
- _content.Loaded([this, isMaximized](winrt::IInspectable const&, winrt::RoutedEventArgs const&) -> winrt::IAsyncAction {
- co_await _content.Dispatcher().RunAsync(winrt::CoreDispatcherPriority::Normal, [hWnd(_hWnd), isMaximized]() {
- // 防止窗口显示时背景闪烁
- // 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);
- ShowWindow(hWnd, isMaximized ? SW_SHOWMAXIMIZED : SW_SHOWNORMAL);
- Win32Utils::SetForegroundWindow(hWnd);
- });
- });
-
_content.ActualThemeChanged([this](winrt::FrameworkElement const&, winrt::IInspectable const&) {
_UpdateTheme();
});
_UpdateTheme();
+ // 窗口尚未显示无法最大化,所以我们设置 _isMaximized 使 XamlWindow 估计 XAML Islands 窗口尺寸。
+ // 否则在显示窗口时可能会看到 NavigationView 的导航栏的展开动画。
+ _isMaximized = isMaximized;
+
+ // 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);
+
+ // Xaml 控件加载完成后显示主窗口
+ _content.Loaded([this, isMaximized](winrt::IInspectable const&, winrt::RoutedEventArgs const&) {
+ if (isMaximized) {
+ // ShowWindow(_hWnd, SW_SHOWMAXIMIZED) 会显示错误的动画。因此我们以窗口化显示,
+ // 但位置和大小都和最大化相同,显示完毕后将状态设为最大化。
+ //
+ // 在此过程中,_isMaximized 始终是 true。
+
+ // 保存原始窗口化位置
+ WINDOWPLACEMENT wp{};
+ wp.length = sizeof(wp);
+ GetWindowPlacement(_hWnd, &wp);
+
+ // 查询最大化窗口位置
+ if (HMONITOR hMon = MonitorFromWindow(_hWnd, MONITOR_DEFAULTTONEAREST)) {
+ MONITORINFO mi{};
+ mi.cbSize = sizeof(mi);
+ GetMonitorInfo(hMon, &mi);
+
+ // 播放窗口显示动画
+ SetWindowPos(
+ _hWnd,
+ NULL,
+ mi.rcWork.left,
+ mi.rcWork.top,
+ mi.rcMonitor.right - mi.rcMonitor.left,
+ mi.rcMonitor.bottom - mi.rcMonitor.top,
+ SWP_NOACTIVATE | SWP_NOZORDER | SWP_SHOWWINDOW
+ );
+ }
+
+ // 将状态设为最大化,也还原了原始的窗口化位置
+ wp.showCmd = SW_SHOWMAXIMIZED;
+ SetWindowPlacement(_hWnd, &wp);
+ } else {
+ ShowWindow(_hWnd, SW_SHOWNORMAL);
+ }
+
+ Win32Utils::SetForegroundWindow(_hWnd);
+
+ _isWindowShown = true;
+ });
+
+ // 创建标题栏窗口,它是主窗口的子窗口。我们将它置于 XAML Islands 窗口之上以防止鼠标事件被吞掉
+ //
+ // 出于未知的原因,必须添加 WS_EX_LAYERED 样式才能发挥作用,见
+ // https://github.com/microsoft/terminal/blob/0ee2c74cd432eda153f3f3e77588164cde95044f/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp#L79
+ // WS_EX_NOREDIRECTIONBITMAP 可以避免 WS_EX_LAYERED 导致的额外内存开销
+ //
+ // WS_MINIMIZEBOX 和 WS_MAXIMIZEBOX 使得鼠标悬停时显示文字提示,Win11 的贴靠布局不依赖它们
+ CreateWindowEx(
+ WS_EX_LAYERED | WS_EX_NOPARENTNOTIFY | WS_EX_NOREDIRECTIONBITMAP | WS_EX_NOACTIVATE,
+ CommonSharedConstants::TITLE_BAR_WINDOW_CLASS_NAME,
+ L"",
+ WS_CHILD | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
+ 0, 0, 0, 0,
+ _hWnd,
+ nullptr,
+ hInstance,
+ this
+ );
+ SetLayeredWindowAttributes(_hwndTitleBar, 0, 255, LWA_ALPHA);
+
+ _content.TitleBar().SizeChanged([this](winrt::IInspectable const&, winrt::SizeChangedEventArgs const&) {
+ _ResizeTitleBarWindow();
+ });
+
return true;
}
@@ -66,16 +140,59 @@ void MainWindow::Show() const noexcept {
LRESULT MainWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) noexcept {
switch (msg) {
+ case WM_SIZE:
+ {
+ LRESULT ret = base_type::_MessageHandler(WM_SIZE, wParam, lParam);
+ _ResizeTitleBarWindow();
+ _content.TitleBar().CaptionButtons().IsWindowMaximized(_isMaximized);
+ return ret;
+ }
case WM_GETMINMAXINFO:
{
// 设置窗口最小尺寸
MINMAXINFO* mmi = (MINMAXINFO*)lParam;
- mmi->ptMinTrackSize = { 500,300 };
+ mmi->ptMinTrackSize = {
+ std::lround(550 * _currentDpi / double(USER_DEFAULT_SCREEN_DPI)),
+ std::lround(300 * _currentDpi / double(USER_DEFAULT_SCREEN_DPI))
+ };
return 0;
}
+ case WM_NCRBUTTONUP:
+ {
+ // 我们自己处理标题栏右键,不知为何 DefWindowProc 没有作用
+ if (wParam == HTCAPTION) {
+ HMENU systemMenu = GetSystemMenu(_hWnd, FALSE);
+
+ // 根据窗口状态更新选项
+ MENUITEMINFO mii{};
+ mii.cbSize = sizeof(MENUITEMINFO);
+ mii.fMask = MIIM_STATE;
+ mii.fType = MFT_STRING;
+ auto setState = [&](UINT item, bool enabled) {
+ mii.fState = enabled ? MF_ENABLED : MF_DISABLED;
+ SetMenuItemInfo(systemMenu, item, FALSE, &mii);
+ };
+ setState(SC_RESTORE, _isMaximized);
+ setState(SC_MOVE, !_isMaximized);
+ setState(SC_SIZE, !_isMaximized);
+ setState(SC_MINIMIZE, true);
+ setState(SC_MAXIMIZE, !_isMaximized);
+ setState(SC_CLOSE, true);
+ SetMenuDefaultItem(systemMenu, UINT_MAX, FALSE);
+
+ BOOL cmd = TrackPopupMenu(systemMenu, TPM_RETURNCMD,
+ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), 0, _hWnd, nullptr);
+ if (cmd != 0) {
+ PostMessage(_hWnd, WM_SYSCOMMAND, cmd, 0);
+ }
+ }
+ break;
+ }
case WM_DESTROY:
{
XamlApp::Get().SaveSettings();
+ _hwndTitleBar = NULL;
+ _trackingMouse = false;
break;
}
case CommonSharedConstants::WM_QUIT_MAGPIE:
@@ -93,27 +210,227 @@ LRESULT MainWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) noex
}
void MainWindow::_UpdateTheme() {
- const bool isDarkTheme = _content.ActualTheme() == winrt::ElementTheme::Dark;
-
- if (Win32Utils::GetOSVersion().Is22H2OrNewer()) {
- // 设置 Mica 背景
- DWM_SYSTEMBACKDROP_TYPE value = DWMSBT_MAINWINDOW;
- DwmSetWindowAttribute(_hWnd, DWMWA_SYSTEMBACKDROP_TYPE, &value, sizeof(value));
- } else {
- // 更改背景色以配合主题
- // 背景色在更改窗口大小时会短暂可见
- HBRUSH hbrOld = (HBRUSH)SetClassLongPtr(
- _hWnd,
- GCLP_HBRBACKGROUND,
- (INT_PTR)CreateSolidBrush(isDarkTheme ?
- CommonSharedConstants::DARK_TINT_COLOR : CommonSharedConstants::LIGHT_TINT_COLOR));
- if (hbrOld) {
- DeleteObject(hbrOld);
+ XamlWindowT::_SetTheme(_content.ActualTheme() == winrt::ElementTheme::Dark);
+}
+
+LRESULT MainWindow::_TitleBarWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) noexcept {
+ if (msg == WM_NCCREATE) {
+ MainWindow* that = (MainWindow*)(((CREATESTRUCT*)lParam)->lpCreateParams);
+ assert(that && !that->_hwndTitleBar);
+ that->_hwndTitleBar = hWnd;
+ SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)that);
+ } else if (MainWindow* that = (MainWindow*)GetWindowLongPtr(hWnd, GWLP_USERDATA)) {
+ return that->_TitleBarMessageHandler(msg, wParam, lParam);
+ }
+
+ return DefWindowProc(hWnd, msg, wParam, lParam);
+}
+
+LRESULT MainWindow::_TitleBarMessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) noexcept {
+ switch (msg) {
+ case WM_NCHITTEST:
+ {
+ POINT cursorPos{ GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam) };
+ ScreenToClient(_hwndTitleBar, &cursorPos);
+
+ RECT titleBarClientRect;
+ GetClientRect(_hwndTitleBar, &titleBarClientRect);
+ if (!PtInRect(&titleBarClientRect, cursorPos)) {
+ // 先检查鼠标是否在窗口内。在标题栏按钮上按下鼠标时我们会捕获光标,从而收到 WM_MOUSEMOVE 和 WM_LBUTTONUP 消息。
+ // 它们使用 WM_NCHITTEST 测试鼠标位于哪个区域
+ return HTNOWHERE;
}
- InvalidateRect(_hWnd, nullptr, TRUE);
+
+ if (!_isMaximized && cursorPos.y + (int)_GetTopBorderHeight() < _GetResizeHandleHeight()) {
+ // 鼠标位于上边框
+ return HTTOP;
+ }
+
+ static const winrt::Size buttonSizeInDips = [this]() {
+ return _content.TitleBar().CaptionButtons().CaptionButtonSize();
+ }();
+
+ const float buttonWidthInPixels = buttonSizeInDips.Width * _currentDpi / USER_DEFAULT_SCREEN_DPI;
+ const float buttonHeightInPixels = buttonSizeInDips.Height * _currentDpi / USER_DEFAULT_SCREEN_DPI;
+
+ if (cursorPos.y >= buttonHeightInPixels) {
+ // 鼠标位于标题按钮下方,如果标题栏很宽,这里也可以拖动
+ return HTCAPTION;
+ }
+
+ // 从右向左检查鼠标是否位于某个标题栏按钮上
+ const LONG cursorToRight = titleBarClientRect.right - cursorPos.x;
+ if (cursorToRight < buttonWidthInPixels) {
+ return HTCLOSE;
+ } else if (cursorToRight < buttonWidthInPixels * 2) {
+ // 支持 Win11 的贴靠布局
+ // FIXME: 最大化时贴靠布局的位置不对,目前没有找到解决方案。似乎只适配了系统原生框架和 UWP
+ return HTMAXBUTTON;
+ } else if (cursorToRight < buttonWidthInPixels * 3) {
+ return HTMINBUTTON;
+ } else {
+ // 不在任何标题栏按钮上则在可拖拽区域
+ return HTCAPTION;
+ }
+ }
+ // 在捕获光标时会收到
+ case WM_MOUSEMOVE:
+ {
+ POINT cursorPos{ GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam) };
+ ClientToScreen(_hwndTitleBar, &cursorPos);
+ wParam = SendMessage(_hwndTitleBar, WM_NCHITTEST, 0, MAKELPARAM(cursorPos.x, cursorPos.y));
+ }
+ [[fallthrough]];
+ case WM_NCMOUSEMOVE:
+ {
+ auto captionButtons = _content.TitleBar().CaptionButtons();
+
+ // 将 hover 状态通知 CaptionButtons。标题栏窗口拦截了 XAML Islands 中的标题栏
+ // 控件的鼠标消息,标题栏按钮的状态由我们手动控制。
+ switch (wParam) {
+ case HTTOP:
+ case HTCAPTION:
+ {
+ captionButtons.LeaveButtons();
+
+ // 将 HTTOP 传给主窗口才能通过上边框调整窗口高度
+ return SendMessage(_hWnd, msg, wParam, lParam);
+ }
+ case HTMINBUTTON:
+ case HTMAXBUTTON:
+ case HTCLOSE:
+ captionButtons.HoverButton((winrt::Magpie::App::CaptionButton)wParam);
+
+ // 追踪鼠标以确保鼠标离开标题栏时我们能收到 WM_NCMOUSELEAVE 消息,否则无法
+ // 可靠的收到这个消息,尤其是在用户快速移动鼠标的时候。
+ if (!_trackingMouse && msg == WM_NCMOUSEMOVE) {
+ TRACKMOUSEEVENT ev{};
+ ev.cbSize = sizeof(TRACKMOUSEEVENT);
+ ev.dwFlags = TME_LEAVE | TME_NONCLIENT;
+ ev.hwndTrack = _hwndTitleBar;
+ ev.dwHoverTime = HOVER_DEFAULT; // 不关心 HOVER 消息
+ TrackMouseEvent(&ev);
+ _trackingMouse = true;
+ }
+
+ break;
+ default:
+ captionButtons.LeaveButtons();
+ }
+ break;
+ }
+ case WM_NCMOUSELEAVE:
+ case WM_MOUSELEAVE:
+ {
+ // 我们需要检查鼠标是否**真的**离开了标题栏按钮,因为在某些情况下 OS 会错误汇报。
+ // 比如:鼠标在关闭按钮上停留了一段时间,系统会显示文字提示,这时按下左键,便会收
+ // 到 WM_NCMOUSELEAVE,但此时鼠标并没有离开标题栏按钮
+ POINT cursorPos;
+ GetCursorPos(&cursorPos);
+ // 先检查鼠标是否在主窗口上,如果正在显示文字提示,会返回 _hwndTitleBar
+ HWND hwndUnderCursor = WindowFromPoint(cursorPos);
+ if (hwndUnderCursor != _hWnd && hwndUnderCursor != _hwndTitleBar) {
+ _content.TitleBar().CaptionButtons().LeaveButtons();
+ } else {
+ // 然后检查鼠标在标题栏上的位置
+ LRESULT hit = SendMessage(_hwndTitleBar, WM_NCHITTEST, 0, MAKELPARAM(cursorPos.x, cursorPos.y));
+ if (hit != HTMINBUTTON && hit != HTMAXBUTTON && hit != HTCLOSE) {
+ _content.TitleBar().CaptionButtons().LeaveButtons();
+ }
+ }
+
+ _trackingMouse = false;
+ break;
+ }
+ case WM_NCLBUTTONDOWN:
+ case WM_NCLBUTTONDBLCLK:
+ {
+ // 手动处理标题栏上的点击。如果在标题栏按钮上,则通知 CaptionButtons,否则将消息传递
+ // 给主窗口。
+ switch (wParam) {
+ case HTTOP:
+ case HTCAPTION:
+ {
+ // 将 HTTOP 传给主窗口才能通过上边框调整窗口高度
+ return SendMessage(_hWnd, msg, wParam, lParam);
+ }
+ case HTMINBUTTON:
+ case HTMAXBUTTON:
+ case HTCLOSE:
+ _content.TitleBar().CaptionButtons().PressButton((winrt::Magpie::App::CaptionButton)wParam);
+ // 在标题栏按钮上按下左键后我们便捕获光标,这样才能在释放时得到通知。注意捕获光标后
+ // 便不会再收到 NC 族消息,这就是为什么我们要处理 WM_MOUSEMOVE 和 WM_LBUTTONUP
+ SetCapture(_hwndTitleBar);
+ break;
+ }
+ return 0;
+ }
+ // 在捕获光标时会收到
+ case WM_LBUTTONUP:
+ {
+ ReleaseCapture();
+
+ POINT cursorPos{ GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam) };
+ ClientToScreen(_hwndTitleBar, &cursorPos);
+ wParam = SendMessage(_hwndTitleBar, WM_NCHITTEST, 0, MAKELPARAM(cursorPos.x, cursorPos.y));
+ }
+ [[fallthrough]];
+ case WM_NCLBUTTONUP:
+ {
+ // 处理鼠标在标题栏上释放。如果位于标题栏按钮上,则传递给 CaptionButtons,不在则将消息传递给主窗口
+ switch (wParam) {
+ case HTTOP:
+ case HTCAPTION:
+ {
+ // 在可拖拽区域或上边框释放左键,将此消息传递给主窗口
+ _content.TitleBar().CaptionButtons().ReleaseButtons();
+ return SendMessage(_hWnd, msg, wParam, lParam);
+ }
+ case HTMINBUTTON:
+ case HTMAXBUTTON:
+ case HTCLOSE:
+ // 在标题栏按钮上释放左键
+ _content.TitleBar().CaptionButtons().ReleaseButton((winrt::Magpie::App::CaptionButton)wParam);
+ break;
+ default:
+ _content.TitleBar().CaptionButtons().ReleaseButtons();
+ }
+
+ return 0;
+ }
+ case WM_NCRBUTTONDOWN:
+ case WM_NCRBUTTONDBLCLK:
+ case WM_NCRBUTTONUP:
+ // 不关心右键,将它们传递给主窗口
+ return SendMessage(_hWnd, msg, wParam, lParam);
}
- ThemeHelper::SetWindowTheme(_hWnd, isDarkTheme);
+ return DefWindowProc(_hwndTitleBar, msg, wParam, lParam);
+}
+
+void MainWindow::_ResizeTitleBarWindow() noexcept {
+ if (!_hwndTitleBar) {
+ return;
+ }
+
+ auto titleBar = _content.TitleBar();
+
+ // 获取标题栏的边框矩形
+ winrt::Rect rect{0.0f, 0.0f, (float)titleBar.ActualWidth(), (float)titleBar.ActualHeight()};
+ rect = titleBar.TransformToVisual(_content).TransformBounds(rect);
+
+ const float dpiScale = _currentDpi / float(USER_DEFAULT_SCREEN_DPI);
+
+ // 将标题栏窗口置于 XAML Islands 窗口上方
+ SetWindowPos(
+ _hwndTitleBar,
+ HWND_TOP,
+ (int)std::floorf(rect.X * dpiScale),
+ (int)std::floorf(rect.Y * dpiScale) + _GetTopBorderHeight(),
+ (int)std::ceilf(rect.Width * dpiScale),
+ (int)std::floorf(rect.Height * dpiScale + 1), // 不知为何,直接向上取整有时无法遮盖 TitleBarControl
+ SWP_SHOWWINDOW
+ );
}
}
diff --git a/src/Magpie/MainWindow.h b/src/Magpie/MainWindow.h
index 81b96cd25..54dd10eba 100644
--- a/src/Magpie/MainWindow.h
+++ b/src/Magpie/MainWindow.h
@@ -17,7 +17,14 @@ class MainWindow : public XamlWindowT
private:
void _UpdateTheme();
- bool _isMainWndMaximized = false;
+ static LRESULT CALLBACK _TitleBarWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) noexcept;
+
+ LRESULT _TitleBarMessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) noexcept;
+
+ void _ResizeTitleBarWindow() noexcept;
+
+ HWND _hwndTitleBar = NULL;
+ bool _trackingMouse = false;
};
}
diff --git a/src/Magpie/ThemeHelper.cpp b/src/Magpie/ThemeHelper.cpp
index d09bd6818..0ba5eebc7 100644
--- a/src/Magpie/ThemeHelper.cpp
+++ b/src/Magpie/ThemeHelper.cpp
@@ -45,17 +45,17 @@ void ThemeHelper::Initialize() noexcept {
RefreshImmersiveColorPolicyState();
}
-void ThemeHelper::SetWindowTheme(HWND hWnd, bool isDark) noexcept {
+void ThemeHelper::SetWindowTheme(HWND hWnd, bool darkBorder, bool darkMenu) noexcept {
InitApis();
- SetPreferredAppMode(isDark ? PreferredAppMode::ForceDark : PreferredAppMode::ForceLight);
- AllowDarkModeForWindow(hWnd, isDark);
+ SetPreferredAppMode(darkMenu ? PreferredAppMode::ForceDark : PreferredAppMode::ForceLight);
+ AllowDarkModeForWindow(hWnd, darkMenu);
// 使标题栏适应黑暗模式
// build 18985 之前 DWMWA_USE_IMMERSIVE_DARK_MODE 的值不同
// https://github.com/MicrosoftDocs/sdk-api/pull/966/files
constexpr const DWORD DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19;
- BOOL value = isDark;
+ BOOL value = darkBorder;
DwmSetWindowAttribute(
hWnd,
Win32Utils::GetOSVersion().Is20H1OrNewer() ? DWMWA_USE_IMMERSIVE_DARK_MODE : DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1,
@@ -65,19 +65,6 @@ void ThemeHelper::SetWindowTheme(HWND hWnd, bool isDark) noexcept {
RefreshImmersiveColorPolicyState();
FlushMenuThemes();
-
- const Win32Utils::OSVersion& osVersion = Win32Utils::GetOSVersion();
- if (osVersion.Is22H2OrNewer()) {
- return;
- }
-
- LONG_PTR style = GetWindowLongPtr(hWnd, GWL_EXSTYLE);
- if (!osVersion.IsWin11()) {
- // 在 Win10 上需要更多 hack
- SetWindowLongPtr(hWnd, GWL_EXSTYLE, style | WS_EX_LAYERED);
- SetLayeredWindowAttributes(hWnd, 0, 254, LWA_ALPHA);
- }
- SetWindowLongPtr(hWnd, GWL_EXSTYLE, style);
}
}
diff --git a/src/Magpie/ThemeHelper.h b/src/Magpie/ThemeHelper.h
index 19ac75671..a31f5fc6e 100644
--- a/src/Magpie/ThemeHelper.h
+++ b/src/Magpie/ThemeHelper.h
@@ -5,7 +5,7 @@ namespace Magpie {
struct ThemeHelper {
// 应用程序启动时调用一次
static void Initialize() noexcept;
- static void SetWindowTheme(HWND hWnd, bool isDark) noexcept;
+ static void SetWindowTheme(HWND hWnd, bool darkBorder, bool darkMenu) noexcept;
};
}
diff --git a/src/Magpie/XamlWindow.h b/src/Magpie/XamlWindow.h
index d465a8fe4..604a2f330 100644
--- a/src/Magpie/XamlWindow.h
+++ b/src/Magpie/XamlWindow.h
@@ -2,6 +2,11 @@
#include
#include
#include "XamlUtils.h"
+#include "Win32Utils.h"
+#include "ThemeHelper.h"
+#include "CommonSharedConstants.h"
+
+#pragma comment(lib, "uxtheme.lib")
namespace Magpie {
@@ -92,13 +97,212 @@ class XamlWindowT {
sender.NavigateFocus(args.Request());
}
});
+ }
+
+ void _SetTheme(bool isDarkTheme) noexcept {
+ _isDarkTheme = isDarkTheme;
- // 防止第一次收到 WM_SIZE 消息时 MainPage 尺寸为 0
- _OnResize();
+ // Win10 中即使在亮色主题下我们也使用暗色边框,这也是 UWP 窗口的行为
+ ThemeHelper::SetWindowTheme(
+ _hWnd,
+ Win32Utils::GetOSVersion().IsWin11() ? isDarkTheme : true,
+ isDarkTheme
+ );
+
+ if (Win32Utils::GetOSVersion().Is22H2OrNewer()) {
+ // 设置 Mica 背景
+ DWM_SYSTEMBACKDROP_TYPE value = DWMSBT_MAINWINDOW;
+ DwmSetWindowAttribute(_hWnd, DWMWA_SYSTEMBACKDROP_TYPE, &value, sizeof(value));
+ return;
+ }
+
+ if (Win32Utils::GetOSVersion().IsWin11()) {
+ // Win11 21H1/21H2 对 Mica 的支持不完善,改为使用纯色背景。Win10 在 WM_PAINT 中
+ // 绘制背景。背景色在更改窗口大小时会短暂可见。
+ HBRUSH hbrOld = (HBRUSH)SetClassLongPtr(
+ _hWnd,
+ GCLP_HBRBACKGROUND,
+ (INT_PTR)CreateSolidBrush(isDarkTheme ?
+ CommonSharedConstants::DARK_TINT_COLOR : CommonSharedConstants::LIGHT_TINT_COLOR));
+ if (hbrOld) {
+ DeleteObject(hbrOld);
+ }
+ }
+
+ // 立即重新绘制
+ InvalidateRect(_hWnd, nullptr, FALSE);
+ UpdateWindow(_hWnd);
}
LRESULT _MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) noexcept {
switch (msg) {
+ case WM_CREATE:
+ {
+ _currentDpi = GetDpiForWindow(_hWnd);
+
+ _UpdateFrameMargins();
+
+ if (!Win32Utils::GetOSVersion().IsWin11()) {
+ // 初始化双缓冲绘图
+ static const int _ = []() {
+ BufferedPaintInit();
+ return 0;
+ }();
+ }
+
+ break;
+ }
+ case WM_NCCALCSIZE:
+ {
+ // 移除标题栏的逻辑基本来自 Windows Terminal
+ // https://github.com/microsoft/terminal/blob/0ee2c74cd432eda153f3f3e77588164cde95044f/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
+
+ if (!wParam) {
+ return 0;
+ }
+
+ NCCALCSIZE_PARAMS* params = (NCCALCSIZE_PARAMS*)lParam;
+ RECT& clientRect = params->rgrc[0];
+
+ // 保存原始上边框位置
+ const LONG originalTop = clientRect.top;
+
+ // 应用默认边框
+ LRESULT ret = DefWindowProc(_hWnd, WM_NCCALCSIZE, wParam, lParam);
+ if (ret != 0) {
+ return ret;
+ }
+
+ // 重新应用原始上边框,因此我们完全移除了默认边框中的上边框和标题栏,但保留了其他方向的边框
+ clientRect.top = originalTop;
+
+ // WM_NCCALCSIZE 在 WM_SIZE 前
+ _UpdateMaximizedState();
+
+ if (_isMaximized) {
+ // 最大化的窗口的实际尺寸比屏幕的工作区更大一点,这是为了将可调整窗口大小的区域隐藏在屏幕外面
+ clientRect.top += _GetResizeHandleHeight();
+
+ // 如果有自动隐藏的任务栏,我们在它的方向稍微减小客户区,这样用户就可以用鼠标呼出任务栏
+ if (HMONITOR hMon = MonitorFromWindow(_hWnd, MONITOR_DEFAULTTONEAREST)) {
+ MONITORINFO monInfo{};
+ monInfo.cbSize = sizeof(MONITORINFO);
+ GetMonitorInfo(hMon, &monInfo);
+
+ // 检查是否有自动隐藏的任务栏
+ APPBARDATA appBarData{};
+ appBarData.cbSize = sizeof(appBarData);
+ if (SHAppBarMessage(ABM_GETSTATE, &appBarData) & ABS_AUTOHIDE) {
+ // 检查显示器的一条边
+ auto hasAutohideTaskbar = [&monInfo](UINT edge) -> bool {
+ APPBARDATA data{};
+ data.cbSize = sizeof(data);
+ data.uEdge = edge;
+ data.rc = monInfo.rcMonitor;
+ HWND hTaskbar = (HWND)SHAppBarMessage(ABM_GETAUTOHIDEBAREX, &data);
+ return hTaskbar != nullptr;
+ };
+
+ static constexpr int AUTO_HIDE_TASKBAR_HEIGHT = 2;
+
+ if (hasAutohideTaskbar(ABE_TOP)) {
+ clientRect.top += AUTO_HIDE_TASKBAR_HEIGHT;
+ }
+ if (hasAutohideTaskbar(ABE_BOTTOM)) {
+ clientRect.bottom -= AUTO_HIDE_TASKBAR_HEIGHT;
+ }
+ if (hasAutohideTaskbar(ABE_LEFT)) {
+ clientRect.left += AUTO_HIDE_TASKBAR_HEIGHT;
+ }
+ if (hasAutohideTaskbar(ABE_RIGHT)) {
+ clientRect.right -= AUTO_HIDE_TASKBAR_HEIGHT;
+ }
+ }
+ }
+ }
+
+ return 0;
+ }
+ case WM_NCHITTEST:
+ {
+ // 让 OS 处理左右下三边,由于我们移除了标题栏,上边框会被视为客户区
+ LRESULT originalRet = DefWindowProc(_hWnd, WM_NCHITTEST, 0, lParam);
+ if (originalRet != HTCLIENT) {
+ return originalRet;
+ }
+
+ // XAML Islands 和它上面的标题栏窗口都会吞掉鼠标事件,因此能到达这里的唯一机会
+ // 是上边框。保险起见做一些额外检查。
+
+ if (!_isMaximized) {
+ RECT rcWindow;
+ GetWindowRect(_hWnd, &rcWindow);
+
+ if (GET_Y_LPARAM(lParam) < rcWindow.top + _GetResizeHandleHeight()) {
+ return HTTOP;
+ }
+ }
+
+ return HTCAPTION;
+ }
+ case WM_PAINT:
+ {
+ if (Win32Utils::GetOSVersion().IsWin11()) {
+ break;
+ }
+
+ PAINTSTRUCT ps{ 0 };
+ HDC hdc = BeginPaint(_hWnd, &ps);
+ if (!hdc) {
+ return 0;
+ }
+
+ const int topBorderHeight = (int)_GetTopBorderHeight();
+
+ // 在顶部绘制黑色实线以显示系统原始边框,见 _UpdateFrameMargins
+ if (ps.rcPaint.top < topBorderHeight) {
+ RECT rcTopBorder = ps.rcPaint;
+ rcTopBorder.bottom = topBorderHeight;
+
+ static HBRUSH hBrush = GetStockBrush(BLACK_BRUSH);
+ FillRect(hdc, &rcTopBorder, hBrush);
+ }
+
+ // 绘制客户区,它会在调整窗口尺寸时短暂可见
+ if (ps.rcPaint.bottom > topBorderHeight) {
+ RECT rcRest = ps.rcPaint;
+ rcRest.top = topBorderHeight;
+
+ static bool isDarkBrush = _isDarkTheme;
+ static HBRUSH backgroundBrush = CreateSolidBrush(isDarkBrush ?
+ CommonSharedConstants::DARK_TINT_COLOR : CommonSharedConstants::LIGHT_TINT_COLOR);
+
+ if (isDarkBrush != _isDarkTheme) {
+ isDarkBrush = _isDarkTheme;
+ DeleteBrush(backgroundBrush);
+ backgroundBrush = CreateSolidBrush(isDarkBrush ?
+ CommonSharedConstants::DARK_TINT_COLOR : CommonSharedConstants::LIGHT_TINT_COLOR);
+ }
+
+ if (isDarkBrush) {
+ // 这里我们想要黑色背景而不是原始边框
+ // hack 来自 https://github.com/microsoft/terminal/blob/0ee2c74cd432eda153f3f3e77588164cde95044f/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp#L1030-L1047
+ HDC opaqueDc;
+ BP_PAINTPARAMS params = { sizeof(params), BPPF_NOCLIP | BPPF_ERASE };
+ HPAINTBUFFER buf = BeginBufferedPaint(hdc, &rcRest, BPBF_TOPDOWNDIB, ¶ms, &opaqueDc);
+ if (buf && opaqueDc) {
+ FillRect(opaqueDc, &rcRest, backgroundBrush);
+ BufferedPaintSetAlpha(buf, nullptr, 255);
+ EndBufferedPaint(buf, TRUE);
+ }
+ } else {
+ FillRect(hdc, &rcRest, backgroundBrush);
+ }
+ }
+
+ EndPaint(_hWnd, &ps);
+ return 0;
+ }
case WM_SHOWWINDOW:
{
if (wParam == TRUE) {
@@ -123,6 +327,8 @@ class XamlWindowT {
}
case WM_DPICHANGED:
{
+ _currentDpi = HIWORD(wParam);
+
RECT* newRect = (RECT*)lParam;
SetWindowPos(_hWnd,
NULL,
@@ -172,8 +378,10 @@ class XamlWindowT {
}
case WM_SIZE:
{
+ _UpdateMaximizedState();
+
if (wParam != SIZE_MINIMIZED) {
- _OnResize();
+ _UpdateIslandPosition(LOWORD(lParam), HIWORD(lParam));
if (_hwndXamlIsland) {
// 使 ContentDialog 跟随窗口尺寸调整
@@ -192,6 +400,8 @@ class XamlWindowT {
}
}
+ _UpdateFrameMargins();
+
return 0;
}
case WM_DESTROY:
@@ -205,6 +415,10 @@ class XamlWindowT {
_xamlSource = nullptr;
_hwndXamlIsland = NULL;
+ _isMaximized = false;
+ _isWindowShown = false;
+ _isDarkTheme = false;
+
_content = nullptr;
_destroyedEvent();
@@ -216,14 +430,82 @@ class XamlWindowT {
return DefWindowProc(_hWnd, msg, wParam, lParam);
}
+ uint32_t _GetTopBorderHeight() const noexcept {
+ static constexpr uint32_t TOP_BORDER_HEIGHT = 1;
+
+ // Win11 或最大化时没有上边框
+ return (Win32Utils::GetOSVersion().IsWin11() || _isMaximized) ? 0 : TOP_BORDER_HEIGHT;
+ }
+
+ int _GetResizeHandleHeight() noexcept {
+ // 没有 SM_CYPADDEDBORDER
+ return GetSystemMetricsForDpi(SM_CXPADDEDBORDER, _currentDpi) +
+ GetSystemMetricsForDpi(SM_CYSIZEFRAME, _currentDpi);
+ }
+
HWND _hWnd = NULL;
C _content{ nullptr };
+ uint32_t _currentDpi = USER_DEFAULT_SCREEN_DPI;
+ bool _isMaximized = false;
+ bool _isWindowShown = false;
+ bool _isDarkTheme = false;
+
private:
- void _OnResize() noexcept {
- RECT clientRect;
- GetClientRect(_hWnd, &clientRect);
- SetWindowPos(_hwndXamlIsland, NULL, 0, 0, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top, SWP_SHOWWINDOW | SWP_NOACTIVATE);
+ void _UpdateIslandPosition(int width, int height) const noexcept {
+ if (!IsWindowVisible(_hWnd) && _isMaximized) {
+ // 初始化过程中此函数会被调用两次。如果窗口以最大化显示,则两次传入的尺寸不一致。第一次
+ // 调用此函数时主窗口尚未显示,因此无法最大化,我们必须估算最大化窗口的尺寸。不执行这个
+ // 操作可能导致窗口显示时展示 NavigationView 导航展开的动画。
+ if (HMONITOR hMon = MonitorFromWindow(_hWnd, MONITOR_DEFAULTTONEAREST)) {
+ MONITORINFO monInfo{};
+ monInfo.cbSize = sizeof(MONITORINFO);
+ GetMonitorInfo(hMon, &monInfo);
+
+ // 最大化窗口的尺寸为当前屏幕工作区的尺寸
+ width = monInfo.rcWork.right - monInfo.rcMonitor.left;
+ height = monInfo.rcWork.bottom - monInfo.rcMonitor.top;
+ }
+ }
+
+ int topBorderHeight = _GetTopBorderHeight();
+
+ // SWP_NOZORDER 确保 XAML Islands 窗口始终在标题栏窗口下方,否则主窗口在调整大小时会闪烁
+ SetWindowPos(
+ _hwndXamlIsland,
+ NULL,
+ 0,
+ topBorderHeight,
+ width,
+ height - topBorderHeight,
+ SWP_NOACTIVATE | SWP_NOZORDER | SWP_SHOWWINDOW
+ );
+ }
+
+ void _UpdateMaximizedState() noexcept {
+ // 如果窗口尚未显示,不碰 _isMaximized
+ if (_isWindowShown) {
+ _isMaximized = IsMaximized(_hWnd);
+ }
+ }
+
+ void _UpdateFrameMargins() const noexcept {
+ if (Win32Utils::GetOSVersion().IsWin11()) {
+ return;
+ }
+
+ MARGINS margins{};
+ if (_GetTopBorderHeight() > 0) {
+ // 在 Win10 中,移除标题栏时上边框也被没了。我们的解决方案是:使用 DwmExtendFrameIntoClientArea
+ // 将边框扩展到客户区,然后在顶部绘制了一个黑色实线来显示系统原始边框(这种情况下操作系统将黑色视
+ // 为透明)。因此我们有**完美**的上边框!
+ // 见 https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#extending-the-client-frame
+ //
+ // 有的软件自己绘制了假的上边框,如 Chromium 系、WinUI 3 等,但窗口失去焦点时边框是半透明的,无法
+ // 完美模拟。
+ margins.cxLeftWidth = -1;
+ }
+ DwmExtendFrameIntoClientArea(_hWnd, &margins);
}
winrt::event> _destroyedEvent;
diff --git a/src/Shared/CommonSharedConstants.h b/src/Shared/CommonSharedConstants.h
index 7b521f5ee..cc6696d10 100644
--- a/src/Shared/CommonSharedConstants.h
+++ b/src/Shared/CommonSharedConstants.h
@@ -2,6 +2,7 @@
struct CommonSharedConstants {
static constexpr const wchar_t* MAIN_WINDOW_CLASS_NAME = L"Magpie_Main";
+ static constexpr const wchar_t* TITLE_BAR_WINDOW_CLASS_NAME = L"Magpie_TitleBar";
static constexpr const wchar_t* NOTIFY_ICON_WINDOW_CLASS_NAME = L"Magpie_NotifyIcon";
static constexpr const wchar_t* HOTKEY_WINDOW_CLASS_NAME = L"Magpie_Hotkey";