You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 21:01:39