自定义标题栏 (#624)

* feat: 去除标题栏

* chore: 添加注释

* feat: 保存最大化状态

* fix: 优化最大化状态

* feat: Win11 无需绘制上边框

* feat: 添加 TitlebarControl

* UI: 更改主界面样式

* fix: 修复一个隐蔽的bug

* feat: 添加 CaptionButtonsControl

* feat: 优化上边框颜色

* feat: 实现标题栏的 UI

* fix: 修复标题栏按钮不跟随主题的问题

* fix: 优化主窗口最小尺寸

* fix: 修复上边框绘制错误

* UI: 优化样式

* UI: 稍微优化标题按钮样式

* UI: 优化标题按钮样式

* UI: 优化标题按钮样式

* UI: 优化标题栏样式

* feat: 实现拖拽功能

* fix: 修复调整窗口大小时闪烁的问题

* fix: 更改上边框的实现方式

* feat: 实现上边框调整尺寸和支持 Win11 的贴靠布局

* feat: 实现标题栏按钮的 hover

* feat: 实现标题栏按钮的功能

* perf: 优化性能和添加注释

* fix: 修复一个小错误

* fix: 小修复

* fix: 优化最大化状态

* fix: 修复标题栏上右键菜单

* chore: 添加注释

* fix: 修复 Win10 中以最大化启动时一瞬间显示主题色背景的问题

* UI: 更新 ToggleSwitch 样式

* fix: 修复以最大化显示时的窗口动画

* fix: 修复 Win11 21H1/21H2 的背景

* chore: 优化注释

* feat: 在标题栏显示图标

* UI: 为标题栏添加动画

* fix: 修复导航栏菜单覆盖标题栏的问题

* fix: 修复标题按钮下方的可拖动区域

* feat: 导航栏不再支持 Minimal 状态

* chore: 删除不再需要的代码

* UI: 修正配置文件页面图标位置

* docs: 更新主窗口截图
This commit is contained in:
刘旭 2023-05-31 19:40:18 +08:00 committed by GitHub
commit 914c683f98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 2150 additions and 1082 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Before After
Before After

View file

@ -26,6 +26,8 @@
#include "ScalingConfigurationPage.idl"
#include "ProfilePage.idl"
#include "SettingsPage.idl"
#include "CaptionButtonsControl.idl"
#include "TitleBarControl.idl"
namespace Magpie.App {
enum ShortcutAction {

View file

@ -17,6 +17,628 @@
<Setter Property="Width" Value="40" />
</Style>
<Style BasedOn="{StaticResource DefaultToggleSwitchStyle}"
TargetType="ToggleSwitch">
<Style.Setters>
<Setter Property="MinWidth" Value="0" />
<Setter Property="Height" Value="36" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleSwitch">
<Grid VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource ToggleSwitchTopHeaderMargin}"
VerticalAlignment="Top"
x:DeferLoadStrategy="Lazy"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Foreground="{ThemeResource ToggleSwitchHeaderForeground}"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource ToggleSwitchPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource ToggleSwitchPostContentMargin}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="12"
MaxWidth="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
Margin="0,5"
Background="{ThemeResource ToggleSwitchContainerBackground}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{TemplateBinding CornerRadius}" />
<ContentPresenter x:Name="OffContentPresenter"
Grid.RowSpan="3"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding OffContent}"
ContentTemplate="{TemplateBinding OffContentTemplate}"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Opacity="0" />
<ContentPresenter x:Name="OnContentPresenter"
Grid.RowSpan="3"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding OnContent}"
ContentTemplate="{TemplateBinding OnContentTemplate}"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Opacity="0" />
<Rectangle x:Name="OuterBorder"
Grid.Row="1"
Grid.Column="2"
Width="40"
Height="20"
Fill="{ThemeResource ToggleSwitchFillOff}"
RadiusX="10"
RadiusY="10"
Stroke="{ThemeResource ToggleSwitchStrokeOff}"
StrokeThickness="{ThemeResource ToggleSwitchOuterBorderStrokeThickness}" />
<Rectangle x:Name="SwitchKnobBounds"
Grid.Row="1"
Grid.Column="2"
Width="40"
Height="20"
Fill="{ThemeResource ToggleSwitchFillOn}"
Opacity="0"
RadiusX="10"
RadiusY="10"
Stroke="{ThemeResource ToggleSwitchStrokeOn}"
StrokeThickness="{ThemeResource ToggleSwitchOnStrokeThickness}" />
<Grid x:Name="SwitchKnob"
Grid.Row="1"
Grid.Column="2"
Width="20"
Height="20"
HorizontalAlignment="Left">
<Border x:Name="SwitchKnobOn"
Width="12"
Height="12"
Margin="0,0,3,0"
HorizontalAlignment="Right"
Background="{ThemeResource ToggleSwitchKnobFillOn}"
BackgroundSizing="OuterBorderEdge"
BorderBrush="{ThemeResource ToggleSwitchKnobStrokeOn}"
CornerRadius="7"
Opacity="0"
RenderTransformOrigin="0.5, 0.5">
<Border.RenderTransform>
<CompositeTransform />
</Border.RenderTransform>
</Border>
<Rectangle x:Name="SwitchKnobOff"
Width="12"
Height="12"
Margin="3,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource ToggleSwitchKnobFillOff}"
RadiusX="7"
RadiusY="7"
RenderTransformOrigin="0.5, 0.5">
<Rectangle.RenderTransform>
<CompositeTransform />
</Rectangle.RenderTransform>
</Rectangle>
<Grid.RenderTransform>
<TranslateTransform x:Name="KnobTranslateTransform" />
</Grid.RenderTransform>
</Grid>
<Thumb x:Name="SwitchThumb"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
AutomationProperties.AccessibilityView="Raw">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Rectangle Fill="Transparent" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchContainerBackground}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchStrokeOffPointerOver}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchFillOffPointerOver}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOffPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchContainerBackgroundPointerOver}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="SwitchKnobOn.HorizontalAlignment" Value="Right" />
<Setter Target="SwitchKnobOn.Margin" Value="0,0,3,0" />
<Setter Target="SwitchKnobOff.HorizontalAlignment" Value="Left" />
<Setter Target="SwitchKnobOff.Margin" Value="3,0,0,0" />
</VisualState.Setters>
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchStrokeOffPressed}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchFillOffPressed}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOffPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchContainerBackgroundPressed}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="17" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="17" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchHeaderForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchStrokeOffDisabled}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchFillOffDisabled}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOffDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchContainerBackgroundDisabled}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ToggleStates">
<VisualStateGroup.Transitions>
<VisualTransition x:Name="DraggingToOnTransition"
GeneratedDuration="0"
From="Dragging"
To="On">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOnOffset}"
TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OnToDraggingTransition"
GeneratedDuration="0"
From="On"
To="Dragging">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="DraggingToOffTransition"
GeneratedDuration="0"
From="Dragging"
To="Off">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOffOffset}"
TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OnToOffTransition"
GeneratedDuration="0"
From="On"
To="Off">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOnToOffOffset}"
TargetName="SwitchKnob" />
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OffToOnTransition"
GeneratedDuration="0"
From="Off"
To="On">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOffToOnOffset}"
TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Dragging" />
<VisualState x:Name="Off" />
<VisualState x:Name="On">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="KnobTranslateTransform"
Storyboard.TargetProperty="X"
To="20"
Duration="0" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ContentStates">
<VisualState x:Name="OffContent">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="OnContent">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
<x:Double x:Key="SecondaryTextFontSize">12</x:Double>
<Style x:Key="SecondaryTextStyle"
TargetType="TextBlock">
@ -48,7 +670,7 @@
<x:Double x:Key="StandardIconSize">14</x:Double>
<!-- ComboBox -->
<x:Double x:Key="SettingBoxMinWidth">200</x:Double>
<x:Double x:Key="SettingBoxMinWidth">220</x:Double>
<Style x:Key="ComboBoxSettingStyle"
BasedOn="{StaticResource DefaultComboBoxStyle}"
TargetType="ComboBox">
@ -157,7 +779,6 @@
<ResourceDictionary.MergedDictionaries>
<muxc:XamlControlsResources ControlsResourcesVersion="Version2" />
<ResourceDictionary Source="ms-appx:///Button.xaml" />
<ResourceDictionary Source="ms-appx:///ToggleSwitch.xaml" />
<ResourceDictionary Source="ms-appx:///KeyVisual.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View file

