WPF自定义控件——Window

  • Post author:
  • Post category:WPF
  • Post comments:0评论

自定义Window在商业桌面软件中十分常见,通常为了让软件界面整体更统一我们都会使用自定义的Window,再者自定义Window的上边缘区域提供了一片非常有用的空间。

这是来自WindowChrome 类 (System.Windows.Shell) | Microsoft Learn中的标准Window,

在WPF中一般有两种方法实现自定义Window,第一种是利用 WindowStyle="None"AllowsTransparency="True 去除原有的 non-client area(上图中除了Client area 外的区域,即Chrome),然后再在client area中定义Window的外观和行为,这种实现方式自由度很高,但是需要自己去实现Window原有的外观和行为;另一种实现方法则是利用 WindowChrome 来实现,WindowChrome 可以控制用户可操作/不可操作的区域,这种实现方式可以避免上一种方法的后遗症。

最终实现成这个样子(ZlaWindow):

相较标准Window ZlaWindow增加了 non-client area 的高度、背景颜色控制,在Window顶部区域的中间添加控件

先创建一个继承自Window的Control

using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Shell;

namespace ZlaWindow
{
    public class ZlaWindow : Window
    {
        public static readonly DependencyProperty NonClientAreaHeightProperty = DependencyProperty.Register(
            nameof(NonClientAreaHeight), typeof(double), typeof(ZlaWindow),
            new PropertyMetadata(35.0));

        /// <summary>
        /// 顶部区域高度
        /// </summary>
        public double NonClientAreaHeight
        {
            get => (double)GetValue(NonClientAreaHeightProperty);
            set => SetValue(NonClientAreaHeightProperty, value);
        }

        public static readonly DependencyProperty HeaderBarContentProperty = DependencyProperty.Register(
            nameof(HeaderBarContent), typeof(FrameworkElement), typeof(ZlaWindow), new PropertyMetadata(default(FrameworkElement)));

        /// <summary>
        /// 顶部区域中间的内容
        /// </summary>
        public FrameworkElement HeaderBarContent
        {
            get => (FrameworkElement)GetValue(HeaderBarContentProperty);
            set => SetValue(HeaderBarContentProperty, value);
        }

        public ZlaWindow()
        {
            DefaultStyleKey = typeof(ZlaWindow);
            var chrome = new WindowChrome
            {
                CornerRadius = new CornerRadius(),
                //控制边框大小
                GlassFrameThickness = new Thickness(5),
                ResizeBorderThickness = new Thickness(5),
                UseAeroCaptionButtons = false
            };
            WindowChrome.SetWindowChrome(this, chrome);

            //CaptionHeight 为顶部区域高度
            BindingOperations.SetBinding(chrome, WindowChrome.CaptionHeightProperty,
                new Binding(NonClientAreaHeightProperty.Name) { Source = this });
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, (s, e) =>
                WindowState = WindowState.Minimized));
            CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, (s, e) =>
                WindowState = WindowState.Maximized));
            CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, (s, e) =>
                WindowState = WindowState.Normal));
            CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, (s, e) => Close()));
        }
    }
}

在构造时设置了 WindowChrome ,通过绑定 NonClientAreaHeightProperty 控制 non-client area 的高度;另外提供 HeaderBarContent 给用户添加自定义控件在 non-client area 中;因为最大化最小化、关闭等按钮我们重新设计定义了,这里需要将这些Command添加到CommandBindings中

