WPF应用全局自定义标题栏的最佳实现方案问询
WPF自定义标题栏复用最佳实践
问题描述
我给WPF应用的所有窗口都设计了自定义标题栏,但目前是在每个窗口中硬编码实现的。希望将其封装为可复用模板,能在不同窗口甚至MessageBox中重复使用,求最佳实践方案。
以下是其中一个窗口的示例代码:
<Window x:Class="MyApp.Configuration.Windows.NotMainWindow" 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" Title="Some text" Height="113.868" Width="405.84" Background="#FFE5E5E5" WindowStyle="None" > <WindowChrome.WindowChrome> <WindowChrome CaptionHeight="34" /> </WindowChrome.WindowChrome> <Window.Resources> <ResourceDictionary Source="pack://application:,,,/somefolder.somefolder;component/Resources/TitleBarIconsStyle.xaml" /> </Window.Resources> <Border x:Name="MainWindowBorder" BorderThickness="0" > <Grid x:Name="parentContainer"> <Grid.RowDefinitions> <RowDefinition Height ="Auto"/> <RowDefinition Height ="*"/> </Grid.RowDefinitions> <!--Window chrome--> <Grid Grid.Row="0" Height="30" Background="#585856"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center"> <!--App icon--> <Image Source="/Resources/icon.png" Width="18" Margin="2" HorizontalAlignment="Left" VerticalAlignment="Center" /> <TextBlock FontFamily="Arial" Margin="4 3 0 0" Text="Some text"/> </StackPanel> <!--Caption buttons--> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" > <Button Style="{StaticResource MinimizeButtonStyle}" WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Minimize" Click="CommandBinding_Executed_Minimize"/> <Button x:Name="RestoreButton" Visibility="Collapsed" Style="{StaticResource RestoreButtonStyle}" Click="CommandBinding_Executed_Restore" WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Restore"/> <Button x:Name="MaximizeButton" Visibility="Visible" Style="{StaticResource MaximizeButtonStyle}" Click="CommandBinding_Executed_Maximize" WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Maximize" /> <Button x:Name="CloseButton" Style="{StaticResource CloseButtonStyle}" WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close" Click="CommandBinding_Executed_Close"/> </StackPanel> </Grid> <Grid Grid.Row="1"> <TextBox x:Name="Input"/> <Button x:Name="OkButton" Content="Ok"/> </Grid> </Grid> </Border> </Window>
最佳实践方案
1. 将标题栏封装为UserControl(基础复用)
把标题栏的XAML和逻辑抽成独立的UserControl,每个窗口直接引用即可。
步骤1:创建CustomTitleBar.xaml
<UserControl x:Class="MyApp.Controls.CustomTitleBar" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" mc:Ignorable="d" Height="30"> <Grid Background="#585856"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center"> <Image Source="/Resources/icon.png" Width="18" Margin="2" HorizontalAlignment="Left" VerticalAlignment="Center" /> <TextBlock x:Name="TitleTextBlock" FontFamily="Arial" Margin="4 3 0 0"/> </StackPanel> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" > <Button x:Name="MinimizeBtn" Style="{StaticResource MinimizeButtonStyle}" WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Minimize"/> <Button x:Name="RestoreBtn" Visibility="Collapsed" Style="{StaticResource RestoreButtonStyle}" WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Restore"/> <Button x:Name="MaximizeBtn" Visibility="Visible" Style="{StaticResource MaximizeButtonStyle}" WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Maximize" /> <Button x:Name="CloseBtn" Style="{StaticResource CloseButtonStyle}" WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"/> </StackPanel> </Grid> </UserControl>
步骤2:UserControl后台代码添加依赖属性与事件
public partial class CustomTitleBar : UserControl { // 标题文本依赖属性 public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(CustomTitleBar), new PropertyMetadata(string.Empty)); public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } // 按钮点击事件,由父窗口处理 public event RoutedEventHandler MinimizeClicked; public event RoutedEventHandler RestoreClicked; public event RoutedEventHandler MaximizeClicked; public event RoutedEventHandler CloseClicked; public CustomTitleBar() { InitializeComponent(); DataContext = this; } private void MinimizeBtn_Click(object sender, RoutedEventArgs e) => MinimizeClicked?.Invoke(sender, e); private void RestoreBtn_Click(object sender, RoutedEventArgs e) => RestoreClicked?.Invoke(sender, e); private void MaximizeBtn_Click(object sender, RoutedEventArgs e) => MaximizeClicked?.Invoke(sender, e); private void CloseBtn_Click(object sender, RoutedEventArgs e) => CloseClicked?.Invoke(sender, e); // 更新最大化/还原按钮状态的方法 public void UpdateWindowState(WindowState state) { MaximizeBtn.Visibility = state == WindowState.Maximized ? Visibility.Collapsed : Visibility.Visible; RestoreBtn.Visibility = state == WindowState.Maximized ? Visibility.Visible : Visibility.Collapsed; } }
步骤3:窗口中引用自定义标题栏
<Window x:Class="MyApp.Configuration.Windows.NotMainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:MyApp.Controls" Title="Some text" Height="113.868" Width="405.84" Background="#FFE5E5E5" WindowStyle="None" StateChanged="Window_StateChanged"> <WindowChrome.WindowChrome> <WindowChrome CaptionHeight="34" /> </WindowChrome.WindowChrome> <Window.Resources> <ResourceDictionary Source="pack://application:,,,/somefolder.somefolder;component/Resources/TitleBarIconsStyle.xaml" /> </Window.Resources> <Border x:Name="MainWindowBorder" BorderThickness="0" > <Grid x:Name="parentContainer"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 复用自定义标题栏 --> <controls:CustomTitleBar Grid.Row="0" Title="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=Title}" MinimizeClicked="CommandBinding_Executed_Minimize" MaximizeClicked="CommandBinding_Executed_Maximize" RestoreClicked="CommandBinding_Executed_Restore" CloseClicked="CommandBinding_Executed_Close"/> <!-- 窗口内容 --> <Grid Grid.Row="1"> <TextBox x:Name="Input"/> <Button x:Name="OkButton" Content="Ok"/> </Grid> </Grid> </Border> </Window>
后台处理窗口状态变化:
private void Window_StateChanged(object sender, EventArgs e) { var titleBar = FindVisualChild<CustomTitleBar>(this); titleBar?.UpdateWindowState(WindowState); } private T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is T t) return t; var result = FindVisualChild<T>(child); if (result != null) return result; } return null; }
2. 封装自定义Window基类(进阶复用)
创建基类Window封装标题栏与通用逻辑,其他窗口直接继承该基类,无需重复编写标题栏代码。
步骤1:创建CustomWindow.xaml基类窗口
<Window x:Class="MyApp.Windows.CustomWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:MyApp.Controls" WindowStyle="None" StateChanged="Window_StateChanged"> <WindowChrome.WindowChrome> <WindowChrome CaptionHeight="34" /> </WindowChrome.WindowChrome> <Window.Resources> <ResourceDictionary Source="pack://application:,,,/somefolder.somefolder;component/Resources/TitleBarIconsStyle.xaml" /> </Window.Resources> <Border x:Name="MainWindowBorder" BorderThickness="0" > <Grid x:Name="parentContainer"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <controls:CustomTitleBar x:Name="CustomTitleBar" Grid.Row="0" Title="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=Title}" MinimizeClicked="MinimizeClicked" MaximizeClicked="MaximizeClicked" RestoreClicked="RestoreClicked" CloseClicked="CloseClicked"/> <!-- 内容占位符,子窗口填充自定义内容 --> <ContentPresenter Grid.Row="1" x:Name="WindowContent"/> </Grid> </Border> </Window>
步骤2:基类后台实现通用逻辑
public partial class CustomWindow : Window { public CustomWindow() => InitializeComponent(); private void MinimizeClicked(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized; private void MaximizeClicked(object sender, RoutedEventArgs e) => WindowState = WindowState.Maximized; private void RestoreClicked(object sender, RoutedEventArgs e) => WindowState = WindowState.Normal; private void CloseClicked(object sender, RoutedEventArgs e) => Close(); private void Window_StateChanged(object sender, EventArgs e) => CustomTitleBar.UpdateWindowState(WindowState); }
步骤3:子窗口继承基类
<local:CustomWindow x:Class="MyApp.Configuration.Windows.NotMainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MyApp.Windows" Title="Some text" Height="113.868" Width="405.84" Background="#FFE5E5E5"> <!-- 直接填充窗口内容,无需重复编写标题栏 --> <Grid> <TextBox x:Name="Input"/> <Button x:Name="OkButton" Content="Ok"/> </Grid> </local:CustomWindow>
3. 适配MessageBox的方案
系统MessageBox无法直接修改标题栏,需自行实现CustomMessageBox继承上述CustomWindow基类,封装为静态方法调用:
public class CustomMessageBox : CustomWindow { public CustomMessageBox(string message, string title) { Title = title; // 构建内容布局:文本块+确认/取消按钮 var contentGrid = new Grid { Margin = new Thickness(20) }; contentGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); contentGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); var messageBlock = new TextBlock { Text = message, Margin = new Thickness(0, 0, 0, 20), TextWrapping = TextWrapping.Wrap }; Grid.SetRow(messageBlock, 0); var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; var okBtn = new Button { Content = "确定", Width = 80, Margin = new Thickness(0, 0, 10, 0) }; okBtn.Click += (s, e) => { DialogResult = true; Close(); }; var cancelBtn = new Button { Content = "取消", Width = 80 }; cancelBtn.Click += (s, e) => { DialogResult = false; Close(); }; buttonPanel.Children.Add(okBtn); buttonPanel.Children.Add(cancelBtn); Grid.SetRow(buttonPanel, 1); contentGrid.Children.Add(messageBlock); contentGrid.Children.Add(buttonPanel); Content = contentGrid; } public static bool Show(string message, string title) { var msgBox = new CustomMessageBox(message, title); return msgBox.ShowDialog() == true; } }
调用时直接使用CustomMessageBox.Show("提示内容", "自定义标题");,自动复用自定义标题栏。
内容的提问来源于stack exchange,提问作者Hub
相关产品推荐
相关产品推荐