@ -0,0 +1,134 @@
#include "pch.h"
#include "CaptionButtonsControl.h"
#if __has_include("CaptionButtonsControl.g.cpp")
#include "CaptionButtonsControl.g.cpp"
#endif
namespace winrt::Magpie::App::implementation {
Size CaptionButtonsControl::CaptionButtonSize() const noexcept {
ResourceDictionary resources = Resources();
return {
(float)unbox_value<double>(resources.Lookup(box_value(L"CaptionButtonWidth"))),
(float)unbox_value<double>(resources.Lookup(box_value(L"CaptionButtonHeight")))
};
}
// 鼠标移动到某个按钮上时调用
void CaptionButtonsControl::HoverButton(CaptionButton button) {
if (_pressedButton) {
bool hoveringOnPressedButton = _pressedButton.value() == button;
_allInNormal = !hoveringOnPressedButton;
VisualStateManager::GoToState(MinimizeButton(),
hoveringOnPressedButton && button == CaptionButton::Minimize ? L"Pressed" : L"Normal", false);
VisualStateManager::GoToState(MaximizeButton(),
hoveringOnPressedButton && button == CaptionButton::Maximize ? L"Pressed" : L"Normal", false);
VisualStateManager::GoToState(CloseButton(),
hoveringOnPressedButton && button == CaptionButton::Close ? L"Pressed" : L"Normal", false);
} else {
_allInNormal = false;
VisualStateManager::GoToState(MinimizeButton(),
button == CaptionButton::Minimize ? L"PointerOver" : L"Normal", false);
VisualStateManager::GoToState(MaximizeButton(),
button == CaptionButton::Maximize ? L"PointerOver" : L"Normal", false);
VisualStateManager::GoToState(CloseButton(),
button == CaptionButton::Close ? L"PointerOver" : L"Normal", false);
}
}
// 在某个按钮上按下鼠标时调用
void CaptionButtonsControl::PressButton(CaptionButton button) {
_allInNormal = false;
_pressedButton = button;
VisualStateManager::GoToState(MinimizeButton(),
button == CaptionButton::Minimize ? L"Pressed" : L"Normal", false);
VisualStateManager::GoToState(MaximizeButton(),
button == CaptionButton::Maximize ? L"Pressed" : L"Normal", false);
VisualStateManager::GoToState(CloseButton(),
button == CaptionButton::Close ? L"Pressed" : L"Normal", false);
}
// 在标题栏按钮上释放鼠标时调用
void CaptionButtonsControl::ReleaseButton(CaptionButton button) {
// 在某个按钮上按下然后释放视为点击,即使中途离开过也是如此,因为 HoverButton 和
// LeaveButtons 都不改变 _pressedButton
const bool clicked = _pressedButton && _pressedButton.value() == button;
if (clicked) {
// 用户点击了某个按钮
HWND hwndMain = (HWND)Application::Current().as<App>().HwndMain();
switch (_pressedButton.value()) {
case CaptionButton::Minimize:
PostMessage(hwndMain, WM_SYSCOMMAND, SC_MINIMIZE | HTMINBUTTON, 0);
break;
case CaptionButton::Maximize:
{
POINT cursorPos;
GetCursorPos(&cursorPos);
PostMessage(
hwndMain,
WM_SYSCOMMAND,
(_isWindowMaximized ? SC_RESTORE : SC_MAXIMIZE) | HTMAXBUTTON,
MAKELPARAM(cursorPos.x, cursorPos.y)
);
break;
}
case CaptionButton::Close:
PostMessage(hwndMain, WM_SYSCOMMAND, SC_CLOSE, 0);
break;
}
}
_pressedButton.reset();
// 如果点击了某个按钮则清空状态,因为此时窗口状态已改变。如果在某个按钮上按下然后在
// 其他按钮上释放,不视为点击,则将当前鼠标所在的按钮状态置为 PointerOver
_allInNormal = clicked;
VisualStateManager::GoToState(MinimizeButton(),
!clicked && button == CaptionButton::Minimize ? L"PointerOver" : L"Normal", false);
VisualStateManager::GoToState(MaximizeButton(),
!clicked && button == CaptionButton::Maximize ? L"PointerOver" : L"Normal", false);
VisualStateManager::GoToState(CloseButton(),
!clicked && button == CaptionButton::Close ? L"PointerOver" : L"Normal", false);
}
// 在非标题按钮上释放鼠标时调用
void CaptionButtonsControl::ReleaseButtons() {
if (!_pressedButton) {
return;
}
_pressedButton.reset();
LeaveButtons();
}
// 离开标题按钮时调用,不更改 _pressedButton
void CaptionButtonsControl::LeaveButtons() {
if (_allInNormal) {
return;
}
_allInNormal = true;
VisualStateManager::GoToState(MinimizeButton(), L"Normal", true);
VisualStateManager::GoToState(MaximizeButton(), L"Normal", true);
VisualStateManager::GoToState(CloseButton(), L"Normal", true);
}
void CaptionButtonsControl::IsWindowMaximized(bool value) {
if (_isWindowMaximized == value) {
return;
}
if (VisualStateManager::GoToState(MaximizeButton(),
value ? L"WindowStateMaximized" : L"WindowStateNormal", false))
{
_isWindowMaximized = value;
}
}
}

View file

