在 WPF 中,如果我想重新定义控件样式,在制作特殊按钮的示例中,我知道我可以使用 Visual Studio 提取模板的副本供我使用。
但是,当 Visual Studio 生成按钮样式时,我最终会得到一堆要定义的静态资源。在我的按钮示例中,我得到以下内容。
<SolidColorBrush x:Key="Button.Static.Background" Color="#FFDDDDDD"/>
<SolidColorBrush x:Key="Button.Static.Border" Color="#FF707070"/>
<SolidColorBrush x:Key="Button.MouseOver.Background" Color="#FFBEE6FD"/>
<SolidColorBrush x:Key="Button.MouseOver.Border" Color="#FF3C7FB1"/>
<SolidColorBrush x:Key="Button.Pressed.Background" Color="#FFC4E5F6"/>
<SolidColorBrush x:Key="Button.Pressed.Border" Color="#FF2C628B"/>
<SolidColorBrush x:Key="Button.Disabled.Background" Color="#FFF4F4F4"/>
<SolidColorBrush x:Key="Button.Disabled.Border" Color="#FFADB2B5"/>
<SolidColorBrush x:Key="Button.Disabled.Foreground" Color="#FF838383"/>
就我而言,我想保留所有标准按钮颜色。
有什么方法可以直接指向标准按钮控件使用的相同资源,而不是拥有所有这些额外的定义。
Button
是ContentControl
。因此,您不必覆盖 ControlTemplate
来更改布局。如果您想更改视觉状态行为,则覆盖 ControlTemplate
通常更有意义。但这是你不想做的。您想保留原始的视觉行为。
因为
Button
是一个 ContentControl
,所以您所要做的就是为 DataTemplate
属性定义一个 ContentControl.ContentTemplate
。优点是自定义按钮的客户端仍然可以定义复杂的内容定义。
以下示例展示了如何实现这一点,以使您的自定义按钮方便使用。
应用程序.xaml
将应用于按钮内部
DataTemplate
属性的 ContentPresenter.ContentTemplate
。
如果您不想将
DataTemplate
添加到应用程序的资源字典中,例如,因为您想将控件与库一起发送,那么您可以将其添加到任何 XAML ResourceDictionary
并将该资源文件嵌入到 DLL 中(构建动作)。然后在 ResourceManager
的帮助下检索资源。
<Application>
<Application.Resources>
<DataTemplate x:Key="{x:Static AdvancedButton.DefaultContentTemplateKey}">
<StackPanel>
<!-- Icon host -->
<ContentControl Content="{Binding RelativeSource={RelativeSource AncestorType=local:AdvancedButton}, Path=Icon}" />
<!--
Content host.
To preserve the original behavior of the Button.ContentTemplate,
we have to delegate Button.ContentTemplate value to the actual content host.
Otherwise, because this current template is already applied to
the original ContentPresenter of the Button,
setting Button.ContentTemplate externally would override/remove
this current DataTemplate.
Because we intentionally broke the link of the button's
ContentPresenter to the AdvancedButton (ContentPresenter host)
we have to explicitly wire the original properties
ContentTemplateSelector and ContentStringFormat to the new content host.
If we did not do this, the behavior would be completely broken
and setting those content related properties would have no effect.
-->
<ContentControl Content="{Binding}"
ContentTemplate="{Binding RelativeSource={RelativeSource AncestorType=local:AdvancedButton}, Path=ContentTemplate}"
ContentTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType=local:AdvancedButton}, Path=ContentTemplateSelector}"
ContentStringFormat="{Binding RelativeSource={RelativeSource AncestorType=local:AdvancedButton}, Path=ContentStringFormat}" />
</StackPanel>
</DataTemplate>
</Application.Resources>
</Application>
高级按钮.cs
public class AdvancedButton : Button
{
public static ResourceKey DefaultContentTemplateKey = new DataTemplateKey();
public object Icon
{
get => (object)GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
"Icon",
typeof(object),
typeof(AdvancedButton),
new PropertyMetadata(default));
public DataTemplate DefaultContentTemplate
{
get => (DataTemplate)GetValue(DefaultContentTemplateProperty);
private set => SetValue(DefaultContentTemplatePropertyKey, value);
}
private static readonly DependencyPropertyKey DefaultContentTemplatePropertyKey = DependencyProperty.RegisterReadOnly(
"DefaultContentTemplate",
typeof(DataTemplate),
typeof(AdvancedButton),
new PropertyMetadata(default));
public static readonly DependencyProperty DefaultContentTemplateProperty = DefaultContentTemplatePropertyKey.DependencyProperty;
protected override void OnInitialized(EventArgs e)
{
ContentPresenter cp;
base.OnInitialized(e);
if (TryFindResource(AdvancedButton.DefaultContentTemplateKey) is DataTemplate defaultContentTemplate)
{
this.DefaultContentTemplate = defaultContentTemplate;
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (TryFindChild(this, out ContentPresenter? contentPresenter))
{
// Setting the ContentPresenter!.ContentTemplate property directly
// will break the default binding between the ContentPresenter
// and its host (the button and its ContentTemplate property).
// We explicitly want to break that link because we have to manuall apply
// our default DataTemplate instead and then delegate the button's ContentTemplate value
// to the ContentControl inside the default template.
// Delegation happens via data binding in the default DataTemplate.
// The goal is to allow the button to behave as expected to the client code.
// This way the Button.ContentTemplate will behave as expected:
// it will (appear to) correctly template the Button.Content value.
contentPresenter!.ContentTemplate = this.DefaultContentTemplate;
}
}
private bool TryFindChild<TChild>(DependencyObject parent, out TChild? child) where TChild : DependencyObject
{
child = parent as TChild;
if (child is not null)
{
return true;
}
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int childIndex = 0; childIndex < childrenCount; childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
child = childElement as TChild;
if (child is not null || TryFindChild(childElement, out child))
{
return true;
}
}
return false;
}
}
MainWindow.xaml
<Window>
<AdvancedButton Content="Label text"
Icon="Set icon resource here" />
<!-- Providing a content template won't break the new AdvancedButton layout -->
<AdvancedButton Content="Label text"
Icon="Set icon resource here">
<AdvancedButton.ContentTemplate>
<!--
As commonly expected, this DataTemplate will only affect
the value of the Button.Content property.
But it will not override or default content template.
-->
<DataTemplate>
<TextBlock Text="{Binding}"
FontSize="48"
Foreground="Orange" />
</DataTemplate>
</AdvancedButton.ContentTemplate>
</AdvancedButton>
</Window>