WPF 自定义基础 Window Class 和样式

WPF Custom Base Window Class and Style

我有一个可以打开很多 windows 的应用程序,我希望所有 windows 看起来都一样。我正在覆盖默认的 Windows window chrome 样式并制作我自己的样式,因此任何打开的新 window(不包括消息框)都应该具有相同的 window风格。但是,无论我尝试什么似乎都行不通。我可以让它与一个 window 一起工作,但是当我想让它成为一种全局样式时,它总是崩溃或者根本无法正常工作。

这是我的代码:

WindowBaseStyle.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:MyProject.Styles"
                    xmlns:views="clr-namespace:Myproject.Views">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="GlobalStyles.xaml" />
    </ResourceDictionary.MergedDictionaries>
    <Style TargetType="{x:Type views:WindowBase}" BasedOn="{StaticResource {x:Type Window}}">
        <Setter Property="AllowsTransparency" Value="False" />
        <Setter Property="BorderBrush" Value="Red" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="WindowState" Value="Normal" />
        <Setter Property="WindowStyle" Value="SingleBorderWindow" />
        <Setter Property="WindowChrome.WindowChrome">
            <Setter.Value>
                <WindowChrome CaptionHeight="30"
                              UseAeroCaptionButtons="False"/>
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type views:WindowBase}">
                    <Border BorderBrush="Blue" BorderThickness="1" SnapsToDevicePixels="True">
                    <DockPanel Background="White" LastChildFill="True" >
                        <Grid Background="Blue" DockPanel.Dock="Top">
                            <StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center">
                                <Button Name="PART_SystemMenuButton" Command="{Binding MenuCommand}" Style="{DynamicResource SystemIconButton}">
                                    <Image Height="16" Width="16" Source="/Resources/icon.png" Stretch="Fill"/>
                                </Button>
                                <Viewbox Height="16" HorizontalAlignment="Stretch" Margin="14,2,0,0" >
                                    <TextBlock FontSize="12" Foreground="White" Text="{Binding Title,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" />
                                </Viewbox>
                            </StackPanel>
                                <StackPanel Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
                                    <Button x:Name="PART_MinimizeButton" Command="{Binding MinimizeCommand}" Height="30" Margin="0,0,0,0" ToolTip="Minimize" Width="45">
                                        <Image Source="/Resources/minimize.png" Stretch="None" />
                                        <Button.Style>
                                            <Style TargetType="{x:Type Button}">
                                                <Setter Property="Background" Value="#0079CB" />
                                                <Setter Property="Template">
                                                    <Setter.Value>
                                                        <ControlTemplate TargetType="{x:Type Button}">
                                                            <Border Background="{TemplateBinding Background}">
                                                                <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                                                            </Border>
                                                        </ControlTemplate>
                                                    </Setter.Value>
                                                </Setter>
                                                <Style.Triggers>
                                                    <Trigger Property="IsMouseOver" Value="True">
                                                        <Setter Property="Background" Value="#64AEEC"/>
                                                    </Trigger>
                                                </Style.Triggers>
                                            </Style>
                                        </Button.Style>
                                    </Button>
                                    <Button x:Name="PART_MaximizeButton" Command="{Binding MaximizeCommand}" Height="30" Margin="0,0,0,0" Width="45">
                                        <Image>
                                            <Image.Style>
                                                <Style TargetType="{x:Type Image}">
                                                    <Style.Triggers>
                                                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Normal">
                                                            <Setter Property="Source" Value="/Resources/maximize.png" />
                                                            <Setter Property="Stretch" Value="None" />
                                                        </DataTrigger>
                                                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Maximized">
                                                            <Setter Property="Source" Value="/Resources/unmaximize.png" />
                                                            <Setter Property="Stretch" Value="None" />
                                                        </DataTrigger>
                                                    </Style.Triggers>
                                                </Style>
                                            </Image.Style>
                                        </Image>
                                        <Button.Style>
                                            <Style TargetType="{x:Type Button}">
                                                <Setter Property="Background" Value="#0079CB" />
                                                <Setter Property="Template">
                                                    <Setter.Value>
                                                        <ControlTemplate TargetType="{x:Type Button}">
                                                            <Border Background="{TemplateBinding Background}">
                                                                <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                                                            </Border>
                                                        </ControlTemplate>
                                                    </Setter.Value>
                                                </Setter>
                                                <Style.Triggers>
                                                    <Trigger Property="IsMouseOver" Value="True">
                                                        <Setter Property="Background" Value="#64AEEC"/>
                                                    </Trigger>
                                                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Normal">
                                                        <Setter Property="ToolTip" Value="Maximize" />
                                                    </DataTrigger>
                                                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Maximized">
                                                        <Setter Property="ToolTip" Value="Restore Down" />
                                                    </DataTrigger>
                                                </Style.Triggers>
                                            </Style>
                                        </Button.Style>
                                    </Button>
                                    <Button x:Name="PART_CloseButton" Command="{Binding CloseCommand}" Height="30" Margin="0,0,0,0" ToolTip="Close" Width="45" >
                                        <Image Source="/Resources/close.png" Stretch="None" />
                                        <Button.Style>
                                            <Style TargetType="{x:Type Button}">
                                                <Setter Property="Background" Value="#0079CB" />
                                                <Setter Property="Template">
                                                    <Setter.Value>
                                                        <ControlTemplate TargetType="{x:Type Button}">
                                                            <Border Background="{TemplateBinding Background}">
                                                                <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                                                            </Border>
                                                        </ControlTemplate>
                                                    </Setter.Value>
                                                </Setter>
                                                <Style.Triggers>
                                                    <Trigger Property="IsMouseOver" Value="True">
                                                        <Setter Property="Background" Value="Red"/>
                                                    </Trigger>
                                                </Style.Triggers>
                                            </Style>
                                        </Button.Style>
                                    </Button>
                                </StackPanel>
                            </StackPanel>
                        </Grid>
                        <!-- this ContentPresenter automatically binds to the content of the window -->
                        <ContentPresenter />
                    </DockPanel>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