@ -0,0 +1,37 @@
#pragma once
#include "CaptionButtonsControl.g.h"
namespace winrt::Magpie::App::implementation {
struct CaptionButtonsControl : CaptionButtonsControlT<CaptionButtonsControl> {
CaptionButtonsControl() {}
Size CaptionButtonSize() const noexcept;
void HoverButton(CaptionButton button);
void PressButton(CaptionButton button);
void ReleaseButton(CaptionButton button);
void ReleaseButtons();
void LeaveButtons();
void IsWindowMaximized(bool value);
private:
std::optional<CaptionButton> _pressedButton;
// 用于避免重复设置状态
bool _allInNormal = true;
bool _isWindowMaximized = false;
};
}
namespace winrt::Magpie::App::factory_implementation {
struct CaptionButtonsControl : CaptionButtonsControlT<CaptionButtonsControl, implementation::CaptionButtonsControl> {
};
}

View file

@ -0,0 +1,22 @@
namespace Magpie.App {
// 为简单起见,确保这些值与 WM_NCHITTEST 所使用的值相同
enum CaptionButton {
Minimize = 8, // HTMINBUTTON
Maximize = 9, // HTMAXBUTTON
Close = 20 // HTCLOSE
};
runtimeclass CaptionButtonsControl : Windows.UI.Xaml.Controls.StackPanel {
CaptionButtonsControl();
Windows.Foundation.Size CaptionButtonSize { get; };
void HoverButton(CaptionButton button);
void PressButton(CaptionButton button);
void ReleaseButton(CaptionButton button);
void ReleaseButtons();
void LeaveButtons();
void IsWindowMaximized(Boolean value);
}
}

View file

@ -0,0 +1,162 @@
<StackPanel x:Class="Magpie.App.CaptionButtonsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Magpie.App"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="Transparent"
Orientation="Horizontal"
mc:Ignorable="d">
<StackPanel.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<Color x:Key="CaptionButtonForegroundColor">Black</Color>
<StaticResource x:Key="CaptionButtonForeground"
ResourceKey="CaptionButtonForegroundColor" />
<StaticResource x:Key="CaptionButtonForegroundPointerOver"
ResourceKey="CaptionButtonForegroundColor" />
<SolidColorBrush x:Key="CaptionButtonForegroundPressed"
Opacity="0.7"
Color="{StaticResource CaptionButtonForegroundColor}" />
<SolidColorBrush x:Key="CaptionButtonBackgroundPointerOver"
Opacity="0.06"
Color="{StaticResource CaptionButtonForegroundColor}" />
<SolidColorBrush x:Key="CaptionButtonBackgroundPressed"
Opacity="0.04"
Color="{StaticResource CaptionButtonForegroundColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="CaptionButtonForegroundColor">White</Color>
<StaticResource x:Key="CaptionButtonForeground"
ResourceKey="CaptionButtonForegroundColor" />
<StaticResource x:Key="CaptionButtonForegroundPointerOver"
ResourceKey="CaptionButtonForegroundColor" />
<SolidColorBrush x:Key="CaptionButtonForegroundPressed"
Opacity="0.7"
Color="{StaticResource CaptionButtonForegroundColor}" />
<SolidColorBrush x:Key="CaptionButtonBackgroundPointerOver"
Opacity="0.06"
Color="{StaticResource CaptionButtonForegroundColor}" />
<SolidColorBrush x:Key="CaptionButtonBackgroundPressed"
Opacity="0.04"
Color="{StaticResource CaptionButtonForegroundColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Color x:Key="CaptionButtonBackground">Transparent</Color>
<x:Double x:Key="CaptionButtonWidth">46</x:Double>
<x:Double x:Key="CaptionButtonHeight">32</x:Double>
<!--
Initializes the string to the close button glyph.
Each specific button overrides it as needed.
-->
<x:String x:Key="CaptionButtonGlyph">&#xE8BB;</x:String>
<Style x:Key="CaptionButton"
TargetType="Button">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{StaticResource CaptionButtonBackground}" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Width" Value="{StaticResource CaptionButtonWidth}" />
<Setter Property="MinWidth" Value="{StaticResource CaptionButtonWidth}" />
<Setter Property="Height" Value="{StaticResource CaptionButtonHeight}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="ButtonBaseElement"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Viewbox Width="10"
Height="10">
<FontIcon x:Name="ButtonIcon"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Foreground="{ThemeResource CaptionButtonForeground}"
Glyph="{ThemeResource CaptionButtonGlyph}" />
</Viewbox>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Target="ButtonBaseElement.Background" Value="{StaticResource CaptionButtonBackground}" />
<Setter Target="ButtonIcon.Foreground" Value="{ThemeResource CaptionButtonForeground}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="ButtonBaseElement.Background" Value="{ThemeResource CaptionButtonBackgroundPointerOver}" />
<Setter Target="ButtonIcon.Foreground" Value="{ThemeResource CaptionButtonForegroundPointerOver}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="ButtonBaseElement.Background" Value="{ThemeResource CaptionButtonBackgroundPressed}" />
<Setter Target="ButtonIcon.Foreground" Value="{ThemeResource CaptionButtonForegroundPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled" />
</VisualStateGroup>
<VisualStateGroup x:Name="MinMaxStates">
<VisualState x:Name="WindowStateNormal" />
<VisualState x:Name="WindowStateMaximized">
<VisualState.Setters>
<Setter Target="ButtonIcon.Glyph" Value="&#xE923;" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</StackPanel.Resources>
<Button x:Name="MinimizeButton"
Style="{StaticResource CaptionButton}">
<Button.Resources>
<x:String x:Key="CaptionButtonGlyph">&#xE921;</x:String>
</Button.Resources>
</Button>
<Button x:Name="MaximizeButton"
Style="{StaticResource CaptionButton}">
<Button.Resources>
<x:String x:Key="CaptionButtonGlyph">&#xE922;</x:String>
</Button.Resources>
</Button>
<Button x:Name="CloseButton"
Style="{StaticResource CaptionButton}">
<Button.Resources>
<ResourceDictionary>
<x:String x:Key="CaptionButtonGlyph">&#xE8BB;</x:String>
<Color x:Key="CloseButtonColor">#C42B1C</Color>
<SolidColorBrush x:Key="CaptionButtonBackgroundPointerOver"
Color="{StaticResource CloseButtonColor}" />
<SolidColorBrush x:Key="CaptionButtonBackgroundPressed"
Opacity="0.9"
Color="{StaticResource CloseButtonColor}" />
<SolidColorBrush x:Key="CaptionButtonForegroundPointerOver"
Color="White" />
<SolidColorBrush x:Key="CaptionButtonForegroundPressed"
Opacity="0.7"
Color="White" />
</ResourceDictionary>
</Button.Resources>
</Button>
</StackPanel>

View file

