编写「WindowAdorner」类为「Window」提供自定义窗口标题栏

This commit is contained in:
zengwenjie
2025-09-28 17:48:41 +08:00
parent 553dd56f92
commit 097f35406f
6 changed files with 367 additions and 1 deletions

View File

@@ -15,7 +15,10 @@
</local:ExMarkup.WindowChrome>
</Border>
<AdornerDecorator x:Name="decorator">
<UniformGrid>
<Button Content="打开抽屉" Click="Button_Click"/>
<Button Content="自定义窗口标题测试" Click="Button_Click_1"/>
</UniformGrid>
</AdornerDecorator>
</DockPanel>
</Window>

View File

@@ -8,6 +8,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Deedy.Testing;
namespace Deedy
{
@@ -31,5 +32,10 @@ namespace Deedy
//DrawerManager.ShowDrawer(this.decorator, new Border() { Background = Brushes.Red, MinWidth = 400, MinHeight = 400, HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch });
new ElegantWindow().ShowDialog();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
new WindowAdornerTest().ShowDialog();
}
}
}

View File

@@ -142,4 +142,68 @@
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type local:WindowAdorner}">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="HoverBrush" Value="Silver"/>
<Setter Property="Padding" Value="6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type deedy:WindowAdorner}">
<ControlTemplate.Resources>
<Style x:Key="controlButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ControlTemplate.Resources>
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="Root">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition />
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Border x:Name="IconView" Width="{Binding ActualHeight, ElementName=Root}">
<Image Margin="{TemplateBinding Padding}" VerticalAlignment="Center" HorizontalAlignment="Center"
Source="{Binding Icon, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
</Border>
<TextBlock x:Name="TitleBar" Grid.Column="1" FontSize="{TemplateBinding FontSize}" Foreground="{TemplateBinding Foreground}"
Margin="{TemplateBinding Padding}" VerticalAlignment="Center" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Text="{Binding Title, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<Border x:Name="Container" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<StackPanel x:Name="Controller" Grid.Column="3" Orientation="Horizontal">
<Button x:Name="Minimize" VerticalAlignment="Stretch" Style="{StaticResource controlButtonStyle}" Cursor="Hand"
Padding="{TemplateBinding Padding}" Width="{Binding ActualHeight, ElementName=Root}">
<Viewbox Stretch="Uniform">
<Path Width="1024" Height="1024" Stroke="{TemplateBinding Foreground}" Fill="{TemplateBinding Foreground}" Data="M917.333333 554.666667H106.666667a21.333333 21.333333 0 0 1 0-42.666667h810.666666a21.333333 21.333333 0 0 1 0 42.666667z"/>
</Viewbox>
</Button>
<Button x:Name="Maximize" VerticalAlignment="Stretch" Style="{StaticResource controlButtonStyle}" Cursor="Hand"
Padding="{TemplateBinding Padding}" Width="{Binding ActualHeight, ElementName=Root}">
<Viewbox Stretch="Uniform">
<Path Width="1024" Height="1024" Stroke="{TemplateBinding Foreground}" Fill="{TemplateBinding Foreground}" Data="M714.666667 256H138.666667a53.393333 53.393333 0 0 0-53.333334 53.333333v576a53.393333 53.393333 0 0 0 53.333334 53.333334h576a53.393333 53.393333 0 0 0 53.333333-53.333334V309.333333a53.393333 53.393333 0 0 0-53.333333-53.333333z m10.666666 629.333333a10.666667 10.666667 0 0 1-10.666666 10.666667H138.666667a10.666667 10.666667 0 0 1-10.666667-10.666667V309.333333a10.666667 10.666667 0 0 1 10.666667-10.666666h576a10.666667 10.666667 0 0 1 10.666666 10.666666z m213.333334-746.666666v565.333333a21.333333 21.333333 0 0 1-42.666667 0V138.666667a10.666667 10.666667 0 0 0-10.666667-10.666667H320a21.333333 21.333333 0 0 1 0-42.666667h565.333333a53.393333 53.393333 0 0 1 53.333334 53.333334z"/>
</Viewbox>
</Button>
<Button x:Name="CloseWin" VerticalAlignment="Stretch" Style="{StaticResource controlButtonStyle}" Cursor=""
Padding="{TemplateBinding Padding}" Width="{Binding ActualHeight, ElementName=Root}">
<Viewbox Stretch="Uniform">
<Path Width="1024" Height="1024" Stroke="{TemplateBinding Foreground}" Fill="{TemplateBinding Foreground}" Data="M542.173333 512l347.58-347.58a21.333333 21.333333 0 1 0-30.173333-30.173333L512 481.826667 164.42 134.246667a21.333333 21.333333 0 0 0-30.173333 30.173333L481.826667 512l-347.58 347.58a21.333333 21.333333 0 0 0 30.173333 30.173333L512 542.173333l347.58 347.58a21.333333 21.333333 0 0 0 30.173333-30.173333z"/>
</Viewbox>
</Button>
</StackPanel>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Shell;
namespace Deedy
{
/// <summary>
/// 按照步骤 1a 或 1b 操作,然后执行步骤 2 以在 XAML 文件中使用此自定义控件。
///
/// 步骤 1a) 在当前项目中存在的 XAML 文件中使用该自定义控件。
/// 将此 XmlNamespace 特性添加到要使用该特性的标记文件的根
/// 元素中:
///
/// xmlns:MyNamespace="clr-namespace:Deedy.Testing"
///
///
/// 步骤 1b) 在其他项目中存在的 XAML 文件中使用该自定义控件。
/// 将此 XmlNamespace 特性添加到要使用该特性的标记文件的根
/// 元素中:
///
/// xmlns:MyNamespace="clr-namespace:Deedy.Testing;assembly=Deedy.Testing"
///
/// 您还需要添加一个从 XAML 文件所在的项目到此项目的项目引用,
/// 并重新生成以避免编译错误:
///
/// 在解决方案资源管理器中右击目标项目,然后依次单击
/// “添加引用”->“项目”->[浏览查找并选择此项目]
///
///
/// 步骤 2)
/// 继续操作并在 XAML 文件中使用控件。
///
/// <MyNamespace:WindowAdorner/>
///
/// </summary>
[ContentProperty("Child")]
[TemplatePart(Name = "IconView", Type = typeof(Border))]
[TemplatePart(Name = "TitleBar", Type = typeof(TextBlock))]
[TemplatePart(Name = "Minimize", Type = typeof(Button))]
[TemplatePart(Name = "Maximize", Type = typeof(Button))]
[TemplatePart(Name = "CloseWin", Type = typeof(Button))]
[TemplatePart(Name = "Controller", Type = typeof(Panel))]
[TemplatePart(Name = "Container", Type = typeof(Decorator))]
public class WindowAdorner : Control
{
static WindowAdorner()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(WindowAdorner), new FrameworkPropertyMetadata(typeof(WindowAdorner)));
}
public Brush HoverBrush
{
get { return (Brush)GetValue(HoverBrushProperty); }
set { SetValue(HoverBrushProperty, value); }
}
public static readonly DependencyProperty HoverBrushProperty =
DependencyProperty.Register("HoverBrush", typeof(Brush), typeof(WindowAdorner), new PropertyMetadata(Brushes.Silver));
public UIElement Child
{
get { return (UIElement)GetValue(ChildProperty); }
set { SetValue(ChildProperty, value); }
}
public static readonly DependencyProperty ChildProperty =
DependencyProperty.Register("Child", typeof(UIElement), typeof(WindowAdorner), new PropertyMetadata(null, (d, e) =>
{
WindowAdorner windowDecorator = (WindowAdorner)d;
if (e.NewValue != null)
{
if (windowDecorator.Container != null)
windowDecorator.Container.Child = e.NewValue as UIElement;
}
}));
private Panel? Controller;
private Decorator? Container;
private Button? Minimize;
private Button? Maximize;
private Button? CloseWin;
private TextBlock? TitleBar;
private Border? IconView;
private Window? Target;
private WindowChrome? Chrome;
//private Decorator? Decorator;
private static bool IsNeedOverrideMetadata = true;
public WindowAdorner() { }
public void OnAttached()
{
FrameworkElement? parent = this.Parent as FrameworkElement;
Window? target = parent as Window;
while (parent != null && target == null)
{
parent = parent.Parent as FrameworkElement;
target = parent as Window;
}
//这是第【1】步
if (target == null && !DesignerProperties.GetIsInDesignMode(this)) throw new ArgumentNullException("窗体装饰器[WindowDecorator]对象不可以附加到一个空的Window对象上");
//if (decorator == null) throw new ArgumentNullException("窗体装饰器[WindowDecorator]对象需要一个用于放置的容器!");
this.Target = target;
//this.Decorator = decorator;
//this.Decorator.Child = this;
if (this.Target == null) return;
this.Target.StateChanged += Target_StateChanged;
this.Target.Loaded += Target_Loaded;
if (IsNeedOverrideMetadata)
{
Window.WindowStyleProperty.OverrideMetadata(this.Target.GetType(), new FrameworkPropertyMetadata((d, e) => this.AdjustVisual()));
Window.ResizeModeProperty.OverrideMetadata(this.Target.GetType(), new FrameworkPropertyMetadata((d, e) => this.AdjustVisual()));
IsNeedOverrideMetadata = false;
}
}
public override void OnApplyTemplate()
{
this.OnDetaching();
base.OnApplyTemplate();
this.OnAttached();
//这是第【2】步
this.Controller = GetTemplateChild("Controller") as Panel;
this.Container = GetTemplateChild("Container") as Decorator;
this.Minimize = GetTemplateChild("Minimize") as Button;
this.Maximize = GetTemplateChild("Maximize") as Button;
this.CloseWin = GetTemplateChild("CloseWin") as Button;
this.IconView = GetTemplateChild("IconView") as Border;
this.TitleBar = GetTemplateChild("TitleBar") as TextBlock;
if (this.Container != null) this.Container.Child = this.Child;
}
private void Target_Loaded(object sender, RoutedEventArgs e)
{
if (this.Background == BackgroundProperty.DefaultMetadata.DefaultValue) this.SetBinding(BackgroundProperty, new Binding() { Source = this.Target, Path = new PropertyPath(BackgroundProperty.Name) });
if (this.BorderBrush == BorderBrushProperty.DefaultMetadata.DefaultValue) this.SetBinding(BorderBrushProperty, new Binding() { Source = this.Target, Path = new PropertyPath(BorderBrushProperty.Name) });
//这是第【3】步
this.Chrome = new WindowChrome() { CaptionHeight = this.ActualHeight };
WindowChrome.SetWindowChrome(this.Target, this.Chrome);
WindowChrome.SetIsHitTestVisibleInChrome(this.Container, true);
WindowChrome.SetIsHitTestVisibleInChrome(this.Controller, true);
//if (this.Container != null) this.Container.Child = this.Child;
if (this.Controller != null)
{
foreach (var c in this.Controller.Children)
{
Button? button = c as Button;
button?.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.CommandButton_Click));
button?.AddHandler(Button.MouseEnterEvent, new RoutedEventHandler((s, e) => ((Button)e.Source).Background = this.HoverBrush));
button?.AddHandler(Button.MouseLeaveEvent, new RoutedEventHandler((s, e) => ((Button)e.Source).Background = Brushes.Transparent));
}
}
this.AdjustVisual();
this.AdjustMargin();
}
public void OnDetaching()
{
if (this.Target != null)
{
this.Target.StateChanged -= this.Target_StateChanged;
this.Target.Loaded -= this.Target_Loaded;
}
}
private void AdjustVisual()
{
if (this.Target != null)
{
if (this.Controller != null) this.Controller.Visibility = Visibility.Visible;
if (this.Minimize != null) this.Minimize.Visibility = Visibility.Visible;
if (this.Maximize != null) this.Maximize.Visibility = Visibility.Visible;
if (this.CloseWin != null) this.CloseWin.Visibility = Visibility.Visible;
if (this.IconView != null) this.IconView.Visibility = Visibility.Visible;
if (this.TitleBar != null) this.TitleBar.Visibility = Visibility.Visible;
if (this.Target.ResizeMode == ResizeMode.NoResize)
{
if (this.Minimize != null) this.Minimize.Visibility = Visibility.Collapsed;
if (this.Maximize != null) this.Maximize.Visibility = Visibility.Collapsed;
}
if (this.Target.ResizeMode == ResizeMode.CanMinimize)
{
if (this.Maximize != null) this.Maximize.Visibility = Visibility.Collapsed;
}
if (this.Target.WindowStyle == WindowStyle.None)
{
if (this.IconView != null) this.IconView.Visibility = Visibility.Collapsed;
if (this.TitleBar != null) this.TitleBar.Visibility = Visibility.Collapsed;
if (this.Controller != null) this.Controller.Visibility = Visibility.Collapsed;
}
if (this.Target.WindowStyle == WindowStyle.ToolWindow)
{
if (this.Controller != null) this.Controller.Visibility = Visibility.Collapsed;
}
}
}
private void AdjustMargin()
{
if (this.Target != null)
{
var winContent = this.Target.Content as FrameworkElement;
if (winContent == null) return;
if (this.Target.WindowState == WindowState.Maximized)
winContent.Margin = new Thickness(8);
else winContent.Margin = new Thickness(0);
}
}
private void Target_StateChanged(object? sender, EventArgs e)
{
this.AdjustMargin();
}
private void CommandButton_Click(object sender, RoutedEventArgs e)
{
Button? button = sender as Button;
if (button != null)
{
switch (button.Name)
{
case "Minimize":
if (this.Target != null) this.Target.WindowState = WindowState.Minimized;
break;
case "Maximize":
{
if (this.Target != null)
{
if (this.Target.WindowState == WindowState.Maximized || this.Target.WindowState == WindowState.Minimized)
this.Target.WindowState = WindowState.Normal;
else this.Target.WindowState = WindowState.Maximized;
}
}
break;
default:
this.Target?.Close();
break;
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
<Window x:Class="Deedy.Testing.WindowAdornerTest"
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:local="clr-namespace:Deedy.Testing"
xmlns:deedy="clr-namespace:Deedy"
mc:Ignorable="d"
Title="WindowAdornerTest" Height="450" Width="800" Background="Gray" d:WindowStyle="None" Foreground="White">
<DockPanel>
<deedy:WindowAdorner Height="40" DockPanel.Dock="Top" HoverBrush="Wheat">
<Button Content="button" HorizontalAlignment="Center"/>
</deedy:WindowAdorner>
<Border Background="White"/>
</DockPanel>
</Window>

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Deedy.Testing
{
/// <summary>
/// WindowAdornerTest.xaml 的交互逻辑
/// </summary>
public partial class WindowAdornerTest : Window
{
public WindowAdornerTest()
{
InitializeComponent();
}
}
}