在WPF中更换默认模板的部分

36

有没有一种“最佳实践”的方法来替换默认模板的部分内容?当前使用情况是树状视图。在默认情况下,树状视图有小三角形图案用于展开和折叠。

我知道如果替换整个控件模板就可以替换这些内容,如下面的代码所示。不确定是否有一种方法“保留所有默认,只更改XY”。这不是一个样式,我基本上需要替换现有控件模板的一部分。

为了说明,看一下下面的XAML。第一个较小的块是我想要适应的相关XAML。

更大的第二部分和第三部分基本上是默认模板的副本,只是为了管理从一开始就“更改”了的部分。

是否有更好的方法来完成这个任务,避免使用第二部分中这么长而且容易混淆的XAML代码?

        <ResourceDictionary 
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >





  <Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton">
    <Setter Property="Focusable" Value="False"/>
     <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="ToggleButton">
          <Grid
            Width="15"
            Height="13"
            Background="Transparent">
            <Path x:Name="ExpandPath"
              HorizontalAlignment="Left" 
              VerticalAlignment="Center" 
              Margin="1,1,1,1"
              Fill="Black"
              Data="M 4 0 L 8 4 L 4 8 Z"/>
          </Grid>
          <ControlTemplate.Triggers>
            <Trigger Property="IsChecked"
                 Value="True">
              <Setter Property="Data"
                  TargetName="ExpandPath"
                  Value="M 0 4 L 8 4 L 4 8 Z"/>
            </Trigger>
          </ControlTemplate.Triggers>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type TreeViewItem}">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition MinWidth="19"
                    Width="Auto"/>
          <ColumnDefinition Width="Auto"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition/>
        </Grid.RowDefinitions>
        <ToggleButton x:Name="Expander"
                Style="{StaticResource ExpandCollapseToggleStyle}"
                IsChecked="{Binding Path=IsExpanded,
                            RelativeSource={RelativeSource TemplatedParent}}"
                ClickMode="Press"/>
        <Border Name="Bd"
            Grid.Column="1"
            Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}"
            Padding="{TemplateBinding Padding}">
          <ContentPresenter x:Name="PART_Header"
                    ContentSource="Header"
                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
        </Border>
        <ItemsPresenter x:Name="ItemsHost"
                Grid.Row="1"
                Grid.Column="1"
                Grid.ColumnSpan="2"/>
      </Grid>
      <ControlTemplate.Triggers>
        <Trigger Property="IsExpanded"
             Value="false">
          <Setter TargetName="ItemsHost"
              Property="Visibility"
              Value="Collapsed"/>
        </Trigger>
        <Trigger Property="HasItems"
             Value="false">
          <Setter TargetName="Expander"
              Property="Visibility"
              Value="Hidden"/>
        </Trigger>
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="HasHeader"
                   Value="false"/>
            <Condition Property="Width"
                   Value="Auto"/>
          </MultiTrigger.Conditions>
          <Setter TargetName="PART_Header"
              Property="MinWidth"
              Value="75"/>
        </MultiTrigger>
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="HasHeader"
                   Value="false"/>
            <Condition Property="Height"
                   Value="Auto"/>
          </MultiTrigger.Conditions>
          <Setter TargetName="PART_Header"
              Property="MinHeight"
              Value="19"/>
        </MultiTrigger>
        <Trigger Property="IsSelected"
             Value="true">
          <Setter TargetName="Bd"
              Property="Background"
              Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
          <Setter Property="Foreground"
              Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
        </Trigger>
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="IsSelected"
                   Value="true"/>
            <Condition Property="IsSelectionActive"
                   Value="false"/>
          </MultiTrigger.Conditions>
          <Setter TargetName="Bd"
              Property="Background"
              Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
          <Setter Property="Foreground"
              Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        </MultiTrigger>
        <Trigger Property="IsEnabled"
             Value="false">
          <Setter Property="Foreground"
              Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>
  </Setter.Value>
</Setter>

4个回答

26

很遗憾,我认为你需要替换整个模板:

来自MSDN:http://msdn.microsoft.com/en-us/library/aa970773.aspx

Windows Presentation Foundation(WPF)中的控件具有包含该控件的可视树的ControlTemplate。通过修改该控件的ControlTemplate,可以更改控件的结构和外观。没有办法仅替换控件可视树的一部分;要更改控件的可视树,必须将控件的Template属性设置为其新的完整ControlTemplate。


89
这真的很糟糕吗?WPF有许多很酷的功能,但无法通过Setters来定位、替换或稍微修改模板部件这一事实真的让我感到不爽。 - jpierson
1
模板没有类似于BasedOn属性之类的东西吗? - Ingó Vals
不幸的是,它不行。 - Stephen Drew
4
已经过去3.5年了,但我认为我有一个初步的解决方案 :) - Rocklan
虽然您无法覆盖模板本身,但在模板内部,您可以引用后来在每个控件上定义的DynamicResource。这使您可以以相当可管理的方式覆盖属性。 - Nick Fisher