@ -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));
{

View file

@ -128,6 +128,10 @@
<DependentUpon>CandidateWindowItem.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="CaptionButtonsControl.h">
<DependentUpon>CaptionButtonsControl.xaml</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="ComboBoxHelper.h" />
<ClInclude Include="ContentDialogHelper.h" />
<ClInclude Include="EffectHelper.h" />
@ -235,6 +239,10 @@
<DependentUpon>TextBlockHelper.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="TitleBarControl.h">
<DependentUpon>TitleBarControl.xaml</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="UpdateService.h" />
<ClInclude Include="WrapPanel.h">
<DependentUpon>WrapPanel.idl</DependentUpon>
@ -275,6 +283,10 @@
<DependentUpon>CandidateWindowItem.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="CaptionButtonsControl.cpp">
<DependentUpon>CaptionButtonsControl.xaml</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="ContentDialogHelper.cpp" />
<ClCompile Include="EffectParametersViewModel.cpp">
<DependentUpon>EffectParametersViewModel.idl</DependentUpon>
@ -381,6 +393,10 @@
<DependentUpon>TextBlockHelper.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="TitleBarControl.cpp">
<DependentUpon>TitleBarControl.xaml</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="UpdateService.cpp" />
<ClCompile Include="WrapPanel.cpp">
<DependentUpon>WrapPanel.idl</DependentUpon>
@ -388,6 +404,14 @@
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="CaptionButtonsControl.idl">
<DependentUpon>CaptionButtonsControl.xaml</DependentUpon>
<SubType>Code</SubType>
</None>
<None Include="TitleBarControl.idl">
<DependentUpon>TitleBarControl.xaml</DependentUpon>
<SubType>Code</SubType>
</None>
<None Include="KeyVisualState.idl">
<SubType>Designer</SubType>
</None>
@ -500,6 +524,9 @@
<Page Include="Button.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="CaptionButtonsControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="ProfilePage.xaml">
<SubType>Designer</SubType>
</Page>
@ -533,7 +560,7 @@
<Page Include="ShortcutDialog.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="ToggleSwitch.xaml">
<Page Include="TitleBarControl.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>

View file

@ -224,9 +224,6 @@
<Page Include="SettingsGroup.xaml">
<Filter>Controls</Filter>
</Page>
<Page Include="ToggleSwitch.xaml">
<Filter>Styles</Filter>
</Page>
<Page Include="KeyVisual.xaml">
<Filter>Controls</Filter>
</Page>
@ -245,6 +242,12 @@
<Page Include="Button.xaml">
<Filter>Styles</Filter>
</Page>
<Page Include="TitleBarControl.xaml">
<Filter>Controls</Filter>
</Page>
<Page Include="CaptionButtonsControl.xaml">
<Filter>Controls</Filter>
</Page>
</ItemGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">

View file

@ -76,7 +76,7 @@ void MainPage::InitializeComponent() {
MUXC::BackdropMaterial::SetApplyToRootOrPageBackground(*this, true);
}
IVector<IInspectable> navMenuItems = __super::RootNavigationView().MenuItems();
IVector<IInspectable> 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<Controls::Page>());
}
}
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<SettingsPage>());
contentFrame.Navigate(xaml_typename<SettingsPage>());
} else {
IInspectable selectedItem = args.SelectedItem();
if (!selectedItem) {
@ -132,22 +118,22 @@ void MainPage::NavigationView_SelectionChanged(
hstring tagStr = unbox_value<hstring>(tag);
Interop::TypeName typeName;
if (tagStr == L"Home") {
typeName = winrt::xaml_typename<HomePage>();
typeName = xaml_typename<HomePage>();
} else if (tagStr == L"ScalingConfiguration") {
typeName = winrt::xaml_typename<ScalingConfigurationPage>();
typeName = xaml_typename<ScalingConfigurationPage>();
} else if (tagStr == L"About") {
typeName = winrt::xaml_typename<AboutPage>();
typeName = xaml_typename<AboutPage>();
} else {
typeName = winrt::xaml_typename<HomePage>();
typeName = xaml_typename<HomePage>();
}
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<ProfilePage>(), box_value((int)index - 4));
contentFrame.Navigate(xaml_typename<ProfilePage>(), 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<DependencyObject>(), 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<FrameworkElement>();
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<IInspectable> navMenuItems = __super::RootNavigationView().MenuItems();
IVector<IInspectable> navMenuItems = RootNavigationView().MenuItems();
navMenuItems.InsertAt(navMenuItems.Size() - 1, item);
RootNavigationView().SelectedItem(item);
}

View file

@ -22,7 +22,7 @@ struct MainPage : MainPageT<MainPage> {
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);

View file

@ -3,6 +3,8 @@ namespace Magpie.App {
MainPage();
Microsoft.UI.Xaml.Controls.NavigationView RootNavigationView { get; };
TitleBarControl TitleBar { get; };
NewProfileViewModel NewProfileViewModel { get; };
void NavigateToAboutPage();

View file

@ -7,151 +7,155 @@
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
Loaded="Loaded"
mc:Ignorable="d">
<muxc:NavigationView Name="RootNavigationView"
DisplayModeChanged="NavigationView_DisplayModeChanged"
IsBackButtonVisible="Collapsed"
ItemInvoked="NavigationView_ItemInvoked"
PaneClosing="NavigationView_PaneClosing"
PaneOpening="NavigationView_PaneOpening"
SelectionChanged="NavigationView_SelectionChanged">
<muxc:NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground"
Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush"
Color="Transparent" />
</muxc:NavigationView.Resources>
<Grid>
<local:TitleBarControl x:Name="TitleBar"
Canvas.ZIndex="1" />
<muxc:NavigationView Name="RootNavigationView"
Canvas.ZIndex="0"
CompactModeThresholdWidth="0"
DisplayModeChanged="NavigationView_DisplayModeChanged"
IsBackButtonVisible="Collapsed"
ItemInvoked="NavigationView_ItemInvoked"
PaneClosing="NavigationView_PaneClosing"
PaneOpening="NavigationView_PaneOpening"
SelectionChanged="NavigationView_SelectionChanged">
<muxc:NavigationView.Resources>
<CornerRadius x:Key="NavigationViewContentGridCornerRadius">0</CornerRadius>
<Thickness x:Key="NavigationViewContentGridBorderThickness">1,0,0,0</Thickness>
</muxc:NavigationView.Resources>
<muxc:NavigationView.MenuItems>
<muxc:NavigationViewItem x:Uid="Main_Home"
IsSelected="True"
Tag="Home">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE80F;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Uid="Main_ScalingConfiguration"
Tag="ScalingConfiguration">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE740;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItemHeader x:Uid="Main_Profiles" />
<muxc:NavigationViewItem x:Uid="Main_Defaults">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE81E;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Name="NewProfileNavigationViewItem"
x:Uid="Main_NewProfile"
Icon="Add"
SelectsOnInvoked="False">
<FlyoutBase.AttachedFlyout>
<Flyout x:Name="NewProfileFlyout"
Placement="Right">
<Grid>
<ContentControl Grid.RowSpan="2"
Visibility="{x:Bind NewProfileViewModel.IsNoCandidateWindow, Mode=OneWay}"
<muxc:NavigationView.MenuItems>
<muxc:NavigationViewItem x:Uid="Main_Home"
IsSelected="True"
Tag="Home">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE80F;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Uid="Main_ScalingConfiguration"
Tag="ScalingConfiguration">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE740;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItemHeader x:Uid="Main_Profiles" />
<muxc:NavigationViewItem x:Uid="Main_Defaults">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE81E;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Name="NewProfileNavigationViewItem"
x:Uid="Main_NewProfile"
Icon="Add"
SelectsOnInvoked="False">
<FlyoutBase.AttachedFlyout>
<Flyout x:Name="NewProfileFlyout"
Placement="Right">
<Grid>
<ContentControl Grid.RowSpan="2"
MinWidth="240"
MinHeight="170"
Margin="10"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center">
<StackPanel Orientation="Vertical"
VerticalContentAlignment="Center"
Visibility="{x:Bind NewProfileViewModel.IsNoCandidateWindow, Mode=OneWay}">
<StackPanel Orientation="Vertical"
Spacing="20">
<FontIcon HorizontalAlignment="Center"
<FontIcon HorizontalAlignment="Center"
FontSize="40"
Glyph="&#xE8FC;" />
<TextBlock x:Uid="Main_NewProfileFlyout_NoCandidateWindow"
<TextBlock x:Uid="Main_NewProfileFlyout_NoCandidateWindow"
HorizontalAlignment="Center" />
</StackPanel>
</ContentControl>
<StackPanel Orientation="Vertical"
Visibility="{x:Bind NewProfileViewModel.IsAnyCandidateWindow, Mode=OneWay}">
<TextBlock x:Uid="Main_NewProfileFlyout_Title"
Margin="0,5,0,20"
FontSize="18"
FontWeight="SemiBold" />
<StackPanel Width="280"
Orientation="Vertical"
Spacing="15">
<ComboBox x:Name="CandidateWindowsComboBox"
x:Uid="Main_NewProfileFlyout_ComboBox"
Margin="0,0,0,10"
HorizontalAlignment="Stretch"
DropDownOpened="ComboBox_DropDownOpened"
ItemsSource="{x:Bind NewProfileViewModel.CandidateWindows, Mode=OneWay}"
SelectedIndex="{x:Bind NewProfileViewModel.CandidateWindowIndex, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="local:CandidateWindowItem">
<Grid MaxWidth="450"
HorizontalAlignment="Stretch"
ColumnSpacing="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Content="{x:Bind Icon, Mode=OneWay}" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
local:TextBlockHelper.IsAutoTooltip="True"
Text="{x:Bind Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<StackPanel Orientation="Vertical"
Spacing="8">
<TextBlock x:Uid="Main_NewProfileFlyout_Name" />
<TextBox Height="32"
HorizontalAlignment="Stretch"
KeyDown="NewProfileNameTextBox_KeyDown"
Text="{x:Bind NewProfileViewModel.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="8">
<TextBlock x:Uid="Main_NewProfileFlyout_CopyFrom" />
<ComboBox x:Name="ProfilesComboBox"
</ContentControl>
<StackPanel Orientation="Vertical"
Visibility="{x:Bind NewProfileViewModel.IsAnyCandidateWindow, Mode=OneWay}">
<TextBlock x:Uid="Main_NewProfileFlyout_Title"
Margin="0,5,0,20"
FontSize="18"
FontWeight="SemiBold" />
<StackPanel Width="280"
Orientation="Vertical"
Spacing="15">
<ComboBox x:Name="CandidateWindowsComboBox"
x:Uid="Main_NewProfileFlyout_ComboBox"
Margin="0,0,0,10"
HorizontalAlignment="Stretch"
DropDownOpened="ComboBox_DropDownOpened"
ItemsSource="{x:Bind NewProfileViewModel.Profiles, Mode=OneWay}"
SelectedIndex="{x:Bind NewProfileViewModel.ProfileIndex, Mode=TwoWay}">
ItemsSource="{x:Bind NewProfileViewModel.CandidateWindows, Mode=OneWay}"
SelectedIndex="{x:Bind NewProfileViewModel.CandidateWindowIndex, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock MaxWidth="300"
HorizontalAlignment="Stretch"
local:TextBlockHelper.IsAutoTooltip="True"
Text="{Binding}"
TextTrimming="CharacterEllipsis" />
<DataTemplate x:DataType="local:CandidateWindowItem">
<Grid MaxWidth="450"
HorizontalAlignment="Stretch"
ColumnSpacing="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Content="{x:Bind Icon, Mode=OneWay}" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
local:TextBlockHelper.IsAutoTooltip="True"
Text="{x:Bind Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<StackPanel Orientation="Vertical"
Spacing="8">
<TextBlock x:Uid="Main_NewProfileFlyout_Name" />
<TextBox Height="32"
HorizontalAlignment="Stretch"
KeyDown="NewProfileNameTextBox_KeyDown"
Text="{x:Bind NewProfileViewModel.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="8">
<TextBlock x:Uid="Main_NewProfileFlyout_CopyFrom" />
<ComboBox x:Name="ProfilesComboBox"
HorizontalAlignment="Stretch"
DropDownOpened="ComboBox_DropDownOpened"
ItemsSource="{x:Bind NewProfileViewModel.Profiles, Mode=OneWay}"
SelectedIndex="{x:Bind NewProfileViewModel.ProfileIndex, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock MaxWidth="300"
HorizontalAlignment="Stretch"
local:TextBlockHelper.IsAutoTooltip="True"
Text="{Binding}"
TextTrimming="CharacterEllipsis" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<Button x:Uid="Main_NewProfileFlyout_Create"
MinWidth="100"
Margin="0,15,0,0"
HorizontalAlignment="Right"
Click="NewProfileConfirmButton_Click"
IsEnabled="{x:Bind NewProfileViewModel.IsConfirmButtonEnabled, Mode=OneWay}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
<Button x:Uid="Main_NewProfileFlyout_Create"
MinWidth="100"
Margin="0,15,0,0"
HorizontalAlignment="Right"
Click="NewProfileConfirmButton_Click"
IsEnabled="{x:Bind NewProfileViewModel.IsConfirmButtonEnabled, Mode=OneWay}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</StackPanel>
</Grid>
</Flyout>
</FlyoutBase.AttachedFlyout>
</muxc:NavigationViewItem>
</muxc:NavigationView.MenuItems>
<muxc:NavigationView.FooterMenuItems>
<muxc:NavigationViewItem x:Uid="Main_About"
Tag="About">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE946;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
</muxc:NavigationView.FooterMenuItems>
</Grid>
</Flyout>
</FlyoutBase.AttachedFlyout>
</muxc:NavigationViewItem>
</muxc:NavigationView.MenuItems>
<muxc:NavigationView.FooterMenuItems>
<muxc:NavigationViewItem x:Uid="Main_About"
Tag="About">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE946;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
</muxc:NavigationView.FooterMenuItems>
<Frame x:Name="ContentFrame" />
</muxc:NavigationView>
<Frame x:Name="ContentFrame" />
</muxc:NavigationView>
</Grid>
</Page>

View file

@ -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<Magpie::App::MainPage>();
_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&) {

View file

@ -43,7 +43,7 @@ struct PageFrame : PageFrameT<PageFrame> {
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 @@ private:
void _Update();
void _UpdateHeaderStyle();
event<Data::PropertyChangedEventHandler> _propertyChangedEvent;
Microsoft::UI::Xaml::Controls::NavigationView _rootNavigationView{ nullptr };
Microsoft::UI::Xaml::Controls::NavigationView::DisplayModeChanged_revoker _displayModeChangedRevoker{};
event<PropertyChangedEventHandler> _propertyChangedEvent;
};
}

View file

@ -12,18 +12,16 @@
<x:Double x:Key="PageMaxWidth">1000</x:Double>
</UserControl.Resources>
<Grid Margin="20,0,0,0">
<Grid Margin="40,54,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel x:Name="HeaderGrid"
Grid.Row="0"
VerticalAlignment="Top"
ChildrenTransitions="{StaticResource SettingsCardsAnimations}">
<StackPanel Grid.Row="0"
VerticalAlignment="Top">
<Grid MaxWidth="{StaticResource PageMaxWidth}"
Margin="0,0,20,16">
Margin="0,0,40,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
@ -31,12 +29,12 @@
</Grid.ColumnDefinitions>
<ContentControl x:Name="IconContainer"
Grid.Column="0"
Margin="0,8,16,0"
Margin="0,6.5,16,0"
Content="{x:Bind Icon, Mode=OneWay}"
IsTabStop="False" />
<TextBlock x:Name="TitleTextBlock"
Grid.Column="1"
<TextBlock Grid.Column="1"
local:TextBlockHelper.IsAutoTooltip="True"
FontSize="28"
FontWeight="SemiBold"
Text="{x:Bind Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
@ -86,10 +84,10 @@
PointerPressed="ScrollViewer_PointerPressed"
VerticalScrollBarVisibility="Auto"
ViewChanging="ScrollViewer_ViewChanging">
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}">
<StackPanel>
<ContentControl Grid.Column="0"
MaxWidth="{StaticResource PageMaxWidth}"
Margin="0,0,20,20"
Margin="0,0,40,20"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Content="{x:Bind MainContent, Mode=OneWay}"

View file

@ -47,7 +47,7 @@ struct SettingsCard : SettingsCardT<SettingsCard> {
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 @@ private:
void _SetEnabledState();
event<Data::PropertyChangedEventHandler> _propertyChangedEvent;
event<PropertyChangedEventHandler> _propertyChangedEvent;
};
}

