WPF Custom/User 带有文本和图标的按钮控件。我应该选择哪一个?

WPF Custom/User Control for button with text and icon. Which one should I choose?

我想使用按钮的自定义控件重构以下代码。

Xaml 文件:LoginWindow.xaml

<Button Grid.Column="0" Style="{DynamicResource BlackLoginButton}" Click="btnLogin_Click" IsDefault="True">
    <DockPanel>
        <TextBlock Text="text1" VerticalAlignment="Center"/>
        <Image Source="../../../Assets/Images/apply.png" Style="{StaticResource ImageButton}" />
    </DockPanel>
</Button>

后面的代码:LoginWindow.xaml.cs

private void btnLogin_Click(object sender, EventArgs e)
{
    Login();
}

我特别想要这样的结构:

<local:CustomButton>
    Grid.Column="0"
    IsDefault="True"
    Style="{DynamicResource BlackLoginButton}"
    Click="btnLogin_Click"
    Text="ciao" 
    Image="../../../Assets/Images/apply.png">
</local:CustomButton>

我尝试同时使用自定义控件和用户控件。

这是我的UserControlxaml

<UserControl x:Class="foo.View.CustomUserControl.IconButton"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Button Style="{DynamicResource BlackLoginButton}">
        <DockPanel>
            <TextBlock VerticalAlignment="Center" Text="{Binding ElementName=Button, Path=Text}" />
            <Image Source="{Binding ElementName=Button, Path=Image}" Style="{StaticResource ImageButton}" />
        </DockPanel>
    </Button>
</UserControl>

后面的代码:

using System.Windows;
using System.Windows.Media;

namespace Mhira3D.View.CustomUserControl
{
    public partial class IconButton
    {
        public static DependencyProperty ClickProperty = DependencyProperty.Register("Click", typeof(RoutedEventHandler),
            typeof(IconButton));

        public static DependencyProperty ImageProperty = DependencyProperty.Register("Image", typeof(ImageSource),
            typeof(IconButton));

        public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(IconButton));

        public IconButton()
        {
            InitializeComponent();
        }

        public RoutedEventHandler Click
        {
            get { return (RoutedEventHandler) GetValue(ClickProperty); }
            set { SetValue(ClickProperty, value); }
        }

        public ImageSource Image
        {
            get { return (ImageSource) GetValue(ImageProperty); }
            set { SetValue(ImageProperty, value); }
        }

        public string Text
        {
            get { return this.GetValue(TextProperty) as string; }
            set { this.SetValue(TextProperty, value); }
        }
    }
}

这个结构的主要问题是我不能使用Button 属性(我没有继承按钮),比如我不能使用IsDefault.

我认为可以使用 CustomControl 来更好地使用按钮 属性,就像这样(在这个例子中我只放了 Image 属性):

public class IconButtonCustom : Button
{
    static IconButtonCustom()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(IconButtonCustom), new FrameworkPropertyMetadata(typeof(IconButtonCustom)));
    }
    public ImageSource Image
    {
        get { return GetValue(SourceProperty) as ImageSource; }
        set { SetValue(SourceProperty, value); }
    }
    public static readonly DependencyProperty SourceProperty =
      DependencyProperty.Register("Image", typeof(ImageSource), typeof(IconButtonCustom));
}

以及 Generic.xaml

中的样式
<Style TargetType="{x:Type customUserControl:IconButtonCustom}" BasedOn="{StaticResource BlackLoginButton}">
        <Setter Property="Template">
            <Setter.Value>
            <ControlTemplate TargetType="{x:Type customUserControl:IconButtonCustom}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <DockPanel>
                        <TextBlock VerticalAlignment="Center" Text="{Binding ElementName=Button, Path=Text}" />
                        <Image Source="{Binding ElementName=Button, Path=Image}" Style="{StaticResource ImageButton}" />
                    </DockPanel>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

现在我有一些问题:

  1. 以下哪种方法更适合创建我的自定义按钮?
  2. 使用这两种方法,我在映射点击函数时遇到问题,实际上我得到了 System.Windows.Markup.XamlParseException,因为 WPF 无法在函数 Click.[=56 中转换字符串 btnLogin_Click =]
  3. 我从未使用过 Generic.xaml,我的示例是否正确或应该在某些方面进行改进?

