创建自定义 WPF 窗口
通常,当 WPF 开发人员需要编写自定义窗口时,他们会发现自己淹没在无数的文章、博客和 StackOverflow 讨论中,每一篇都展示了不同的方法来解决这个问题。
有人可能会争辩说,WPF 是一项遗留技术,没有有意义的未来。嗯……如果你看看当前桌面开发生态系统,并且你的目标是 Windows,那么选择并不多。当然,你可以使用 Java、Electron、老式的 win32 等。但是……如果你像我一样是 .NET 人,喜欢获得良好的性能和操作系统集成,WPF 是做到这一点的好方法。
现在,虽然 WPF 很棒,并提供了丰富的自定义选项,但有一方面它一直让许多许多开发人员感到非常头疼。
自定义窗口...
我肯定花了很多时间研究、试错、结合各种博客文章,并阅读了大量的 WinAPI 文档,才设法组合出一些接近于不用 Win32 主机来托管 WPF 应用程序的效果。
所以,废话不多说,我们开始吧。这会是一篇很长的文章……
初始设置
如果你正在阅读关于自定义 WPF 窗口的文章,你可能知道如何在 Visual Studio 中创建项目,所以我们跳过这一步。
总的来说,在我们开始之前,你需要有一个包含一个空“控件”库的解决方案,以及一个引用该库的 WPF 项目。
然后,让我们在“控件”库项目中创建新的 `Window` 类。
public partial class SWWindow : System.Windows.Window
{
}
另外,在“主题”文件夹中为我们的样式添加一个 `ResourceDictionary`。
之后,我们需要更改 WPF 项目中 `MainWindow` 的基类。
<sw:SWWindow x:Class="WPFCustomWIndow.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sw="clr-namespace:SourceWeave.Controls;assembly=SourceWeave.Controls"
xmlns:local="clr-namespace:WPFCustomWIndow"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
</sw:SWWindow>
public partial class MainWindow : SWWindow
在 `App.xaml` 中合并创建的“样式”字典,这样我们就可以开始进行“有趣”的部分了。
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/
SourceWeave.Controls;component/Themes/SWStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
创建我们的窗口“内容”
好的。到目前为止一切顺利。此时,启动应用程序应该会显示一个空的“正常”窗口。
我们的目标是移除默认的、无聊的标题栏和边框,并用我们自己的替换它们。
作为第一步,我们需要为新窗口创建一个自定义的 `ControlTemplate`。我们将它添加到设置步骤中创建的 `SWStyles.xaml` 资源字典中。
之后,我们需要为 `MainWindow` 创建一个 `Style`,并基于已创建的样式。为此,我们在 WPF 项目中创建一个资源字典,并在 `App.xaml` 文件中与第一个一起合并它。
SWStyles.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:local="clr-namespace:SourceWeave.Controls">
<Style TargetType="{x:Type Button}" x:Key="WindowButtonStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ButtonBase}">
<Border
x:Name="Chrome"
BorderBrush="{TemplateBinding BorderBrush}"
Margin="0"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="True">
<ContentPresenter
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
HorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment=
"{TemplateBinding VerticalContentAlignment}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="FontFamily" Value="Webdings"/>
<Setter Property="FontSize" Value="13.333" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Margin" Value="0,2,3,0"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="Gray" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="local:SWWindow" x:Key="SWWindowStyle">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="MinHeight" Value="320"/>
<Setter Property="MinWidth" Value="480"/>
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality"/>
<Setter Property="Title" Value="{Binding Title}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:SWWindow}">
<Grid Background="Transparent" x:Name="WindowRoot">
<Grid x:Name="LayoutRoot"
Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--TitleBar-->
<Grid x:Name="PART_HeaderBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{TemplateBinding Title}"
Grid.Column="0"
Grid.ColumnSpan="3"
TextTrimming="CharacterEllipsis"
HorizontalAlignment="Stretch"
FontSize="13"
TextAlignment="Center"
VerticalAlignment="Center"
Width="Auto"
Padding="200 0 200 0"
Foreground="Black"
Panel.ZIndex="0"
IsEnabled="{TemplateBinding IsActive}"/>
<Grid x:Name="WindowControlsGrid" Grid.Column="2"
Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="36"/>
<ColumnDefinition Width="36"/>
<ColumnDefinition Width="36"/>
</Grid.ColumnDefinitions>
<Button x:Name="MinimizeButton"
Style="{StaticResource WindowButtonStyle}"
fa:Awesome.Content="WindowMinimize"
TextElement.FontFamily="pack://application:,,,
/FontAwesome.WPF;component/#FontAwesome"
Grid.Column="0"/>
<Button x:Name="MaximizeButton" Style="{StaticResource
WindowButtonStyle}"
fa:Awesome.Content="WindowMaximize"
TextElement.FontFamily="pack://application:,,,
/FontAwesome.WPF;component/#FontAwesome"
Grid.Column="1"/>
<Button x:Name="RestoreButton"
Style="{StaticResource WindowButtonStyle}"
fa:Awesome.Content="WindowRestore"
Visibility="Collapsed"
TextElement.FontFamily=
"pack://application:,,,/FontAwesome.WPF;
component/#FontAwesome"
Grid.Column="1"/>
<Button x:Name="CloseButton"
Style="{StaticResource WindowButtonStyle}"
fa:Awesome.Content="Times"
TextElement.FontFamily="pack://application:,,,
/FontAwesome.WPF;component/#FontAwesome"
TextElement.FontSize="24"
Grid.Column="2"/>
</Grid>
</Grid>
<Grid x:Name="PART_MainContentGrid"
Grid.Row="1"
Panel.ZIndex="10">
<ContentPresenter x:Name="PART_MainContentPresenter"
Grid.Row="1"/>
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
WPF Project -> Styles.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFCustomWindowSample">
<Style TargetType="local:MainWindow" BasedOn="{StaticResource SWWindowStyle}"/>
</ResourceDictionary>
WPF Project -> App.xaml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,
/SourceWeave.Controls;component/Themes/SWStyles.xaml"/>
<ResourceDictionary Source="Styles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
好的。让我们看看 `SWStyles.xaml`。
第一个样式是我们窗口控件的基本按钮样式。
有趣的部分从第二个样式开始。我们有一个非常基础和标准的控件模板,包含一个标题栏和一个内容呈现器。
哦……
本文还将介绍一个额外的奖励内容——如何在 WPF 中使用 `FontAwesome`。:)
只需为 **两个项目** 在你的 `PackageManager` 控制台中调用此命令,即可完成所有设置。
PM> Install-Package FontAwesome.WPF
我们使用它来制作酷炫的窗口控件图标,但你还可以用它做更多事情。只需访问他们的 github 页面。
此时,启动项目应该看起来像
自定义标题栏上的按钮仍然不起作用,在移除默认标题栏后我们需要它们。让我们将它们连接起来。
public partial class SWWindow : Window
{
public Grid WindowRoot { get; private set; }
public Grid LayoutRoot { get; private set; }
public Button MinimizeButton { get; private set; }
public Button MaximizeButton { get; private set; }
public Button RestoreButton { get; private set; }
public Button CloseButton { get; private set; }
public Grid HeaderBar { get; private set; }
public T GetRequiredTemplateChild<T>(string childName) where T : DependencyObject
{
return (T)base.GetTemplateChild(childName);
}
public override void OnApplyTemplate()
{
this.WindowRoot = this.GetRequiredTemplateChild<Grid>("WindowRoot");
this.LayoutRoot = this.GetRequiredTemplateChild<Grid>("LayoutRoot");
this.MinimizeButton = this.GetRequiredTemplateChild
<System.Windows.Controls.Button>("MinimizeButton");
this.MaximizeButton = this.GetRequiredTemplateChild
<System.Windows.Controls.Button>("MaximizeButton");
this.RestoreButton = this.GetRequiredTemplateChild
<System.Windows.Controls.Button>("RestoreButton");
this.CloseButton = this.GetRequiredTemplateChild
<System.Windows.Controls.Button>("CloseButton");
this.HeaderBar = this.GetRequiredTemplateChild<Grid>("PART_HeaderBar");
if (this.CloseButton != null)
{
this.CloseButton.Click += CloseButton_Click;
}
if (this.MinimizeButton != null)
{
this.MinimizeButton.Click += MinimizeButton_Click;
}
if (this.RestoreButton != null)
{
this.RestoreButton.Click += RestoreButton_Click;
}
if (this.MaximizeButton != null)
{
this.MaximizeButton.Click += MaximizeButton_Click;
}
base.OnApplyTemplate();
}
protected void ToggleWindowState()
{
if (base.WindowState != WindowState.Maximized)
{
base.WindowState = WindowState.Maximized;
}
else
{
base.WindowState = WindowState.Normal;
}
}
private void MaximizeButton_Click(object sender, RoutedEventArgs e)
{
this.ToggleWindowState();
}
private void RestoreButton_Click(object sender, RoutedEventArgs e)
{
this.ToggleWindowState();
}
private void MinimizeButton_Click(object sender, RoutedEventArgs e)
{
this.WindowState = WindowState.Minimized;
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
}
太棒了!
现在按钮已经连接并且可以工作了,是时候移除那个讨厌的 Windows 边框了。
移除窗口装饰
好吧。网上能找到的大多数文章都会告诉你将窗口样式设置为 `None`。虽然这确实解决了讨厌的窗口边框问题,但在此过程中你会失去很多窗口功能。例如,用鼠标拖动窗口进行停靠、使用组合键最小化、停靠等将无法工作。另一个“酷”的副作用是,当你最大化窗口时,它也会覆盖任务栏。哦,如果你对视觉效果很讲究——窗口阴影和动画也消失了。
我有一个更好的方法给你。准备好了吗?
SWStyles.xaml -> SWWindowStyle
<Setter Property="WindowChrome.WindowChrome">
<Setter.Value>
<WindowChrome GlassFrameThickness="1"
ResizeBorderThickness="4"
CaptionHeight="0"/>
</Setter.Value>
</Setter>
以这种方式启动应用程序,你会得到你一直梦想的自定义窗口……几乎。
还有一些事情要做。首先也是最重要的——窗口不可拖动。让我们来修复它。
//SWWindow.cs
public override void OnApplyTemplate()
{
// ...
this.HeaderBar = this.GetRequiredTemplateChild<Grid>("PART_HeaderBar");
// ...
if (this.HeaderBar != null)
{
this.HeaderBar.AddHandler(Grid.MouseLeftButtonDownEvent,
new MouseButtonEventHandler(this.OnHeaderBarMouseLeftButtonDown));
}
base.OnApplyTemplate();
}
protected virtual void OnHeaderBarMouseLeftButtonDown
(object sender, MouseButtonEventArgs e)
{
System.Windows.Point position = e.GetPosition(this);
int headerBarHeight = 36;
int leftmostClickableOffset = 50;
if (position.X - this.LayoutRoot.Margin.Left <= leftmostClickableOffset &&
position.Y <= headerBarHeight)
{
if (e.ClickCount != 2)
{
// this.OpenSystemContextMenu(e);
}
else
{
base.Close();
}
e.Handled = true;
return;
}
if (e.ClickCount == 2 && base.ResizeMode == ResizeMode.CanResize)
{
this.ToggleWindowState();
return;
}
if (base.WindowState == WindowState.Maximized)
{
this.isMouseButtonDown = true;
this.mouseDownPosition = position;
}
else
{
try
{
this.positionBeforeDrag = new System.Windows.Point(base.Left, base.Top);
base.DragMove();
}
catch
{
}
}
}
现在,这里有很多内容,但重点是:窗口会像普通窗口一样通过与 `HeaderBar` 的交互来移动、最大化和关闭。那里有一行被注释掉的代码,我们稍后会处理它。
此时,这可能对你来说已经足够了,因为这是一个功能齐全的窗口。但是……你可能已经注意到一些奇怪的地方。
在某些情况下,最大化窗口会截断框架的一部分。如果你有双显示器设置,你甚至可能看到被截断的部分延伸到相邻的显示器上。
为了解决这个问题……我们需要变得……有创意。
打磨行为
现在,请忍耐一下。以下 **魔法** 是经过一周关于不同 DPI 的研究和测试的结果,但我找到了解决该问题的方法。为此,你需要为“控件”库项目添加两个额外的引用。
……并创建一个系统辅助类来获取一些操作系统配置值。
internal static class SystemHelper
{
public static int GetCurrentDPI()
{
return (int)typeof(SystemParameters).GetProperty
("Dpi", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null, null);
}
public static double GetCurrentDPIScaleFactor()
{
return (double)SystemHelper.GetCurrentDPI() / 96;
}
public static Point GetMousePositionWindowsForms()
{
System.Drawing.Point point = Control.MousePosition;
return new Point(point.X, point.Y);
}
}
之后,我们需要处理窗口的一些重置大小和状态更改事件。
// SWWindow.Sizing.cs
public SWWindow()
{
double currentDPIScaleFactor =
(double)SystemHelper.GetCurrentDPIScaleFactor();
Screen screen =
Screen.FromHandle((new WindowInteropHelper(this)).Handle);
base.SizeChanged +=
new SizeChangedEventHandler(this.OnSizeChanged);
base.StateChanged += new EventHandler(this.OnStateChanged);
base.Loaded += new RoutedEventHandler(this.OnLoaded);
Rectangle workingArea = screen.WorkingArea;
base.MaxHeight =
(double)(workingArea.Height + 16) / currentDPIScaleFactor;
SystemEvents.DisplaySettingsChanged +=
new EventHandler(this.SystemEvents_DisplaySettingsChanged);
this.AddHandler(Window.MouseLeftButtonUpEvent,
new MouseButtonEventHandler(this.OnMouseButtonUp), true);
this.AddHandler(Window.MouseMoveEvent,
new System.Windows.Input.MouseEventHandler(this.OnMouseMove));
}
protected virtual Thickness GetDefaultMarginForDpi()
{
int currentDPI = SystemHelper.GetCurrentDPI();
Thickness thickness = new Thickness(8, 8, 8, 8);
if (currentDPI == 120)
{
thickness = new Thickness(7, 7, 4, 5);
}
else if (currentDPI == 144)
{
thickness = new Thickness(7, 7, 3, 1);
}
else if (currentDPI == 168)
{
thickness = new Thickness(6, 6, 2, 0);
}
else if (currentDPI == 192)
{
thickness = new Thickness(6, 6, 0, 0);
}
else if (currentDPI == 240)
{
thickness = new Thickness(6, 6, 0, 0);
}
return thickness;
}
protected virtual Thickness GetFromMinimizedMarginForDpi()
{
int currentDPI = SystemHelper.GetCurrentDPI();
Thickness thickness = new Thickness(7, 7, 5, 7);
if (currentDPI == 120)
{
thickness = new Thickness(6, 6, 4, 6);
}
else if (currentDPI == 144)
{
thickness = new Thickness(7, 7, 4, 4);
}
else if (currentDPI == 168)
{
thickness = new Thickness(6, 6, 2, 2);
}
else if (currentDPI == 192)
{
thickness = new Thickness(6, 6, 2, 2);
}
else if (currentDPI == 240)
{
thickness = new Thickness(6, 6, 0, 0);
}
return thickness;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
double width = (double)screen.WorkingArea.Width;
Rectangle workingArea = screen.WorkingArea;
this.previousScreenBounds = new System.Windows.Point(width,
(double)workingArea.Height);
}
private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
{
Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
double width = (double)screen.WorkingArea.Width;
Rectangle workingArea = screen.WorkingArea;
this.previousScreenBounds = new System.Windows.Point(width,
(double)workingArea.Height);
this.RefreshWindowState();
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (base.WindowState == WindowState.Normal)
{
this.HeightBeforeMaximize = base.ActualHeight;
this.WidthBeforeMaximize = base.ActualWidth;
return;
}
if (base.WindowState == WindowState.Maximized)
{
Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
if (this.previousScreenBounds.X != (double)screen.WorkingArea.Width ||
this.previousScreenBounds.Y != (double)screen.WorkingArea.Height)
{
double width = (double)screen.WorkingArea.Width;
Rectangle workingArea = screen.WorkingArea;
this.previousScreenBounds = new System.Windows.Point(width,
(double)workingArea.Height);
this.RefreshWindowState();
}
}
}
private void OnStateChanged(object sender, EventArgs e)
{
Screen screen = Screen.FromHandle((new WindowInteropHelper(this)).Handle);
Thickness thickness = new Thickness(0);
if (this.WindowState != WindowState.Maximized)
{
double currentDPIScaleFactor = (double)SystemHelper.GetCurrentDPIScaleFactor();
Rectangle workingArea = screen.WorkingArea;
this.MaxHeight = (double)(workingArea.Height + 16) / currentDPIScaleFactor;
this.MaxWidth = double.PositiveInfinity;
if (this.WindowState != WindowState.Maximized)
{
this.SetMaximizeButtonsVisibility(Visibility.Visible, Visibility.Collapsed);
}
}
else
{
thickness = this.GetDefaultMarginForDpi();
if (this.PreviousState == WindowState.Minimized ||
this.Left == this.positionBeforeDrag.X &&
this.Top == this.positionBeforeDrag.Y)
{
thickness = this.GetFromMinimizedMarginForDpi();
}
this.SetMaximizeButtonsVisibility(Visibility.Collapsed, Visibility.Visible);
}
this.LayoutRoot.Margin = thickness;
this.PreviousState = this.WindowState;
}
private void OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (!this.isMouseButtonDown)
{
return;
}
double currentDPIScaleFactor = (double)SystemHelper.GetCurrentDPIScaleFactor();
System.Windows.Point position = e.GetPosition(this);
System.Diagnostics.Debug.WriteLine(position);
System.Windows.Point screen = base.PointToScreen(position);
double x = this.mouseDownPosition.X - position.X;
double y = this.mouseDownPosition.Y - position.Y;
if (Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2)) > 1)
{
double actualWidth = this.mouseDownPosition.X;
if (this.mouseDownPosition.X <= 0)
{
actualWidth = 0;
}
else if (this.mouseDownPosition.X >= base.ActualWidth)
{
actualWidth = this.WidthBeforeMaximize;
}
if (base.WindowState == WindowState.Maximized)
{
this.ToggleWindowState();
this.Top = (screen.Y - position.Y) / currentDPIScaleFactor;
this.Left = (screen.X - actualWidth) / currentDPIScaleFactor;
this.CaptureMouse();
}
this.isManualDrag = true;
this.Top = (screen.Y - this.mouseDownPosition.Y) / currentDPIScaleFactor;
this.Left = (screen.X - actualWidth) / currentDPIScaleFactor;
}
}
private void OnMouseButtonUp(object sender, MouseButtonEventArgs e)
{
this.isMouseButtonDown = false;
this.isManualDrag = false;
this.ReleaseMouseCapture();
}
private void RefreshWindowState()
{
if (base.WindowState == WindowState.Maximized)
{
this.ToggleWindowState();
this.ToggleWindowState();
}
}
我知道这看起来怎么样?哦,是的!
它好看吗?肯定不!
但是……
大约 80% 的时间,它一直有效!这对于大多数 WPF 自定义窗口应用程序来说已经足够了。此外,如果你看看常用的 WPF IDE(VisualStudio,还有谁会用其他东西呢……)的后台,你会发现很多类似甚至更糟糕的情况。不信?只需反编译 devenv.exe,然后看看;)
当然,**很多** 代码都可以通过架构、抽象等方式进行改进。然而,这不是帖子的重点。你可以随意使用你所看到的信息和方法。
现在,我答应要看一下 `HeaderBar MouseDown` 处理程序中被注释掉的部分。这里才是真正硬核的部分。
显示系统的上下文菜单
这是我找不到不使用互操作服务就能实现的事情。唯一的另一种方法是手动实现每一个功能,但这太……疯狂了。所以……
首先,我们需要一个“`bridge`”类来调用原生函数。
internal static class NativeUtils
{
internal static uint TPM_LEFTALIGN;
internal static uint TPM_RETURNCMD;
static NativeUtils()
{
NativeUtils.TPM_LEFTALIGN = 0;
NativeUtils.TPM_RETURNCMD = 256;
}
[DllImport("user32.dll", CharSet = CharSet.None, ExactSpelling = false)]
internal static extern IntPtr PostMessage
(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = false,
SetLastError = true)]
internal static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll", CharSet = CharSet.None, ExactSpelling = false)]
internal static extern bool EnableMenuItem
(IntPtr hMenu, uint uIDEnableItem, uint uEnable);
[DllImport("user32.dll", CharSet = CharSet.None, ExactSpelling = false)]
internal static extern int TrackPopupMenuEx
(IntPtr hmenu, uint fuFlags, int x, int y, IntPtr hwnd, IntPtr lptpm);
}
之后,这相当直接。只需取消注释 `Header MouseLeftButtonDown` 处理程序中的那部分代码,然后添加以下方法
private void OpenSystemContextMenu(MouseButtonEventArgs e)
{
System.Windows.Point position = e.GetPosition(this);
System.Windows.Point screen = this.PointToScreen(position);
int num = 36;
if (position.Y < (double)num)
{
IntPtr handle = (new WindowInteropHelper(this)).Handle;
IntPtr systemMenu = NativeUtils.GetSystemMenu(handle, false);
if (base.WindowState != WindowState.Maximized)
{
NativeUtils.EnableMenuItem(systemMenu, 61488, 0);
}
else
{
NativeUtils.EnableMenuItem(systemMenu, 61488, 1);
}
int num1 = NativeUtils.TrackPopupMenuEx(systemMenu,
NativeUtils.TPM_LEFTALIGN | NativeUtils.TPM_RETURNCMD,
Convert.ToInt32(screen.X + 2), Convert.ToInt32(screen.Y + 2),
handle, IntPtr.Zero);
if (num1 == 0)
{
return;
}
NativeUtils.PostMessage(handle, 274, new IntPtr(num1), IntPtr.Zero);
}
}
我承认,这是复制粘贴的。我不记得是从哪一千篇文章里复制来的了,但它确实有效。
填充
现在,为了好玩,让我们在 `Main` 窗口中添加一些内容。你知道,为了看看它是否真的有效。
<sw:SWWindow x:Class="WPFCustomWindow.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sw="clr-namespace:SourceWeave.Controls;assembly=SourceWeave.Controls"
xmlns:local="clr-namespace:WPFCustomWindow"
mc:Ignorable="d"
Title="MagicMainWindow" Height="450" Width="800">
<Grid>
<Button Content="Click me to see some magic!" Click="Button_Click"/>
</Grid>
</sw:SWWindow>
public partial class MainWindow : SWWindow
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Some Magic");
}
}
总结
好的……我们学会了如何
- 继承自系统窗口
- 自定义窗口的内容模板
- 移除窗口装饰
- 让无边框窗口真正按照我们期望的那样运行
- 在我们的自定义窗口上显示默认的窗口上下文菜单。
你可以在 我们的 github 中找到代码。你可以根据需要使用它。当我需要这样做的时候,我肯定会利用这样的例子。
如果你知道有更好的方法来创建 WPF 的自定义窗口,请告诉我。