View file

@ -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();

View file

@ -33,7 +33,7 @@ struct SettingsGroup : SettingsGroupT<SettingsGroup> {
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 @@ private:
void _SetEnabledState();
event<Data::PropertyChangedEventHandler> _propertyChangedEvent;
event<PropertyChangedEventHandler> _propertyChangedEvent;
};
}

View file

@ -1,5 +1,5 @@
namespace Magpie.App {
runtimeclass ShortcutControl : Windows.UI.Xaml.Controls.UserControl {
runtimeclass ShortcutControl : Windows.UI.Xaml.Controls.Grid {
ShortcutControl();
ShortcutAction Action;

View file

@ -1,66 +1,62 @@
<!-- 移植自 https://github.com/microsoft/PowerToys/blob/35bfb0f83e5fc08cc04398e7aa98d77774412d3f/src/settings-ui/Settings.UI/Controls/ShortcutControl/ShortcutControl.xaml -->
<UserControl x:Class="Magpie.App.ShortcutControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Magpie.App"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
<Grid Margin="0,-6,0,-6"
HorizontalAlignment="Right">
<Button Padding="0"
Background="Transparent"
BorderBrush="Transparent"
Click="EditButton_Click">
<Button.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default" />
<ResourceDictionary x:Key="HighContrast" />
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="ButtonBackground"
ResourceKey="SubtleFillColorTransparentBrush" />
<StaticResource x:Key="ButtonBackgroundPointerOver"
ResourceKey="SubtleFillColorSecondaryBrush" />
<StaticResource x:Key="ButtonBackgroundPressed"
ResourceKey="SubtleFillColorTertiaryBrush" />
<StaticResource x:Key="ButtonBorderBrush"
ResourceKey="ControlFillColorTransparentBrush" />
<StaticResource x:Key="ButtonBorderBrushPointerOver"
ResourceKey="ControlFillColorTransparentBrush" />
<StaticResource x:Key="ButtonBorderBrushPressed"
ResourceKey="ControlFillColorTransparentBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
<StackPanel Margin="6,0,6,0"
Orientation="Horizontal"
Spacing="16">
<ItemsControl x:Name="KeysControl"
VerticalAlignment="Center"
IsTabStop="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:KeyVisualState">
<local:KeyVisual Key="{x:Bind Key, Mode=OneTime}"
VerticalAlignment="Center"
IsError="{x:Bind IsError, Mode=OneTime}"
IsTabStop="False"
VisualType="Small" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<FontIcon FontSize="16"
Glyph="&#xE70F;" />
</StackPanel>
</Button>
</Grid>
</UserControl>
<Grid x:Class="Magpie.App.ShortcutControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Magpie.App"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Button Margin="0,-6,0,-6"
Padding="0"
Background="Transparent"
BorderBrush="Transparent"
Click="EditButton_Click">
<Button.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default" />
<ResourceDictionary x:Key="HighContrast" />
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="ButtonBackground"
ResourceKey="SubtleFillColorTransparentBrush" />
<StaticResource x:Key="ButtonBackgroundPointerOver"
ResourceKey="SubtleFillColorSecondaryBrush" />
<StaticResource x:Key="ButtonBackgroundPressed"
ResourceKey="SubtleFillColorTertiaryBrush" />
<StaticResource x:Key="ButtonBorderBrush"
ResourceKey="ControlFillColorTransparentBrush" />
<StaticResource x:Key="ButtonBorderBrushPointerOver"
ResourceKey="ControlFillColorTransparentBrush" />
<StaticResource x:Key="ButtonBorderBrushPressed"
ResourceKey="ControlFillColorTransparentBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
<StackPanel Margin="6,0,6,0"
Orientation="Horizontal"
Spacing="16">
<ItemsControl x:Name="KeysControl"
VerticalAlignment="Center"
IsTabStop="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:KeyVisualState">
<local:KeyVisual Key="{x:Bind Key, Mode=OneTime}"
VerticalAlignment="Center"
IsError="{x:Bind IsError, Mode=OneTime}"
IsTabStop="False"
VisualType="Small" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<FontIcon FontSize="16"
Glyph="&#xE70F;" />
</StackPanel>
</Button>
</Grid>

View file

@ -1,5 +1,5 @@
namespace Magpie.App {
runtimeclass ShortcutDialog : Windows.UI.Xaml.Controls.UserControl {
runtimeclass ShortcutDialog : Windows.UI.Xaml.Controls.Grid {
ShortcutDialog();
ShortcutError Error;

View file

@ -1,82 +1,82 @@
<UserControl x:Class="Magpie.App.ShortcutDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Magpie.App"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
<Grid MinWidth="498"
MinHeight="220">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition MinHeight="110" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid x:Class="Magpie.App.ShortcutDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Magpie.App"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
MinWidth="498"
MinHeight="220"
mc:Ignorable="d">
<TextBlock x:Uid="ShortcutDialog_Description"
Grid.Row="0" />
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition MinHeight="110" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl x:Name="KeysControl"
Grid.Row="1"
Height="56"
Margin="0,64,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Top"
HorizontalContentAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:KeyVisualState">
<local:KeyVisual Key="{x:Bind Key, Mode=OneTime}"
Height="56"
IsError="{x:Bind IsError, Mode=OneTime}"
IsTabStop="False"
VisualType="Large" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock x:Uid="ShortcutDialog_Description"
Grid.Row="0" />
<StackPanel Grid.Row="2"
Margin="0,24,0,0"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="8">
<Grid Height="36">
<Border x:Name="WarningBanner"
Margin="-2,0,0,0"
Padding="8"
Background="{ThemeResource InfoBarErrorSeverityBackgroundBrush}"
BorderBrush="{ThemeResource InfoBarBorderBrush}"
BorderThickness="{ThemeResource InfoBarBorderThickness}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ItemsControl x:Name="KeysControl"
Grid.Row="1"
Height="56"
Margin="0,64,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Top"
HorizontalContentAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:KeyVisualState">
<local:KeyVisual Key="{x:Bind Key, Mode=OneTime}"
Height="56"
IsError="{x:Bind IsError, Mode=OneTime}"
IsTabStop="False"
VisualType="Large" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<muxc:InfoBadge Margin="2,0,12,0"
Style="{StaticResource CriticalIconInfoBadgeStyle}" />
<StackPanel Grid.Row="2"
Margin="0,24,0,0"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="8">
<Grid Height="36">
<Border x:Name="WarningBanner"
Margin="-2,0,0,0"
Padding="8"
Background="{ThemeResource InfoBarErrorSeverityBackgroundBrush}"
BorderBrush="{ThemeResource InfoBarBorderBrush}"
BorderThickness="{ThemeResource InfoBarBorderThickness}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="InvalidShortcutWarningLabel"
Grid.Column="1"
Margin="0,-1,0,0"
VerticalAlignment="Center"
FontWeight="{ThemeResource InfoBarTitleFontWeight}"
Foreground="{ThemeResource InfoBarTitleForeground}" />
</Grid>
</Border>
</Grid>
<TextBlock x:Uid="ShortcutDialog_Tip"
HorizontalAlignment="Left"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Grid>
</UserControl>
<muxc:InfoBadge Margin="2,0,12,0"
Style="{StaticResource CriticalIconInfoBadgeStyle}" />
<TextBlock x:Name="InvalidShortcutWarningLabel"
Grid.Column="1"
Margin="0,-1,0,0"
VerticalAlignment="Center"
FontWeight="{ThemeResource InfoBarTitleFontWeight}"
Foreground="{ThemeResource InfoBarTitleForeground}" />
</Grid>
</Border>
</Grid>
<TextBlock x:Uid="ShortcutDialog_Tip"
HorizontalAlignment="Left"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Grid>

View file

@ -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<App>().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);
});
}
}

View file

@ -0,0 +1,33 @@
#pragma once
#include "TitleBarControl.g.h"
namespace winrt::Magpie::App::implementation {
struct TitleBarControl : TitleBarControlT<TitleBarControl> {
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<PropertyChangedEventHandler> _propertyChangedEvent;
};
}
namespace winrt::Magpie::App::factory_implementation {
struct TitleBarControl : TitleBarControlT<TitleBarControl, implementation::TitleBarControl> {
};
}

View file

@ -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; };
}
}