好的,所以大多数框架元素都有一个方便的花花公子 属性 称为 Tag,它存在于像这样的实例中,我们可以使用一种方法将某些东西搭载到模板中,而不必去声明额外的依赖属性等等。

因此,如果我们采用默认的 Button 样式模板(右键单击按钮 -> 编辑模板 -> 编辑副本),我们会在其中看到一个 ContentPresenter,它通常会传递任何 CLR对象。所以我们很乐意为您提供文本或您想要的任何其他内容。

我们现在有多种选择来实现您的目标。一种是简单地以这种方式(伪)传递你的两个元素;

<Button>
  <StackPanel Orientation="Horizontal">
     <TextBlock/>
     <Image/>
  </StackPanel>
</Button>

只是看起来有点乏味。所以我们进入 Style 模板选项,而不是对它做这样的事情;

<Style x:Key="SpecialButtonStyle" TargetType="{x:Type Button}">
    <!-- We set a default icon/image path for the instance one isn't defined. -->
    <Setter Property="Tag" Value="../../../Assets/Images/apply.png"/>
    <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
    <Setter Property="Background" Value="{StaticResource Button.Static.Background}"/>
    <Setter Property="BorderBrush" Value="{StaticResource Button.Static.Border}"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Padding" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
             <!-- This is where we've added an Image to our Button with it's source bound to Tag. PS - In everything else like Silverlight, WP, UWP, Embedded etc, it's just {TemplateBinding Tag} -->
                        <Image Grid.Column="1" Source="{Binding Path=Tag, RelativeSource={RelativeSource TemplatedParent}}"/>
                    </Grid>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsDefaulted" Value="true">
                        <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter Property="Background" TargetName="border" Value="{StaticResource Button.MouseOver.Background}"/>
                        <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.MouseOver.Border}"/>
                    </Trigger>
                    <Trigger Property="IsPressed" Value="true">
                        <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Pressed.Background}"/>
                        <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Pressed.Border}"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Disabled.Background}"/>
                        <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Disabled.Border}"/>
                        <Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{StaticResource Button.Disabled.Foreground}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

注意添加的 Image 元素,其 Source 设置为 {TemplateBinding Tag} 以及顶部的 setter 指定可能是默认图像的字符串路径.

所以现在我们可以这样做;

<Button Content="Blah Blah Blah" Click="Click_Handler"
        Style="{StaticResource SpecialButtonStyle}"/>

...我们会得到一个带有默认图标和您的点击处理程序的按钮。除了现在我们需要为每个实例显示不同的图标。所以我们可以做类似的事情;

<Button Content="Blah Blah Blah" 
        Click="Click_Handler"
        Tag="../../../Assets/Images/DIFFERENTIMAGE.png"
        Style="{StaticResource SpecialButtonStyle}"/>

除了这似乎仍然有点令人生畏。所以我们可以更进一步,将您的资产字符串变成资源。将 mscorlib 命名空间添加到您的字典中,作为类似 `xmlns:sys" 的前缀,您可以这样做;

<sys:String x:Key="imageONE">../../../Assets/Images/imageONE.png</sys:String>
<sys:String x:Key="imageTWO">../../../Assets/Images/imageTWO.png</sys:String>
<sys:String x:Key="imageTHREE">../../../Assets/Images/imageTHREE.png</sys:String>

这对我们有一些好处。第一,如果您需要更改其中一个图标的文件 path/name,维护会更容易。你可以在一个地方完成它,它会继承到所有实例。而不是必须追踪它很难设置的每个实例。它还允许我们在实例中调用这些作为资源,所以现在我们的 Button 实例看起来像这样;

<Button Content="Blah Blah Blah" 
        Click="Click_Handler"
        Tag="{StaticResource imageTWO}"
        Style="{StaticResource SpecialButtonStyle}"/>

瞧,大功告成。然而,您应该考虑的另一个观察结果是您的文件路径。我肯定会改用 Pack Uri。只要你的路径是正确的并且你对图像的构建操作也是你应该设置的。

希望这对您有所帮助,干杯。