11

实际上有一种方法(有点类似)。您可以创建自己的自定义控件并重写OnApplyTemplate函数以动态更改样式。

例如,像这样创建一个自定义控件(我在Silverlight中执行此操作,但我认为它都是相同的):

namespace SilverlightClassLibrary1
{
    public class MyButton: Button
    {
        public string BackgroundColor { get; set; }

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

            if (BackgroundColor != null)
            {
                Rectangle r = this.GetTemplateChild("BackgroundGradient") as Rectangle;

                if (r != null)
                {
                    r.Fill = new SolidColorBrush(Color.FromArgb(255, 
                        Convert.ToByte(BackgroundColor.Substring(1,2),16),
                        Convert.ToByte(BackgroundColor.Substring(3,2),16),
                        Convert.ToByte(BackgroundColor.Substring(5,2),16)));
                }
            }
        }
    }
}

有趣的部分是 GetTemplateChild 方法,它正在查找名为“BackgroundGradient”的矩形控件。(顺便说一句,如果您还没有这样做,最好在一个单独的项目中定义自定义控件,因此请创建一个新的"Silverlight类库"项目,并将其放入该项目中。)

然后添加一个新的资源字典文件并覆盖控件模板,确保您有一个名为“BackgroundGradient”的矩形。 在这种情况下,我们使用了标准按钮控件模板,我对它进行了一些裁剪:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:custom="clr-namespace:SilverlightClassLibrary1;assembly=SilverlightClassLibrary1">

    <Style TargetType="custom:MyButton">
        <Setter Property="Background" Value="#FF1F3B53"/>
        <Setter Property="Foreground" Value="#FF000000"/>
        <Setter Property="Padding" Value="3"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="BorderBrush">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FFA3AEB9" Offset="0"/>
                    <GradientStop Color="#FF8399A9" Offset="0.375"/>
                    <GradientStop Color="#FF718597" Offset="0.375"/>
                    <GradientStop Color="#FF617584" Offset="1"/>
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Grid>
                        <Border x:Name="Background" CornerRadius="3" Background="White" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
                            <Grid Background="{TemplateBinding Background}"  Margin="1">
                                <Border Opacity="0"  x:Name="BackgroundAnimation" Background="#FF448DCA" />
                                <Rectangle x:Name="BackgroundGradient" Fill="White" >
                                </Rectangle>
                            </Grid>
                        </Border>
                        <ContentPresenter
                              x:Name="contentPresenter"
                              Content="{TemplateBinding Content}"
                              ContentTemplate="{TemplateBinding ContentTemplate}"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              Margin="{TemplateBinding Padding}"/>
                        <Rectangle x:Name="DisabledVisualElement" RadiusX="3" RadiusY="3" Fill="#FFFFFFFF" Opacity="0" IsHitTestVisible="false" />
                        <Rectangle x:Name="FocusVisualElement" RadiusX="2" RadiusY="2" Margin="1" Stroke="#FF6DBDD1" StrokeThickness="1" Opacity="0" IsHitTestVisible="false" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

所以现在你可以声明一个按钮控件,并覆盖样式(如果你想的话):

<UserControl x:Class="SilverlightApplication1.MainPage"
            ...
            xmlns:custom="clr-namespace:SilverlightClassLibrary1;assembly=SilverlightClassLibrary1">

        <custom:MyButton>Normal Button 1</custom:MyButton>
        <custom:MyButton>Normal Button 2</custom:MyButton>

        <custom:MyButton BackgroundColor="#8888cc">Customized Background</custom:MyButton>

我相信你甚至可以更聪明,通过传递资源名称或样式名称并动态加载它。

然后,您需要将资源文件包含在应用程序中:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="SilverlightApplication1.App"
             >
    <Application.Resources>
        <ResourceDictionary >
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Dictionary1.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

然后你会在你的 XAML 设计师中看到你的自定义属性的更改。


我可以确认这个方法救了我的命。我需要在XAML中使用MaterialDesign的可伸缩面板,而不必复制模板并随着任何更改来更新它。 - Brent Rittenhouse
很酷,很高兴能够帮到你。 - Rocklan

4

3

2020更新

我在寻找答案时遇到了这个问题,以下是我解决的方式:

要覆盖默认控件模板:使用Microsoft Blend > 通过设计师右键单击要覆盖的控件 > 编辑模板 > 编辑当前项编辑副本(如果您在多个地方使用相同的控件,例如我的情况下使用菜单项),将模板保存在资源字典中,并将其用作要覆盖的控件的动态资源。当然,按照您的意愿编辑模板。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接