View file

@ -0,0 +1,77 @@
<UserControl x:Class="Magpie.App.TitleBarControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Magpie.App"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="Root"
Height="40"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
mc:Ignorable="d">
<Grid Loading="Loading">
<StackPanel Margin="16,10,0,0"
Orientation="Horizontal"
Spacing="8">
<StackPanel.RenderTransform>
<TranslateTransform x:Name="TitleTranslation" />
</StackPanel.RenderTransform>
<Image Width="16"
Height="16"
VerticalAlignment="Center"
Source="{x:Bind Logo, Mode=OneWay}" />
<TextBlock Margin="0,0,0,2"
VerticalAlignment="Center"
FontSize="12"
Text="Magpie" />
</StackPanel>
<local:CaptionButtonsControl x:Name="CaptionButtons"
HorizontalAlignment="Right"
VerticalAlignment="Top" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStates">
<VisualStateGroup.Transitions>
<VisualTransition From="Expanded"
To="Compact">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="TitleTranslation"
Storyboard.TargetProperty="X"
From="0"
To="45"
Duration="0:0:0.22">
<DoubleAnimation.EasingFunction>
<ExponentialEase EasingMode="EaseOut"
Exponent="7" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</VisualTransition>
<VisualTransition From="Compact"
To="Expanded">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="TitleTranslation"
Storyboard.TargetProperty="X"
From="45"
To="0"
Duration="0:0:0.22">
<DoubleAnimation.EasingFunction>
<ExponentialEase EasingMode="EaseOut"
Exponent="7" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Expanded" />
<VisualState x:Name="Compact">
<VisualState.Setters>
<Setter Target="Root.Margin" Value="45,0,0,0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View file