WindowBase.cs

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;

namespace MyProject.Views
{
    [TemplatePart(Name = "PART_MinimizeButton", Type = typeof(Button))]
    [TemplatePart(Name = "PART_MaximizeButton", Type = typeof(Button))]
    [TemplatePart(Name = "PART_CloseButton", Type = typeof(Button))]
    [TemplatePart(Name = "PART_SystemMenuButton", Type = typeof(Button))]
    public class WindowBase: Window
    {
        static WindowBase()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomWindow), new FrameworkPropertyMetadata(typeof(CustomWindow)));
        }

        public WindowBase()
        {
            Loaded += (sender, evnt) =>
            {
                var MinimizeButton = (Button)Template.FindName("PART_MinimizeButton", this);
                var MaximizeButton = (Button)Template.FindName("PART_MaximizeButton", this);
                var CloseButton = (Button)Template.FindName("PART_CloseButton", this);
                var SystemMenuButton = (Button)Template.FindName("PART_SystemMenuButton", this);

                MinimizeButton.Click += (s, e) => WindowState = WindowState.Minimized;
                MaximizeButton.Click += (s, e) => WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
                CloseButton.Click += (s, e) => Close();
                SystemMenuButton.Click += (s, e) => SystemCommands.ShowSystemMenu(this, GetMousePosition());
            };
        }
    }
}

Window1.xaml

<local:WindowBase x:Class="MyProject.Views.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MyProject.Views"
        Height="750"
        Width="1125">
    <Grid>
    </Grid>
</local:WindowBase>

Window1.xaml.cs

using System.Windows;

namespace MyProject.Views
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1: WindowBase
    {
        public Window1()
        {
            InitializeComponent();
        }
    }
}    

我总体上遵循 MVVM 模式,而且从我在网上看到的所有文章和视频中的大部分内容来看,他们都遵循这种基本方法并且他们说这一切都有效,但我似乎无法让它工作。

另外请注意,每当我将自定义 window 控件添加到 Window1.xaml 文件时,它会破坏设计器并显示它是 "Invalid Markup"

另请注意,我将 "WindowBaseStyle" 资源字典作为合并资源字典添加到 App.xaml 文件中。

非常感谢任何帮助!!谢谢

好的,正如我们在评论中讨论的那样,似乎解决您描述的问题的最快方法是使用 StaticResource 从资源字典中获取 window 的样式(或为 windows 创建一个隐式样式。)我质疑 CustomWindow 的作用,因为我认为这可能会导致您的默认样式覆盖出现问题。 (记住:如果你走无形控制路线并尝试使用 DefaultStyleKeyProperty 覆盖,你必须在该控制的每个子 class 上执行此操作。)

但是,我认为做这样的事情会让你可以重复使用由视图模型驱动的多个 windows 管道...

PopupHost

A class 将派生自您的自定义 window。此代码提供以下行为:

  1. 允许视图模型将自己标记为已达到目的,从而导致 window 关闭。
  2. 允许附加属性由可以影响 window 出现方式的单个视图指定 on-screen,例如window 标题。
  3. 可以扩展以通知显示的项目用户已尝试关闭 window,允许执行 interception/cancellation 或清理操作。

