65.9K
CodeProject 正在变化。 阅读更多。
Home

恢复 Windows 系统功能的自定义 WPF 窗口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (6投票s)

2018 年 5 月 27 日

CPOL

4分钟阅读

viewsIcon

35828

downloadIcon

1394

一个完全自定义的 WPF 窗口,具有您期望的所有功能

引言

通过在 WPF 中设置 WindowStyle="None",您可以完全自定义窗口,但是,这会移除窗口应具有的所有功能。一些丢失的功能是

  • 最大化到屏幕的工作区域(除去任务栏的可用空间)
  • 还原窗口时记住窗口的大小和位置
  • 通过拖动窗口边缘进行窗口大小调整
  • 通过拖动窗口标题栏进行还原
  • 在多屏幕设置中最大化到正确的显示器

在这篇文章中,我创建了一些带有蓝色或红色边框的示例窗口,以说明调整大小元素所在的位置。基本示例有一个标准的标题栏,项目还提供了另外两个示例,一个是浏览器风格的窗口,另一个是带有左对齐导航列的窗口。

工作示例

下面的 GIF 是使用本文中的代码创建的,其风格类似于 Opera 浏览器。我选择这个示例是为了突出显示通常非客户端区域的潜在用途。

贷方

此代码使用了 micdenny 的 WpfScreenHelper 来查找显示大小和边界信息,而无需引入对 Windows Forms 的依赖。它可以在 GitHubNuGet 上找到,使用以下包命令

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 类,用于在多屏幕设置中确定窗口应该最大化到哪个屏幕。它按以下顺序进行检查

  1. 整个窗口是否在单个屏幕的边界内?如果是,则返回该屏幕。
  2. 屏幕是否在两个屏幕之间(并排方向)?如果是,则测量窗口在每个屏幕中的比例,并返回最大结果。
  3. 如果前两个条件不满足,则返回主屏幕。
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
© . All rights reserved.