@ -1,654 +0,0 @@
<!-- Copied from: https://github.com/microsoft/PowerToys/blob/35bfb0f83e5fc08cc04398e7aa98d77774412d3f/src/settings-ui/Settings.UI/Styles/Button.xaml#L96 -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- This style overrides the default style so that all ToggleSwitches are right aligned, with the label on the left -->
<Style TargetType="ToggleSwitch">
<Setter Property="Foreground" Value="{ThemeResource ToggleSwitchContentForeground}" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="System,TranslateX" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,-3,-7,-3" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleSwitch">
<Grid Margin="0,-4,0,-4"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource ToggleSwitchTopHeaderMargin}"
VerticalAlignment="Top"
x:DeferLoadStrategy="Lazy"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Foreground="{ThemeResource ToggleSwitchHeaderForeground}"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource ToggleSwitchPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource ToggleSwitchPostContentMargin}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="12"
MaxWidth="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
Margin="0,5"
Background="{ThemeResource ToggleSwitchContainerBackground}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{TemplateBinding CornerRadius}" />
<ContentPresenter x:Name="OffContentPresenter"
Grid.RowSpan="3"
Grid.Column="0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding OffContent}"
ContentTemplate="{TemplateBinding OffContentTemplate}"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Opacity="0" />
<ContentPresenter x:Name="OnContentPresenter"
Grid.RowSpan="3"
Grid.Column="0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding OnContent}"
ContentTemplate="{TemplateBinding OnContentTemplate}"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Opacity="0" />
<Rectangle x:Name="OuterBorder"
Grid.Row="1"
Grid.Column="2"
Width="40"
Height="20"
Fill="{ThemeResource ToggleSwitchFillOff}"
RadiusX="10"
RadiusY="10"
Stroke="{ThemeResource ToggleSwitchStrokeOff}"
StrokeThickness="{ThemeResource ToggleSwitchOuterBorderStrokeThickness}" />
<Rectangle x:Name="SwitchKnobBounds"
Grid.Row="1"
Grid.Column="2"
Width="40"
Height="20"
Fill="{ThemeResource ToggleSwitchFillOn}"
Opacity="0"
RadiusX="10"
RadiusY="10"
Stroke="{ThemeResource ToggleSwitchStrokeOn}"
StrokeThickness="{ThemeResource ToggleSwitchOnStrokeThickness}" />
<Grid x:Name="SwitchKnob"
Grid.Row="1"
Grid.Column="2"
Width="20"
Height="20"
HorizontalAlignment="Left">
<Border x:Name="SwitchKnobOn"
Grid.Column="2"
Width="12"
Height="12"
Margin="0,0,1,0"
HorizontalAlignment="Center"
Background="{ThemeResource ToggleSwitchKnobFillOn}"
BackgroundSizing="OuterBorderEdge"
BorderBrush="{ThemeResource ToggleSwitchKnobStrokeOn}"
CornerRadius="7"
Opacity="0"
RenderTransformOrigin="0.5, 0.5">
<Border.RenderTransform>
<CompositeTransform />
</Border.RenderTransform>
</Border>
<Rectangle x:Name="SwitchKnobOff"
Grid.Column="2"
Width="12"
Height="12"
Margin="-1,0,0,0"
HorizontalAlignment="Center"
Fill="{ThemeResource ToggleSwitchKnobFillOff}"
RadiusX="7"
RadiusY="7"
RenderTransformOrigin="0.5, 0.5">
<Rectangle.RenderTransform>
<CompositeTransform />
</Rectangle.RenderTransform>
</Rectangle>
<Grid.RenderTransform>
<TranslateTransform x:Name="KnobTranslateTransform" />
</Grid.RenderTransform>
</Grid>
<Thumb x:Name="SwitchThumb"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
AutomationProperties.AccessibilityView="Raw">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Rectangle Fill="Transparent" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchContainerBackground}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchStrokeOffPointerOver}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchFillOffPointerOver}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOffPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchContainerBackgroundPointerOver}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="SwitchKnobOn.HorizontalAlignment" Value="Right" />
<Setter Target="SwitchKnobOn.Margin" Value="0,0,3,0" />
<Setter Target="SwitchKnobOff.HorizontalAlignment" Value="Left" />
<Setter Target="SwitchKnobOff.Margin" Value="3,0,0,0" />
</VisualState.Setters>
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchStrokeOffPressed}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchFillOffPressed}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOffPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchContainerBackgroundPressed}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="17" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="17" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchHeaderForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchStrokeOffDisabled}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchFillOffDisabled}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchFillOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchStrokeOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOffDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleSwitchKnobFillOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="{ThemeResource ToggleSwitchContainerBackgroundDisabled}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ToggleStates">
<VisualStateGroup.Transitions>
<VisualTransition x:Name="DraggingToOnTransition"
GeneratedDuration="0"
From="Dragging"
To="On">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOnOffset}"
TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OnToDraggingTransition"
GeneratedDuration="0"
From="On"
To="Dragging">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="DraggingToOffTransition"
GeneratedDuration="0"
From="Dragging"
To="Off">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOffOffset}"
TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OnToOffTransition"
GeneratedDuration="0"
From="On"
To="Off">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOnToOffOffset}"
TargetName="SwitchKnob" />
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OffToOnTransition"
GeneratedDuration="0"
From="Off"
To="On">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOffToOnOffset}"
TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Dragging" />
<VisualState x:Name="Off" />
<VisualState x:Name="On">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="KnobTranslateTransform"
Storyboard.TargetProperty="X"
To="20"
Duration="0" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ContentStates">
<VisualState x:Name="OffContent">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="OnContent">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -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,