代码:

public class PopupHost : Window
{
    private readonly AwaitableViewModelBase _viewModel;

    public PopupHost(Window owner, AwaitableViewModelBase viewModel, string dataTemplateKey = null)
    {
        Owner = owner;
        _viewModel = viewModel;

        // Wrap the content in another presenter -- makes it a little easier to get to in order to look for attached properties.
        var contentPresenter = new ContentPresenter
        {
            Content = viewModel
        };

        if (!string.IsNullOrWhiteSpace(dataTemplateKey))
            contentPresenter.ContentTemplate = (DataTemplate) FindResource(dataTemplateKey);

        Content = contentPresenter;

        Task.Run(async () =>
        {
            await viewModel.Task;
            Dispatcher.Invoke(Close);
        });

        Closed += ClosedHandler;

        ApplyTemplate();

        // Grab attached property values from the user control (or whatever element... you just need to find the descendant)
        var contentElement = FindDescendantWithNonDefaultPropertyValue(contentPresenter, PopupWindowProperties.TitleProperty);
        if (contentElement != null)
        {
            var binding = new Binding { Source = contentElement, Path = new PropertyPath(PopupWindowProperties.TitleProperty) };
            SetBinding(TitleProperty, binding);
        }
    }

    private void ClosedHandler(object sender, EventArgs args)
    {
        _viewModel?.Cancel();
        Closed -= ClosedHandler;
    }

    private static Visual FindDescendant(Visual element, Predicate<Visual> predicate)
    {
        if (element == null)
            return null;

        if (predicate(element))
            return element;

        Visual foundElement = null;
        (element as FrameworkElement)?.ApplyTemplate();

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
        {
            var visual = VisualTreeHelper.GetChild(element, i) as Visual;
            foundElement = FindDescendant(visual, predicate);
            if (foundElement != null)
                break;
        }

        return foundElement;
    }

    private static Visual FindDescendantWithNonDefaultPropertyValue(Visual element, DependencyProperty dp)
    {
        return FindDescendant(element, e => !(dp.GetMetadata(e).DefaultValue ?? new object()).Equals(e.GetValue(dp)));
    }
}

弹出窗口属性

只是一个愚蠢的 object,仅包含附加属性,因此您的视图可以将一些信息传达给 window。

public static class PopupWindowProperties
{
    public static readonly DependencyProperty TitleProperty = DependencyProperty.RegisterAttached("Title", typeof(string), typeof(PopupWindowProperties), new FrameworkPropertyMetadata(string.Empty));

    public static void SetTitle(UIElement element, string value) => element.SetValue(TitleProperty, value);

    public static string GetTitle(UIElement element) => element.GetValue(TitleProperty) as string;
}

AwaitableViewModelBase

一个具有 TaskCompletionSource 的简单抽象视图模型。这允许弹出 window 和视图模型协调关闭。

public abstract class AwaitableViewModelBase : ViewModelBase
{
    protected TaskCompletionSource<bool> TaskCompletionSource { get; set; }

    public Task<bool> Task => TaskCompletionSource?.Task;

    public void RegisterTaskCompletionSource(TaskCompletionSource<bool> tcs)
    {
        var current = TaskCompletionSource;
        if (current != null && current.Task.Status == TaskStatus.Running)
            throw new InvalidOperationException();

        TaskCompletionSource = tcs;
    }

    public virtual void Cancel() => SetResult(false);

    protected void SetResult(bool result) => TaskCompletionSource?.TrySetResult(result);
}

窗口服务

最后但同样重要的是,可以呈现请求的视图和视图模型的简单服务。您可以为视图模型使用隐式 DataTemplates,或提供您希望使用的模板的特定 x:Key 值。请注意 await 在这里实际上没有做任何事情,因为 ShowDialog 块。我们 return bool 因为它可以用来轻松识别用户是否在模态上点击了 OKCancel

public class WindowService
{
    public async Task<bool> ShowModalAsync(AwaitableViewModelBase viewModel, string dataTemplateKey = null)
    {
        var tcs = new TaskCompletionSource<bool>();
        viewModel.RegisterTaskCompletionSource(tcs);

        Application.Current.Dispatcher.Invoke(() =>
        {
            var currentWindow = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive) ?? Application.Current.MainWindow;
            var window = new PopupHost(currentWindow, viewModel, dataTemplateKey);
            window.ShowDialog();
        });

        return await viewModel.Task;
    }
}