WIN11 Snap 是什么?自定义 WINDOW 怎样使用 Snap 功能?
控件名:Snap
作 者:WPFDevelopersOrg – 驚鏵
原文链接[1]:https://github.com/WPFDevelopersOrg/WPFDevelopers
码云链接[2]:https://gitee.com/WPFDevelopersOrg/WPFDevelopers
框架支持.NET4 至 .NET8;Visual Studio 2022;
Issue:Window 控件在 Win11 下不支持 Snap 功能[3]
Snap 是什么?
Snap Layouts 是 Windows 11 的一项新功能,然而体系版本号必须 ≥ 21H2 。
Snap Layouts
Snap Layouts 提供 6 种不同的布局;双窗口排列 2 种三窗口排列 3 种四窗口排列 1 种提供了多种预设的窗口布局,可以将窗口快速调整到屏幕的左侧、右侧、上方、下方,甚至是四分其中一个屏幕区域;可以通过将窗口拖动到屏幕边缘来启动 Snap,或使用快捷键(Win + 左/右箭头、Win + 上/下箭头)来实现;在应用程序或窗口中按下「Win + Z」快捷键,或是将鼠标光标悬停在窗口最大化按钮上以显示 Snap Layout 菜单。分屏允许通过将屏幕的一半或四分其中一个,让多任务处理变得更加流畅,对于大屏幕或多显示器的用户尤其有用;怎样在 WPF 中自定义 Window 支持 SnapLayout 功能1. 修改 Window.cs重写 OnSourceInitialized,实现 Win32 消息钩子 HwndSourceHook;WM_NCHITTEST:用于检测鼠标在 Window 的哪个区域;WM_NCLBUTTONDOWN:鼠标左键按下,是否在深入了解栏按钮;处理 Snap Layout 消息 HandleSnapLayoutMessage;获取当前最大化还是还原按钮;通过 lParam 获取鼠标屏幕坐标,是否在最大化或者还原按钮上;接着返回 HTMAXBUTTON 通知体系鼠标现在在最大化按钮上;
using Microsoft.Windows.Shell;
using System;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using WPFDevelopers.Controls;
using WPFDevelopers.Core.Helpers;
using WPFDevelopers.Helpers;
namespaceWPFDevelopers.Net40
[TemplatePart(Name = TitleBarIcon, Type = typeof(Button))]
[TemplatePart(Name = HighTitleMaximizeButton, Type = typeof(Button))]
[TemplatePart(Name = HighTitleRestoreButton, Type = typeof(Button))]
[TemplatePart(Name = TitleBarMaximizeButton, Type = typeof(Button))]
[TemplatePart(Name = TitleBarRestoreButton, Type = typeof(Button))]
publicclassWindow : System.Windows.Window
privateconststring TitleBarIcon = “PART_TitleBarIcon”;
privateconststring HighTitleMaximizeButton = “PART_MaximizeButton”;
privateconststring HighTitleRestoreButton = “PART_RestoreButton”;
privateconststring TitleBarMaximizeButton = “PART_TitleBarMaximizeButton”;
privateconststring TitleBarRestoreButton = “PART_TitleBarRestoreButton”;
private WindowStyle _windowStyle;
private Button _titleBarIcon;
private Button _highTitleMaximizeButton;
private Button _highTitleRestoreButton;
private Button _titleBarMaximizeButton;
private Button _titleBarRestoreButton;
private IntPtr hWnd;
publicstaticreadonly DependencyProperty TitleHeightProperty =
DependencyProperty.Register(“TitleHeight”, typeof(double), typeof(Window), new PropertyMetadata(50d));
publicstaticreadonly DependencyProperty NoChromeProperty =
DependencyProperty.Register(“NoChrome”, typeof(bool), typeof(Window), new PropertyMetadata(false));
publicstaticreadonly DependencyProperty TitleBarProperty =
DependencyProperty.Register(“TitleBar”, typeof(object), typeof(Window), new PropertyMetadata());
publicstaticreadonly DependencyProperty TitleBackgroundProperty =
DependencyProperty.Register(“TitleBackground”, typeof(Brush), typeof(Window), new PropertyMetadata());
publicstaticreadonly DependencyProperty TitleBarModeProperty =
DependencyProperty.Register(“TitleBarMode”, typeof(TitleBarMode), typeof(Window), new PropertyMetadata(TitleBarMode.Normal));
static Window()
DefaultStyleKeyProperty.OverrideMetadata(typeof(Window), new FrameworkPropertyMetadata(typeof(Window)));
}
public Window()
WPFDevelopers.Resources.ThemeChanged += Resources_ThemeChanged;
CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, CloseWindow));
CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, MaximizeWindow,
CanResizeWindow));
CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, MinimizeWindow,
CanMinimizeWindow));
CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, RestoreWindow,
CanResizeWindow));
}
private void Resources_ThemeChanged(ThemeType currentTheme)
var isDark = currentTheme == ThemeType.Dark ? true : false;
var source = (HwndSource)PresentationSource.FromVisual(this);
Win32.EnableDarkModeForWindow(source, isDark);
}
public override void OnApplyTemplate()
base.OnApplyTemplate();
_windowStyle = WindowStyle;
_titleBarIcon = GetTemplateChild(TitleBarIcon) as Button;
if (_titleBarIcon != )
_titleBarIcon.MouseDoubleClick -= Icon_MouseDoubleClick;
_titleBarIcon.MouseDoubleClick += Icon_MouseDoubleClick;
}
_highTitleMaximizeButton = GetTemplateChild(HighTitleMaximizeButton) as Button;
_highTitleRestoreButton = GetTemplateChild(HighTitleRestoreButton) as Button;
_titleBarMaximizeButton = GetTemplateChild(TitleBarMaximizeButton) as Button;
_titleBarRestoreButton = GetTemplateChild(TitleBarRestoreButton) as Button;
}
private void Icon_MouseDoubleClick(object sender, MouseButtonEventArgs e)
if (e.ChangedButton == MouseButton.Left)
Close();
}
publicdouble TitleHeight
get => (double)GetValue(TitleHeightProperty);
set => SetValue(TitleHeightProperty, value);
}
publicbool NoChrome
get => (bool)GetValue(NoChromeProperty);
set => SetValue(NoChromeProperty, value);
}
publicobject TitleBar
get => (object)GetValue(TitleBarProperty);
set => SetValue(TitleBarProperty, value);
}
public Brush TitleBackground
get => (Brush)GetValue(TitleBackgroundProperty);
set => SetValue(TitleBackgroundProperty, value);
}
public TitleBarMode TitleBarMode
get => (TitleBarMode)GetValue(TitleBarModeProperty);
set => SetValue(TitleBarModeProperty, value);
}
protected override void OnSourceInitialized(EventArgs e)
base.OnSourceInitialized(e);
hWnd = new WindowInteropHelper(this).Handle;
HwndSource.FromHwnd(hWnd).AddHook(WindowProc);
if (TitleBarMode == TitleBarMode.Normal)
TitleHeight = SystemParameters2.Current.WindowNonClientFrameThickness.Top;
}
protected override void OnContentRendered(EventArgs e)
base.OnContentRendered(e);
if (SizeToContent == SizeToContent.WidthAndHeight)
InvalidateMeasure();
}
region Window Commands
private void CanResizeWindow(object sender, CanExecuteRoutedEventArgs e)
e.CanExecute = ResizeMode == ResizeMode.CanResize || ResizeMode == ResizeMode.CanResizeWithGrip;
}
private void CanMinimizeWindow(object sender, CanExecuteRoutedEventArgs e)
e.CanExecute = ResizeMode != ResizeMode.NoResize;
}
private void CloseWindow(object sender, ExecutedRoutedEventArgs e)
SystemCommands.CloseWindow(this);
}
private void MaximizeWindow(object sender, ExecutedRoutedEventArgs e)
if (WindowState == WindowState.Normal)
WindowStyle = WindowStyle.SingleBorderWindow;
WindowState = WindowState.Maximized;
WindowStyle = WindowStyle.;
}
}
private void MinimizeWindow(object sender, ExecutedRoutedEventArgs e)
Win32.SendMessage(hWnd, WindowsMessageCodes.WM_SYSCOMMAND, new IntPtr(WindowsMessageCodes.SC_MINIMIZE), IntPtr.Zero);
}
private void RestoreWindow(object sender, ExecutedRoutedEventArgs e)
SystemCommands.RestoreWindow(this);
}
private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
switch (msg)
case WindowsMessageCodes.WM_SYSCOMMAND:
if (wParam.ToInt32() == WindowsMessageCodes.SC_MINIMIZE)
_windowStyle = WindowStyle;
if (WindowStyle != WindowStyle.SingleBorderWindow)
WindowStyle = WindowStyle.SingleBorderWindow;
WindowState = WindowState.Minimized;
handled = true;
}
elseif (wParam.ToInt32() == WindowsMessageCodes.SC_RESTORE)
WindowState = WindowState.Normal;
WindowStyle = WindowStyle.;
if (WindowStyle. != _windowStyle)
WindowStyle = _windowStyle;
handled = true;
}
break;
case WindowsMessageCodes.WM_NCHITTEST:
case WindowsMessageCodes.WM_NCLBUTTONDOWN:
try
if (!OSVersionHelper.IsSnapLayoutSupported()
||
ResizeMode == ResizeMode.NoResize
||
ResizeMode == ResizeMode.CanMinimize)
break;
else
IntPtr result = IntPtr.Zero;
if (HandleSnapLayoutMessage(msg, lParam, ref result))
handled = true;
return result;
}
}
}
catch (OverflowException)
handled = true;
}
break;
}
return IntPtr.Zero;
}
private bool HandleSnapLayoutMessage(int msg, IntPtr lParam, ref IntPtr result)
Button button = TitleBarMode == TitleBarMode.Normal
? (WindowState != WindowState.Maximized ? _titleBarMaximizeButton : _titleBarRestoreButton)
: (WindowState != WindowState.Maximized ? _highTitleMaximizeButton : _highTitleRestoreButton);
if (button == || button.ActualWidth <= 0 || button.ActualHeight <= 0)
returnfalse;
var contentPresenter = button.Template.FindName(“PART_ContentPresenter”, button) as ContentPresenter;
var x = lParam.ToInt32() & 0xffff;
var y = lParam.ToInt32() >> 16;
var dpiX = OSVersionHelper.DeviceUnitsScalingFactorX;
var rect = new Rect(button.PointToScreen(new Point()), new Size(button.ActualWidth dpiX, button.ActualHeight dpiX));
var point = new Point(x, y);
if (msg == WindowsMessageCodes.WM_NCHITTEST && contentPresenter != )
if (!rect.Contains(point))
if(contentPresenter.Opacity != 0.7)
contentPresenter.Opacity = 0.7;
returnfalse;
}
contentPresenter.Opacity = 1;
result = new IntPtr(OSVersionHelper.HTMAXBUTTON);
}
elseif (msg == WindowsMessageCodes.WM_NCLBUTTONDOWN)
IInvokeProvider invokeProv = new ButtonAutomationPeer(button).GetPattern(PatternInterface.Invoke) as IInvokeProvider;
invokeProv?.Invoke();
}
returntrue;
}
private void ShowSystemMenu(object sender, ExecutedRoutedEventArgs e)
var element = e.OriginalSource as FrameworkElement;
if (element == )
return;
var point = WindowState == WindowState.Maximized
? new Point(0. element.ActualHeight)
: new Point(Left + BorderThickness.Left, element.ActualHeight + Top + BorderThickness.Top);
point = element.TransformToAncestor(this).Transform(point);
SystemCommands.ShowSystemMenu(this, point);
}
endregion
}
}
2. 修改 Window.xaml
<Style BasedOn=”x:}” TargetType=”x:Type wd:Window}”>
<Setter Property=”Foreground” Value=”DynamicResource WD.RegularTextBrush}” />
<Setter Property=”Background” Value=”DynamicResource WD.BackgroundBrush}” />
<Setter Property=”BorderBrush” Value=”DynamicResource WD.WindowBorderBrush}” />
<Setter Property=”TitleBackground” Value=”DynamicResource WD.WindowBorderBrush}” />
<Setter Property=”IsTabStop” Value=”False” />
<Setter Property=”BorderThickness” Value=”1″ />
<Setter Property=”SnapsToDevicePixels” Value=”True” />
<Setter Property=”UseLayoutRounding” Value=”True” />
<Setter Property=”TextOptions.TextFormattingMode” Value=”Ideal” />
<Setter Property=”WindowStyle” Value=”” />
<Setter Property=”FontFamily” Value=”DynamicResource WD.FontFamily}” />
<Setter Property=”shell:WindowChrome.WindowChrome”>
<Setter.Value>
<shell:WindowChrome CaptionHeight=”Binding TitleHeight, RelativeSource=RelativeSource AncestorType=wd:Window}}” GlassFrameThickness=”0.0.0..1″ />
</Setter.Value>
</Setter>
<Setter Property=”Template”>
<Setter.Value>
<ControlTemplate TargetType=”x:Type wd:Window}”>
<Border
Name=”PART_Border”
Background=”TemplateBinding Background}”
BorderBrush=”TemplateBinding BorderBrush}”
BorderThickness=”TemplateBinding BorderThickness}”
SnapsToDevicePixels=”True”>
<Grid x:Name=”LayoutRoot”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto” />
<RowDefinition Height=”” />
</Grid.RowDefinitions>
<control:SmallPanel
x:Name=”PART_Normal”
Grid.Row=”0″
Background=”TemplateBinding TitleBackground}”>
<Grid Height=”TemplateBinding TitleHeight}”>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”” />
<ColumnDefinition Width=”Auto” />
</Grid.ColumnDefinitions>
<StackPanel Orientation=”Horizontal”>
<Button
x:Name=”PART_TitleBarIcon”
Margin=”5,0.0.0″
VerticalAlignment=”Center”
shell:WindowChrome.IsHitTestVisibleInChrome=”True”
Background=”Transparent”
BorderThickness=”0″
Visibility=”Binding Icon, RelativeSource=RelativeSource Mode=TemplatedParent}, Converter=StaticResource ObjectToVisibilityConverter}}”>
<Image
Width=”x:Static SystemParameters.SmallIconWidth}”
Height=”x:Static SystemParameters.SmallIconHeight}”
IsHitTestVisible=”False”
RenderOptions.BitmapScalingMode=”HighQuality”
Source=”TemplateBinding Icon}” />
</Button>
<ContentControl
Margin=”5,0.0.0″
VerticalAlignment=”Center”
Content=”TemplateBinding Title}”
FontSize=”DynamicResource x:Static SystemFonts.CaptionFontSizeKey}}”
Foreground=”DynamicResource WD.WindowTextBrush}”
IsTabStop=”False” />
</StackPanel>
<StackPanel
Grid.Column=”1″
HorizontalAlignment=”Right”
VerticalAlignment=”Top”
shell:WindowChrome.IsHitTestVisibleInChrome=”True”
Orientation=”Horizontal”>
<StackPanel x:Name=”PART_TitleBarMinAndMax” Orientation=”Horizontal”>
<Button
x:Name=”PART_TitleBarMinimizeButton”
Padding=”0″
Command=”Binding Source=x:Static shell:SystemCommands.MinimizeWindowCommand}}”
IsTabStop=”False”
ToolTip=”Binding [Minimize], Source=x:Static resx:LanguageManager.Instance}}”>
<Grid HorizontalAlignment=”Center” VerticalAlignment=”Center”>
<Rectangle
Width=”10″
Height=”1″
Margin=”0.7,0.0″
VerticalAlignment=”Bottom”
Fill=”DynamicResource WD.WindowTextBrush}” />
</Grid>
</Button>
<Button
x:Name=”PART_TitleBarMaximizeButton”
Padding=”0″
Command=”Binding Source=x:Static shell:SystemCommands.MaximizeWindowCommand}}”
IsTabStop=”False”
ToolTip=”Binding [Maximize], Source=x:Static resx:LanguageManager.Instance}}”>
<Grid HorizontalAlignment=”Center” VerticalAlignment=”Center”>
<Path
Width=”10″
Height=”10″
HorizontalAlignment=”Center”
VerticalAlignment=”Center”
Data=”DynamicResource WD.WindowMaximizeGeometry}”
Fill=”DynamicResource WD.WindowTextBrush}”
Stretch=”Uniform”
UseLayoutRounding=”False” />
</Grid>
</Button>
<Button
x:Name=”PART_TitleBarRestoreButton”
Padding=”0″
Command=”Binding Source=x:Static shell:SystemCommands.RestoreWindowCommand}}”
IsTabStop=”False”
ToolTip=”Binding [Restore], Source=x:Static resx:LanguageManager.Instance}}”
Visibility=”Collapsed”>
<Grid HorizontalAlignment=”Center” VerticalAlignment=”Center”>
<Path
Width=”10″
Height=”10″
HorizontalAlignment=”Center”
VerticalAlignment=”Center”
Data=”DynamicResource WD.WindowRestoreGeometry}”
Fill=”DynamicResource WD.WindowTextBrush}”
Stretch=”Uniform”
UseLayoutRounding=”False” />
</Grid>
</Button>
</StackPanel>
<Button
Name=”PART_TitleBarCloseButton”
Command=”Binding Source=x:Static shell:SystemCommands.CloseWindowCommand}}”
IsTabStop=”False”
ToolTip=”Binding [Close], Source=x:Static resx:LanguageManager.Instance}}”>
<Path
Width=”10″
Height=”10″
HorizontalAlignment=”Center”
VerticalAlignment=”Center”
Data=”DynamicResource WD.WindowCloseGeometry}”
Fill=”DynamicResource WD.WindowTextBrush}”
Stretch=”Uniform” />
</Button>
</StackPanel>
</Grid>
</control:SmallPanel>
<control:SmallPanel
x:Name=”PART_HighTitleBar”
Grid.Row=”0″
Background=”TemplateBinding TitleBackground}”
Visibility=”Collapsed”>
<Grid
x:Name=”PART_GridChrome”
Height=”TemplateBinding TitleHeight}”
Margin=”10.0.0.0″>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”Auto” />
<ColumnDefinition Width=”” />
<ColumnDefinition Width=”Auto” MinWidth=”30″ />
</Grid.ColumnDefinitions>
<Image
Width=”23″
Height=”23″
VerticalAlignment=”Center”
RenderOptions.BitmapScalingMode=”HighQuality”
Source=”TemplateBinding Icon}”
Visibility=”TemplateBinding Icon,
Converter=StaticResource ObjectToVisibilityConverter}}” />
<TextBlock
x:Name=”PART_Title”
Grid.Column=”1″
Margin=”6,0″
VerticalAlignment=”Center”
FontSize=”DynamicResource WD.TitleFontSize}”
Foreground=”DynamicResource WD.WindowTextBrush}”
Text=”TemplateBinding Title}” />
<StackPanel
Grid.Column=”2″
Margin=”0.6″
shell:WindowChrome.IsHitTestVisibleInChrome=”True”
Orientation=”Horizontal”>
<StackPanel x:Name=”PART_MinAndMax” Orientation=”Horizontal”>
<Button
Name=”PART_MinimizeButton”
Padding=”0″
Command=”Binding Source=x:Static shell:SystemCommands.MinimizeWindowCommand}}”
IsTabStop=”False”
ToolTip=”Binding [Minimize], Source=x:Static resx:LanguageManager.Instance}}”>
<Grid HorizontalAlignment=”Center” VerticalAlignment=”Center”>
<Rectangle
x:Name=”MinimizeGlyph”
Width=”10″
Height=”1″
Margin=”0.7,0.0″
VerticalAlignment=”Bottom”
Fill=”DynamicResource WD.WindowTextBrush}” />
</Grid>
</Button>
<Button
x:Name=”PART_MaximizeButton”
Padding=”0″
Command=”Binding Source=x:Static shell:SystemCommands.MaximizeWindowCommand}}”
IsTabStop=”False”
ToolTip=”Binding [Maximize], Source=x:Static resx:LanguageManager.Instance}}”>
<Grid HorizontalAlignment=”Center” VerticalAlignment=”Center”>
<Path
Width=”10″
Height=”10″
HorizontalAlignment=”Center”
VerticalAlignment=”Center”
Data=”DynamicResource WD.WindowMaximizeGeometry}”
Fill=”DynamicResource WD.WindowTextBrush}”
Stretch=”Uniform”
UseLayoutRounding=”False” />
</Grid>
</Button>
<Button
x:Name=”PART_RestoreButton”
Padding=”0″
Command=”Binding Source=x:Static shell:SystemCommands.RestoreWindowCommand}}”
IsTabStop=”False”
ToolTip=”Binding [Restore], Source=x:Static resx:LanguageManager.Instance}}”
Visibility=”Collapsed”>
<Grid HorizontalAlignment=”Center” VerticalAlignment=”Center”>
<Path
Width=”10″
Height=”10″
HorizontalAlignment=”Center”
VerticalAlignment=”Center”
Data=”DynamicResource WD.WindowRestoreGeometry}”
Fill=”DynamicResource WD.WindowTextBrush}”
Stretch=”Uniform”
UseLayoutRounding=”False” />
&