View file

@ -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;
XamlWindowT::_SetTheme(_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);
}
InvalidateRect(_hWnd, nullptr, TRUE);
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);
}
ThemeHelper::SetWindowTheme(_hWnd, isDarkTheme);
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;
}
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);
}
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
);
}
}

View file

@ -17,7 +17,14 @@ protected:
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;
};
}

View file

@ -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);
}
}

View file

@ -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;
};
}

View file

@ -2,6 +2,11 @@
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>
#include <CoreWindow.h>
#include "XamlUtils.h"
#include "Win32Utils.h"
#include "ThemeHelper.h"
#include "CommonSharedConstants.h"
#pragma comment(lib, "uxtheme.lib")
namespace Magpie {
@ -92,13 +97,212 @@ protected:
sender.NavigateFocus(args.Request());
}
});
}
// 防止第一次收到 WM_SIZE 消息时 MainPage 尺寸为 0
_OnResize();
void _SetTheme(bool isDarkTheme) noexcept {
_isDarkTheme = isDarkTheme;
// 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, &params, &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 @@ protected:
}
case WM_DPICHANGED:
{
_currentDpi = HIWORD(wParam);
RECT* newRect = (RECT*)lParam;
SetWindowPos(_hWnd,
NULL,
@ -172,8 +378,10 @@ protected:
}
case WM_SIZE:
{
_UpdateMaximizedState();
if (wParam != SIZE_MINIMIZED) {
_OnResize();
_UpdateIslandPosition(LOWORD(lParam), HIWORD(lParam));
if (_hwndXamlIsland) {
// 使 ContentDialog 跟随窗口尺寸调整
@ -192,6 +400,8 @@ protected:
}
}
_UpdateFrameMargins();
return 0;
}
case WM_DESTROY:
@ -205,6 +415,10 @@ protected:
_xamlSource = nullptr;
_hwndXamlIsland = NULL;
_isMaximized = false;
_isWindowShown = false;
_isDarkTheme = false;
_content = nullptr;
_destroyedEvent();
@ -216,14 +430,82 @@ protected:
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<winrt::delegate<>> _destroyedEvent;

View file

@ -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";