恢复 Windows 系统功能的自定义 WPF 窗口
一个完全自定义的 WPF 窗口,具有您期望的所有功能
引言
通过在 WPF 中设置 WindowStyle="None"
,您可以完全自定义窗口,但是,这会移除窗口应具有的所有功能。一些丢失的功能是
- 最大化到屏幕的工作区域(除去任务栏的可用空间)
- 还原窗口时记住窗口的大小和位置
- 通过拖动窗口边缘进行窗口大小调整
- 通过拖动窗口标题栏进行还原
- 在多屏幕设置中最大化到正确的显示器
在这篇文章中,我创建了一些带有蓝色或红色边框的示例窗口,以说明调整大小元素所在的位置。基本示例有一个标准的标题栏,项目还提供了另外两个示例,一个是浏览器风格的窗口,另一个是带有左对齐导航列的窗口。
工作示例
下面的 GIF 是使用本文中的代码创建的,其风格类似于 Opera 浏览器。我选择这个示例是为了突出显示通常非客户端区域的潜在用途。
贷方
此代码使用了 micdenny 的 WpfScreenHelper 来查找显示大小和边界信息,而无需引入对 Windows Forms 的依赖。它可以在 GitHub
或 NuGet
上找到,使用以下包命令
Install-Package WpfScreenHelper -Version 0.3.0
Using the Code
- 将文件提取到您所需的位置
- 将
ExampleBaseWindow
、Screen Finder 和WindowStateHelper
导入您的项目 - 在 ExampleBaseWindow.xaml 中搜索“Window resize behaviour”,这将带您到具有蓝色或红色“
Fill
”的矩形,将它们设置为“Transparent
” - 编辑“
MainGrid
”的其余部分以适应您想要使用的设计
理解代码
除了窗口中的代码和 WpfScreenHelper
之外,我创建了两个 static
类来恢复我们期望从普通窗口获得的行为。
1. 窗口代码
a) 调整大小效果
首先,我们需要向窗口添加调整大小的效果,这需要将 HwndSource
包含在应用程序中,并将我们的窗口添加到其呈现源。这在窗口的 SourceInitialized
事件中完成。我们还需要一个 enum
,其中包含每个方向的值,以便传递给通过使用 Win32 中的 SendMessage
来处理调整大小效果的方法。
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Shapes;
using CustomWindowWpf.Classes;
namespace CustomWindowWpf.Windows
{
public partial class MainWindow : Window
{
private HwndSource _hwndSource;
public MainWindow()
{
InitializeComponent();
ButtonWindowStateNormal.Visibility = Visibility.Collapsed;
}
#region WindowResizing
private void Window_OnSourceInitialized(object sender, EventArgs e)
{
// Call for resizing effects
_hwndSource = (HwndSource)PresentationSource.FromVisual(this);
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage
(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam);
private void ResizeWindow(ResizeDirection direction)
{
SendMessage(_hwndSource.Handle, 0x112, (IntPtr)(61440 + direction), IntPtr.Zero);
}
private enum ResizeDirection
{
Left = 1,
Right = 2,
Top = 3,
TopLeft = 4,
TopRight = 5,
Bottom = 6,
BottomLeft = 7,
BottomRight = 8,
}
在示例中,每个角落的矩形用红色标出,线条边框用蓝色标出,这些都已触发 PreviewMouseDown
事件和 MouseMove
事件,以便在鼠标移入这些区域时处理光标更改。 WindowStateHelper.IsMaximized
是我们自己的布尔值,稍后将在文章中讨论。
private void WindowResize_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var rectangle = (Rectangle)sender;
if (rectangle == null) return;
if (WindowStateHelper.IsMaximized) return;
switch (rectangle.Name)
{
case "WindowResizeTop":
Cursor = Cursors.SizeNS;
ResizeWindow(ResizeDirection.Top);
break;
case "WindowResizeBottom":
Cursor = Cursors.SizeNS;
ResizeWindow(ResizeDirection.Bottom);
break;
case "WindowResizeLeft":
Cursor = Cursors.SizeWE;
ResizeWindow(ResizeDirection.Left);
break;
case "WindowResizeRight":
Cursor = Cursors.SizeWE;
ResizeWindow(ResizeDirection.Right);
break;
case "WindowResizeTopLeft":
Cursor = Cursors.SizeNWSE;
ResizeWindow(ResizeDirection.TopLeft);
break;
case "WindowResizeTopRight":
Cursor = Cursors.SizeNESW;
ResizeWindow(ResizeDirection.TopRight);
break;
case "WindowResizeBottomLeft":
Cursor = Cursors.SizeNESW;
ResizeWindow(ResizeDirection.BottomLeft);
break;
case "WindowResizeBottomRight":
Cursor = Cursors.SizeNWSE;
ResizeWindow(ResizeDirection.BottomRight);
break;
}
}
private void WindowResize_OnMouseMove(object sender, MouseEventArgs e)
{
var rectangle = (Rectangle)sender;
if (rectangle == null) return;
if (WindowStateHelper.IsMaximized) return;
// ReSharper disable once SwitchStatementMissingSomeCases
switch (rectangle.Name)
{
case "WindowResizeTop":
Cursor = Cursors.SizeNS;
break;
case "WindowResizeBottom":
Cursor = Cursors.SizeNS;
break;
case "WindowResizeLeft":
Cursor = Cursors.SizeWE;
break;
case "WindowResizeRight":
Cursor = Cursors.SizeWE;
break;
case "WindowResizeTopLeft":
Cursor = Cursors.SizeNWSE;
break;
case "WindowResizeTopRight":
Cursor = Cursors.SizeNESW;
break;
case "WindowResizeBottomLeft":
Cursor = Cursors.SizeNESW;
break;
case "WindowResizeBottomRight":
Cursor = Cursors.SizeNWSE;
break;
}
}
private void Window_OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
Cursor = Cursors.Arrow;
}
b) 窗口按钮:最小化、还原、最大化和关闭
在 XAML 中创建按钮后,我们需要将单击事件挂钩到这些按钮,并创建两个方法来显示或隐藏最大化窗口或还原窗口,具体取决于窗口状态的变化。
private void ShowRestoreDownButton()
{
ButtonMaximize.Visibility = Visibility.Collapsed;
ButtonWindowStateNormal.Visibility = Visibility.Visible;
}
private void ShowMaximumWindowButton()
{
ButtonWindowStateNormal.Visibility = Visibility.Collapsed;
ButtonMaximize.Visibility = Visibility.Visible;
}
private void ButtonClose_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
private void ButtonMinimize_OnClick(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}
private void ButtonRestoreDown_OnClick(object sender, RoutedEventArgs e)
{
ShowMaximumWindowButton();
WindowStateHelper.SetWindowSizeToNormal(this);
}
private void ButtonMaximize_OnClick(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Maximized;
ShowRestoreDownButton();
}
2. WindowStateHelper
由于窗口中最后两个方法主要关注 WindowStateHelper
和 ScreenFinder
,因此我们将先讨论这两个 static
类,然后再展示窗口中的最后两个方法。
WindowStateHelper
存储窗口在“正常”状态下的最后一个已知的位置(顶部、左侧、宽度和高度)。此类中还有另外两个属性
IsMaximized
- 我们需要设置自己的属性来表示最大化,而不是WindowState.Maximized
,因为通过WindowState
设置会将您的窗口大小设置为整个屏幕(覆盖任务栏),而不是工作区域(排除任务栏)。BlockStateChange
- 我们会在窗口在屏幕上拖动时更新普通窗口的大小和位置,因此这可以防止在您单击最大化按钮时,事件会更新最后一个已知的正常大小为最大化。
using System.Windows;
using WpfScreenHelper;
namespace CustomWindowWpf.Classes
{
public static class WindowStateHelper
{
private static double Top { get; set; }
private static double Left { get; set; }
private static double Width { get; set; }
private static double Height { get; set; }
// Required because using WindowState.Maximized will not respect
// the WorkingArea of the screen in a fully custom window
public static bool IsMaximized { get; private set; }
// Blocks the window from running OnSizeChanged when resizing the window
public static bool BlockStateChange { get; set; }
private static void SetWindowTop(Window window)
{
BlockStateChange = true;
window.Top = Top;
}
private static void SetWindowLeft(Window window)
{
BlockStateChange = true;
window.Left = Left;
}
private static void SetWindowWidth(Window window)
{
BlockStateChange = true;
window.Width = Width;
}
private static void SetWindowHeight(Window window)
{
BlockStateChange = true;
window.Height = Height;
}
public static void UpdateLastKnownLocation(double top, double left)
{
Top = top;
Left = left;
}
public static void UpdateLastKnownNormalSize(double width, double height)
{
Width = width;
Height = height;
}
public static void SetWindowMaximized(Window window)
{
IsMaximized = true;
window.WindowState = WindowState.Normal;
}
将窗口大小设置为 normal
时,必须为每个属性阻止状态更改,否则窗口会将最后一个已知位置视为 0, 0
(当它最大化时所在的位置)。
当我们将窗口大小设置为 normal
时,我们还检查鼠标距离窗口左侧的距离。这允许我们创建一个从最大化状态平滑拖动效果,当 useMouseLocation = true
时,如下所示。
// Returns a percentage which is how far the mouse pointer is from the left of the window
private static double MousePercentageFromLeft(Window window)
{
var mouseMinusZeroToLeft = MouseHelper.MousePosition.X - window.Left;
var percentage = mouseMinusZeroToLeft / window.Width;
return percentage;
}
// Returns the window to its last known size and location before it was maximized.
// When useMouseLocation = true (dragging away from maximized) then the location is below the
// mouse pointer, respecting the percentage the pointer is from the left of the window
public static void SetWindowSizeToNormal(Window window, bool useMouseLocation = false)
{
IsMaximized = false;
var percentage = MousePercentageFromLeft(window);
SetWindowWidth(window);
SetWindowHeight(window);
if (useMouseLocation)
{
Top = MouseHelper.MousePosition.Y;
var valueOnNewSize = percentage * Width;
Left = MouseHelper.MousePosition.X - valueOnNewSize;
}
SetWindowTop(window);
SetWindowLeft(window);
}
3. Screen Finder
接下来是 ScreenFinder
类,用于在多屏幕设置中确定窗口应该最大化到哪个屏幕。它按以下顺序进行检查
- 整个窗口是否在单个屏幕的边界内?如果是,则返回该屏幕。
- 屏幕是否在两个屏幕之间(并排方向)?如果是,则测量窗口在每个屏幕中的比例,并返回最大结果。
- 如果前两个条件不满足,则返回主屏幕。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using WpfScreenHelper;
namespace CustomWindowWpf.Classes
{
public static class ScreenFinder
{
public static Screen FindAppropriateScreen(Window window)
{
var windowRight = window.Left + window.Width;
var windowBottom = window.Top + window.Height;
var allScreens = Screen.AllScreens.ToList();
// If the window is inside all of a single screen boundaries, maximize to that
var screenInsideAllBounds = allScreens.Find(x => window.Top >= x.Bounds.Top
&& window.Left >= x.Bounds.Left
&& windowRight <= x.Bounds.Right
&& windowBottom <= x.Bounds.Bottom);
if (screenInsideAllBounds != null)
{
return screenInsideAllBounds;
}
// Failing the above (between two screens in side-by-side configuration)
// Measure if the window is between the top and bottom of any screens.
// Then measure the percentage it is within each screen and pick a winner
var screensInBounds = allScreens.FindAll(x => window.Top >= x.Bounds.Top
&& windowBottom <= x.Bounds.Bottom);
if (screensInBounds.Count > 0)
{
var values = new List<Tuple<double, Screen>>();
// Determine the amount of width inside each screen
foreach (var screen in screensInBounds.OrderBy(x => x.Bounds.Left))
{
// This has only been tested in a two screen, side-by-side setup.
double amountInScreen;
if (screen.Bounds.Left == 0)
{
var rightOfWindow = window.Left + window.Width;
var outsideRightBoundary = rightOfWindow - screen.Bounds.Right;
amountInScreen = window.Width - outsideRightBoundary;
values.Add(new Tuple<double, Screen>(amountInScreen, screen));
}
else
{
var outsideLeftBoundary = screen.Bounds.Left - window.Left;
amountInScreen = window.Width - outsideLeftBoundary;
values.Add(new Tuple<double, Screen>(amountInScreen, screen));
}
}
values = values.OrderByDescending(x => x.Item1).ToList();
if (values.Count > 0)
{
return values[0].Item2;
}
}
// Failing all else
return Screen.PrimaryScreen;
}
}
}
4. 回到窗口
现在我们已经看到了 WindowStateHelper
和 ScreenFinder
,我们可以回到窗口来查看最后两个方法
PreviewMouseDown
(在可拖动区域上)SizeChanged
(在整个窗口上)
private void WindowDraggableArea_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
return;
if (WindowStateHelper.IsMaximized)
{
WindowStateHelper.SetWindowSizeToNormal(this, true);
ShowMaximumWindowButton();
DragMove();
}
else
{
DragMove();
}
WindowStateHelper.UpdateLastKnownLocation(Top, Left);
}
private void Window_OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (WindowState == WindowState.Maximized)
{
WindowStateHelper.SetWindowMaximized(this);
WindowStateHelper.BlockStateChange = true;
var screen = ScreenFinder.FindAppropriateScreen(this);
if (screen != null)
{
Top = screen.WorkingArea.Top;
Left = screen.WorkingArea.Left;
Width = screen.WorkingArea.Width;
Height = screen.WorkingArea.Height;
}
ShowRestoreDownButton();
}
else
{
if (WindowStateHelper.BlockStateChange)
{
WindowStateHelper.BlockStateChange = false;
return;
}
WindowStateHelper.UpdateLastKnownNormalSize(Width, Height);
WindowStateHelper.UpdateLastKnownLocation(Top, Left);
}
}
历史
- 2018 年 5 月:首次发布
- 2018 年 7 月:修复了
ScreenFinder
中的“expression is always true”错误 - 2018 年 7 月:添加了使用此代码创建的窗口的 GIF