目录

WPF-CustomControl(控件库)

起因

偶然间看到了下面这种用法,之前记录过自定义控件的开发:通用开发步骤,只不过不跨程序集,下面的这种是类似控件库的开发,特此记录。

<super:BaseWindow
    x:Class="SuperCom.Window_Monitor"
    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:super="https://github.com/SuperStudio/SuperControls"
    Title="监视器"
    Width="1000"
    Height="600"
    Background="{DynamicResource Window.Background}"
    ContentRendered="BaseWindow_ContentRendered"
    Foreground="{DynamicResource Window.Foreground}"
    Icon="pack://application:,,,/Resources/Ico/ICON.ico"
    WindowStartupLocation="CenterScreen"
    mc:Ignorable="d">
    <Grid>
        <!-- 省略具体逻辑 -->
    </Grid>
</super:BaseWindow>

示例

项目结构:

Solution
 ├─ DemoApp            主程序
 └─ MyControl       自定义控件库

MyControl 中可以将 CustomControl1.cs, Themes/Generic.xaml 删除

新建:

BaseWindow.cs
using System.Windows;

namespace SuperControls
{
    public class BaseWindow : Window
    {
        public BaseWindow()
        {
            Width = 800;
            Height = 500;
            WindowStartupLocation = WindowStartupLocation.CenterScreen;

            Title = "我是 BaseWindow";

            Background = System.Windows.Media.Brushes.White;
        }
    }
}

让 XAML 能识别前缀( XmlnsDefinition定义)

打开:

MyControl/Properties/AssemblyInfo.cs

如果没有,就新建。

内容:

using System.Windows.Markup;

[assembly: XmlnsDefinition(
    "https://hexin.pw/MyControl",
    "MyControl")]

意思:

XAML URI
映射到
namespace MyControl

这一步就是:

xmlns:super="https://hexin.pw/MyControl"

引用DLL

主程序引用控件库

右键:

DemoApp
→ 添加项目引用

勾选:

MyControl

使用

<mycontrol:BaseWindow
    x:Class="DemoApp.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:local="clr-namespace:DemoApp"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:mycontrol="https://hexin.pw/MyControl"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="30"
            Text="Hello BaseWindow" />
    </Grid>
</mycontrol:BaseWindow>
using MyControl;

namespace DemoApp
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : BaseWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

附:升级版示例

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Shapes;

namespace MyControl
{
    /// <summary>
    /// 自定义无边框窗口基类,支持拖拽、最大化还原、窗口缩放。
    /// </summary>
    public class BaseWindow : Window
    {
        #region 依赖属性

        public static readonly DependencyProperty HeaderProperty =
            DependencyProperty.Register(
                nameof(Header),
                typeof(string),
                typeof(BaseWindow),
                new PropertyMetadata(string.Empty));

        /// <summary>标题栏显示文本</summary>
        public string Header
        {
            get => (string)GetValue(HeaderProperty);
            set => SetValue(HeaderProperty, value);
        }

        #endregion

        #region 模板部件名称

        private const string PART_TitleBar = "PART_TitleBar";
        private const string PART_MinButton = "PART_MinButton";
        private const string PART_MaxButton = "PART_MaxButton";
        private const string PART_CloseButton = "PART_CloseButton";
        private const string PART_MaxIcon = "PART_MaxIcon";

        #endregion

        #region 字段

        private Border _titleBar;
        private Button _minButton;
        private Button _maxButton;
        private Button _closeButton;
        private Path _maxIcon;

        // 最大化图标路径(双矩形)
        private const string RestoreIconData =
            "M2.5,0.5 L10.5,0.5 L10.5,8.5 L2.5,8.5 Z M0.5,2.5 L0.5,10.5 L8.5,10.5 L8.5,8.5 L2.5,8.5 L2.5,2.5 Z";

        // 正常状态图标路径(单矩形)
        private const string NormalIconData =
            "M0.5,1.5 L9.5,1.5 L9.5,10.5 L0.5,10.5 Z";

        #endregion