自定义Window的布局

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:local="clr-namespace:ZlaWindow">

    <Style x:Key="CmdPathStyle" TargetType="Path">
        <Setter Property="Width" Value="12" />
        <Setter Property="Height" Value="12" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="Fill" Value="White" />
        <Setter Property="UseLayoutRounding" Value="True" />
    </Style>

    <Style x:Key="CmdButtonStyle" TargetType="{x:Type Button}">
        <Setter Property="Background" Value="#32FFFFFF" />
        <Setter Property="BorderThickness" Value="0" />
        <Setter Property="Focusable" Value="False" />
        <Setter Property="HorizontalContentAlignment" Value="Center" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True" />
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Button}">
                    <Grid x:Name="LayoutRoot" Background="Transparent">
                        <Rectangle
                            x:Name="ButtonBackground"
                            Fill="{TemplateBinding Background}"
                            Opacity="0" />
                        <Border x:Name="ButtonBorder" SnapsToDevicePixels="true">
                            <ContentPresenter
                                x:Name="TitleBarButtonContentPresenter"
                                Margin="{TemplateBinding Padding}"
                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                Focusable="False"
                                RecognizesAccessKey="True"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        </Border>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="true">
                            <Setter TargetName="ButtonBackground" Property="Opacity" Value="1" />
                        </Trigger>
                        <Trigger Property="IsPressed" Value="True">
                            <Setter TargetName="ButtonBackground" Property="Opacity" Value="0.6" />
                        </Trigger>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter TargetName="TitleBarButtonContentPresenter" Property="Opacity" Value=".5" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Path
        x:Key="RestoreIcon"
        Data="M1,3 L1,11 L9,11 L9,3 z M3,1 L3,2 L10,2 L10,9 L11,9 L11,1 z M2 ,0 L12,0 L12,10 L10,10 L10,12 L0,12 L0,2 L2 ,2 z"
        Style="{StaticResource CmdPathStyle}" />

    <Path
        x:Key="CloseIcon"
        Data="M1,0 L6,5 L11,0 L12,1 L7,6 L12,11 L11,12 L6,7 L1,12 L0,11 L5,6 L0,1 z"
        Style="{StaticResource CmdPathStyle}" />

    <Path
        x:Key="MaximizeIcon"
        Data="M1,1  L1 ,11 L11,11 L11,1 z M0,0 L12,0 L12,12 L0,12 z"
        Style="{StaticResource CmdPathStyle}" />

    <Path
        x:Key="MinimizeIcon"
        Data="M0,6 L12,6 L12,7 L0,7 z"
        Style="{StaticResource CmdPathStyle}" />

    <Style TargetType="{x:Type local:ZlaWindow}">
        <Setter Property="Background" Value="White" />
        <Setter Property="SnapsToDevicePixels" Value="True" />
        <Setter Property="BorderBrush" Value="#4f4e4f" />
        <Setter Property="Icon">
            <Setter.Value>
                <BitmapImage UriSource="pack://application:,,,/ZlaWindow;component/Zla.png" />
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ZlaWindow}">
                    <Border x:Name="WindowBorder" BorderThickness="0">
                        <Grid x:Name="LayoutRoot" Background="{TemplateBinding Background}">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="*" />
                            </Grid.RowDefinitions>
                            <Grid
                                x:Name="WindowTitlePanel"
                                Grid.Row="0"
                                Height="{TemplateBinding NonClientAreaHeight}"
                                Background="{TemplateBinding BorderBrush}">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="*" />
                                    <ColumnDefinition Width="Auto" />
                                </Grid.ColumnDefinitions>
                                <StackPanel
                                    Grid.Column="0"
                                    Margin="5,0,0,0"
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Center"
                                    Orientation="Horizontal">
                                    <Image
                                        Width="{x:Static SystemParameters.SmallIconWidth}"
                                        Height="{x:Static SystemParameters.SmallIconHeight}"
                                        Margin="5,0,0,0"
                                        VerticalAlignment="Center"
                                        Source="{TemplateBinding Icon}"
                                        WindowChrome.IsHitTestVisibleInChrome="True" />
                                    <ContentControl
                                        Margin="5,0,0,0"
                                        VerticalAlignment="Center"
                                        Content="{TemplateBinding Title}"
                                        FontSize="16"
                                        Foreground="White" />
                                </StackPanel>
                                <Grid Grid.Column="1" WindowChrome.IsHitTestVisibleInChrome="True">
                                    <ContentPresenter
                                        VerticalAlignment="Center"
                                        Content="{Binding HeaderBarContent, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
                                        Focusable="False" />
                                </Grid>
                                <!--  IsHitTestVisibleInChrome设置此元素在chrome中可以交互  -->
                                <StackPanel
                                    Grid.Column="2"
                                    Background="Transparent"
                                    Orientation="Horizontal"
                                    WindowChrome.IsHitTestVisibleInChrome="True">
                                    <Button
                                        Name="ButtonMinimize"
                                        Width="{TemplateBinding NonClientAreaHeight}"
                                        Height="{TemplateBinding NonClientAreaHeight}"
                                        Command="{Binding Source={x:Static SystemCommands.MinimizeWindowCommand}}"
                                        Content="{StaticResource MinimizeIcon}"
                                        Style="{StaticResource CmdButtonStyle}" />
                                    <Grid Name="SizeChangeButtons">
                                        <Button
                                            Name="ButtonRestore"
                                            Width="{TemplateBinding NonClientAreaHeight}"
                                            Height="{TemplateBinding NonClientAreaHeight}"
                                            Command="{Binding Source={x:Static SystemCommands.RestoreWindowCommand}}"
                                            Content="{StaticResource RestoreIcon}"
                                            Style="{StaticResource CmdButtonStyle}"
                                            Visibility="Collapsed" />
                                        <Button
                                            Name="ButtonMax"
                                            Width="{TemplateBinding NonClientAreaHeight}"
                                            Height="{TemplateBinding NonClientAreaHeight}"
                                            Command="{Binding Source={x:Static SystemCommands.MaximizeWindowCommand}}"
                                            Content="{StaticResource MaximizeIcon}"
                                            Style="{StaticResource CmdButtonStyle}" />
                                    </Grid>
                                    <Button
                                        Name="ButtonClose"
                                        Width="{TemplateBinding NonClientAreaHeight}"
                                        Height="{TemplateBinding NonClientAreaHeight}"
                                        Background="Red"
                                        Command="{Binding Source={x:Static SystemCommands.CloseWindowCommand}}"
                                        Content="{StaticResource CloseIcon}"
                                        Style="{StaticResource CmdButtonStyle}" />
                                </StackPanel>
                            </Grid>
                            <AdornerDecorator Grid.Row="1" KeyboardNavigation.IsTabStop="False">
                                <ContentPresenter ClipToBounds="True" />
                            </AdornerDecorator>
                        </Grid>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="WindowState" Value="Maximized">
                            <Setter TargetName="ButtonMax" Property="Visibility" Value="Collapsed" />
                            <Setter TargetName="ButtonRestore" Property="Visibility" Value="Visible" />
                            <Setter TargetName="WindowBorder" Property="Padding" Value="{x:Static SystemParameters.WindowResizeBorderThickness}" />
                            <Setter TargetName="LayoutRoot" Property="Margin" Value="{x:Static local:WindowParameters.PaddedBorderThickness}" />
                        </Trigger>
                        <Trigger Property="WindowState" Value="Normal">
                            <Setter TargetName="ButtonMax" Property="Visibility" Value="Visible" />
                            <Setter TargetName="ButtonRestore" Property="Visibility" Value="Collapsed" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