        static BaseWindow()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(BaseWindow),
                new FrameworkPropertyMetadata(typeof(BaseWindow)));
        }

        // 实例构造函数:所有继承 BaseWindow 的窗口默认居中显示
        public BaseWindow()
        {
            WindowStartupLocation = WindowStartupLocation.CenterScreen;
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // 获取标题栏,用代码挂载鼠标事件
            _titleBar = GetTemplateChild(PART_TitleBar) as Border;
            if (_titleBar != null)
            {
                _titleBar.MouseLeftButtonDown += TitleBar_MouseLeftButtonDown;
            }

            _minButton = GetTemplateChild(PART_MinButton) as Button;
            _maxButton = GetTemplateChild(PART_MaxButton) as Button;
            _closeButton = GetTemplateChild(PART_CloseButton) as Button;
            _maxIcon = GetTemplateChild(PART_MaxIcon) as Path;

            if (_minButton != null)
            {
                _minButton.Click += (s, e) => WindowState = WindowState.Minimized;
            }

            if (_maxButton != null)
            {
                _maxButton.Click += (s, e) =>
                {
                    WindowState = WindowState == WindowState.Maximized
                        ? WindowState.Normal
                        : WindowState.Maximized;
                };
            }

            if (_closeButton != null)
            {
                _closeButton.Click += (s, e) => Close();
            }

            // 初始化图标
            UpdateMaxIcon();

            StateChanged += (_, e) => UpdateMaxIcon();
        }

        private void UpdateMaxIcon()
        {
            if (_maxIcon == null)
            {
                return;
            }

            _maxIcon.Data = Geometry.Parse(WindowState == WindowState.Maximized ? RestoreIconData : NormalIconData);
        }

        #region 标题栏拖拽 + 双击最大化

        private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left)
            {
                if (e.ClickCount == 2)
                {
                    WindowState = WindowState == WindowState.Maximized
                        ? WindowState.Normal
                        : WindowState.Maximized;
                    return;
                }

                DragMove();
            }
        }

        #endregion

        #region 窗口缩放(WM_NCHITTEST

        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);
            var hwndSource = PresentationSource.FromVisual(this) as HwndSource;
            hwndSource?.AddHook(WndProc);
        }

        private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            const int WM_NCHITTEST = 0x0084;

            if (msg == WM_NCHITTEST)
            {
                short x = (short)(lParam.ToInt64() & 0xFFFF);
                short y = (short)((lParam.ToInt64() >> 16) & 0xFFFF);

                var pos = PointFromScreen(new Point(x, y));

                double resizeBorder = 6;
                double actualWidth = ActualWidth;
                double actualHeight = ActualHeight;

                if (WindowState == WindowState.Maximized)
                    return IntPtr.Zero;

                bool onLeft = pos.X <= resizeBorder;
                bool onRight = pos.X >= actualWidth - resizeBorder;
                bool onTop = pos.Y <= resizeBorder;
                bool onBottom = pos.Y >= actualHeight - resizeBorder;

                const int HTLEFT = 10;
                const int HTRIGHT = 11;
                const int HTTOP = 12;
                const int HTBOTTOM = 15;
                const int HTTOPLEFT = 13;
                const int HTTOPRIGHT = 14;
                const int HTBOTTOMLEFT = 16;
                const int HTBOTTOMRIGHT = 17;

                if (onTop && onLeft) { handled = true; return (IntPtr)HTTOPLEFT; }
                if (onTop && onRight) { handled = true; return (IntPtr)HTTOPRIGHT; }
                if (onBottom && onLeft) { handled = true; return (IntPtr)HTBOTTOMLEFT; }
                if (onBottom && onRight) { handled = true; return (IntPtr)HTBOTTOMRIGHT; }
                if (onLeft) { handled = true; return (IntPtr)HTLEFT; }
                if (onRight) { handled = true; return (IntPtr)HTRIGHT; }
                if (onTop) { handled = true; return (IntPtr)HTTOP; }
                if (onBottom) { handled = true; return (IntPtr)HTBOTTOM; }
            }

            return IntPtr.Zero;
        }

        #endregion
    }
}

Themes/Generic.xaml

Generic.xaml
→ 属性
→ Build Action

改成:

Page // 一般默认就是 Page
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyControl">

    <!--  ===== 标题栏按钮通用样式 =====  -->
    <Style x:Key="TitleBarButtonStyle" TargetType="Button">
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="Foreground" Value="#CCCCCC" />
        <Setter Property="BorderThickness" Value="0" />
        <Setter Property="HorizontalContentAlignment" Value="Center" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border
                        x:Name="Bd"
                        Background="{TemplateBinding Background}"
                        SnapsToDevicePixels="True">
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="Bd" Property="Background" Value="#3E3E42" />
                            <Setter Property="Foreground" Value="White" />
                        </Trigger>
                        <Trigger Property="IsPressed" Value="True">
                            <Setter TargetName="Bd" Property="Background" Value="#4F4F56" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!--  ===== 关闭按钮样式(悬停红色) =====  -->
    <Style
        x:Key="CloseButtonStyle"
        BasedOn="{StaticResource TitleBarButtonStyle}"
        TargetType="Button">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border
                        x:Name="Bd"
                        Background="Transparent"
                        SnapsToDevicePixels="True">
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="Bd" Property="Background" Value="#E81123" />
                            <Setter Property="Foreground" Value="White" />
                        </Trigger>
                        <Trigger Property="IsPressed" Value="True">
                            <Setter TargetName="Bd" Property="Background" Value="#F1707A" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!--  ===== BaseWindow 主样式 =====  -->
    <Style TargetType="{x:Type local:BaseWindow}">
        <!--  默认属性  -->
        <Setter Property="Background" Value="#1E1E1E" />
        <Setter Property="Foreground" Value="#CCCCCC" />
        <Setter Property="WindowStyle" Value="None" />
        <Setter Property="AllowsTransparency" Value="True" />
        <Setter Property="ResizeMode" Value="CanResize" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="Header" Value="My Application" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:BaseWindow}">
                    <!--  外层容器:阴影 + 圆角  -->
                    <Border
                        x:Name="PART_OuterBorder"
                        Margin="6"
                        Background="{TemplateBinding Background}"
                        BorderBrush="#3F3F46"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        CornerRadius="8"
                        SnapsToDevicePixels="True">

                        <Border.Effect>
                            <DropShadowEffect
                                BlurRadius="12"
                                Opacity="0.5"
                                ShadowDepth="2"
                                Color="#000000" />
                        </Border.Effect>

                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="40" />
                                <RowDefinition Height="*" />
                            </Grid.RowDefinitions>

                            <!--  ========== 标题栏 ==========  -->
                            <Border
                                x:Name="PART_TitleBar"
                                Grid.Row="0"
                                Background="#2D2D30"
                                CornerRadius="8,8,0,0">

                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*" />
                                        <ColumnDefinition Width="Auto" />
                                    </Grid.ColumnDefinitions>

                                    <!--  应用图标  -->
                                    <Path
                                        Grid.Column="0"
                                        Width="14"
                                        Height="14"
                                        Margin="14,0,0,0"
                                        VerticalAlignment="Center"
                                        Data="M7,0 L14,7 L7,14 L0,7 Z"
                                        Fill="#569CD6"
                                        Stretch="Uniform" />

                                    <!--  标题文字  -->
                                    <TextBlock
                                        Grid.Column="1"
                                        Margin="8,0,0,0"
                                        VerticalAlignment="Center"
                                        FontFamily="Segoe UI"
                                        FontSize="13"
                                        FontWeight="SemiBold"
                                        Foreground="#FFFFFF"
                                        Text="{TemplateBinding Header}"
                                        TextTrimming="CharacterEllipsis" />

                                    <!--  窗口控制按钮  -->
                                    <StackPanel
                                        Grid.Column="2"
                                        VerticalAlignment="Top"
                                        Orientation="Horizontal">

                                        <!--  最小化  -->
                                        <Button
                                            x:Name="PART_MinButton"
                                            Width="46"
                                            Height="40"
                                            Style="{StaticResource TitleBarButtonStyle}"
                                            ToolTip="最小化">
                                            <Path
                                                Width="10"
                                                Height="1"
                                                Data="M0,0 L10,0"
                                                Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"
                                                StrokeEndLineCap="Round"
                                                StrokeStartLineCap="Round"
                                                StrokeThickness="1" />
                                        </Button>

                                        <!--  最大化 / 还原  -->
                                        <Button
                                            x:Name="PART_MaxButton"
                                            Width="46"
                                            Height="40"
                                            Style="{StaticResource TitleBarButtonStyle}"
                                            ToolTip="最大化">
                                            <!--  默认:正常状态图标  -->
                                            <Path
                                                x:Name="PART_MaxIcon"
                                                Width="10"
                                                Height="10"
                                                Data="M0.5,1.5 L9.5,1.5 L9.5,10.5 L0.5,10.5 Z"
                                                Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"
                                                StrokeEndLineCap="Round"
                                                StrokeStartLineCap="Round"
                                                StrokeThickness="1" />
                                        </Button>

                                        <!--  关闭  -->
                                        <Button
                                            x:Name="PART_CloseButton"
                                            Width="46"
                                            Height="40"
                                            Style="{StaticResource CloseButtonStyle}"
                                            ToolTip="关闭">
                                            <Path
                                                Width="10"
                                                Height="10"
                                                Data="M0,0 L10,10 M10,0 L0,10"
                                                Stretch="Uniform"
                                                Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"
                                                StrokeEndLineCap="Round"
                                                StrokeStartLineCap="Round"
                                                StrokeThickness="1.2" />
                                        </Button>

                                    </StackPanel>
                                </Grid>
                            </Border>

                            <!--  ========== 内容区 ==========  -->
                            <Border Grid.Row="1" CornerRadius="0,0,8,8">
                                <AdornerDecorator>
                                    <ContentPresenter Margin="0" />
                                </AdornerDecorator>
                            </Border>

                        </Grid>
                    </Border>

                    <ControlTemplate.Triggers>
                        <!--  最大化时去掉阴影和圆角  -->
                        <Trigger Property="WindowState" Value="Maximized">
                            <Setter TargetName="PART_OuterBorder" Property="Margin" Value="0" />
                            <Setter TargetName="PART_OuterBorder" Property="CornerRadius" Value="0" />
                            <Setter TargetName="PART_TitleBar" Property="CornerRadius" Value="0" />
                            <Setter TargetName="PART_OuterBorder" Property="Effect" Value="{x:Null}" />
                            <Setter TargetName="PART_MaxButton" Property="ToolTip" Value="还原" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

注册 Generic.xaml

在App.xaml.cs 或 AssemblyInfo.cs,一般都在AssemblyInfo.cs。

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None,
    ResourceDictionaryLocation.SourceAssembly
)]

确保存在,一般默认有。

主程序使用:

<mycontrol:BaseWindow
    x:Class="DemoApp.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:local="clr-namespace:DemoApp"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:mycontrol="https://hexin.pw/MyControl"
    Width="1000"
    Height="600"
    Header="我的超级窗口">
    <Grid>
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="40"
            Text="Hello Super Window" />
    </Grid>
</mycontrol:BaseWindow>
using MyControl;

namespace DemoApp
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : BaseWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}