另外提供一个WindowParameter来解决最大化存在的问题,关于这个问题可以参考一下这篇文章([WPF 自定义控件]使用WindowChrome的问题 – dino.c – 博客园 (cnblogs.com)

using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Windows;
using System.Windows.Media;

namespace ZlaWindow
{
    public static class WindowParameters
    {
        private static Thickness? _paddedBorderThickness;

        [ThreadStatic]
        private static Matrix _transformToDip;

        /// <summary>
        /// returns the border thickness padding around captioned windows,in pixels. Windows XP/2000:  This value is not supported.
        /// </summary>
        public static Thickness PaddedBorderThickness
        {
            [SecurityCritical]
            get
            {
                if (_paddedBorderThickness == null)
                {
                    var paddedBorder = NativeMethods.GetSystemMetrics(SM.CXPADDEDBORDER);
                    var dpi = GetDpi();
                    Size frameSize = new Size(paddedBorder, paddedBorder);
                    Size frameSizeInDips = DeviceSizeToLogical(frameSize, dpi / 96.0, dpi / 96.0);
                    _paddedBorderThickness = new Thickness(frameSizeInDips.Width, frameSizeInDips.Height, frameSizeInDips.Width, frameSizeInDips.Height);
                }

                return _paddedBorderThickness.Value;
            }
        }

        /// <summary>
        /// Get Dpi
        /// </summary>
        /// <returns>Return 96,144</returns>
        public static double GetDpi()
        {
            var dpiXProperty = typeof(SystemParameters).GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static);

            var dpiX = (int)dpiXProperty.GetValue(null, null);
            return dpiX;
        }

        /// <summary>
        /// Convert a point in system coordinates to a point in device independent pixels (1/96").
        /// </summary>
        /// <param name="devicePoint">A point in the physical coordinate system.</param>
        /// <param name="dpiScaleX">dpiScaleX</param>
        /// <param name="dpiScaleY">dpiScaleY</param>
        /// <returns>Returns the parameter converted to the device independent coordinate system.</returns>
        public static Point DevicePixelsToLogical(Point devicePoint, double dpiScaleX, double dpiScaleY)
        {
            _transformToDip = Matrix.Identity;
            _transformToDip.Scale(1d / dpiScaleX, 1d / dpiScaleY);
            return _transformToDip.Transform(devicePoint);
        }

        public static Size DeviceSizeToLogical(Size deviceSize, double dpiScaleX, double dpiScaleY)
        {
            Point pt = DevicePixelsToLogical(new Point(deviceSize.Width, deviceSize.Height), dpiScaleX, dpiScaleY);

            return new Size(pt.X, pt.Y);
        }
    }

    /// <summary>
    /// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724385(v=vs.85).aspx
    /// Retrieves the specified system metric or system configuration setting.
    /// Note that all dimensions retrieved by GetSystemMetrics are in pixels.
    /// </summary>
    public enum SM
    {
        /// <summary>
        /// The amount of border padding for captioned windows, in pixels.
        /// Returns the amount of extra border padding around captioned windows
        /// Windows XP/2000:  This value is not supported.
        /// </summary>
        CXPADDEDBORDER = 92,
    }

    internal static class NativeMethods
    {
        [DllImport("user32.dll")]
        internal static extern int GetSystemMetrics(SM nIndex);
    }
}

最大化问题

第一张是使用了PaddedBorderThickness的,第二张没有使用,看出差异了吗?注意看边缘

发表回复