WPF自定义按钮的最佳方法

7
我想在WPF中创建一个自定义的Button。当然,这个按钮将是一个UserControl,并且它将包含许多视觉元素(如描边、高亮、阴影、发光、图像等)。
问题是,如果我使用DependencyProperties并在XAML中绑定它们,我将无法在设计时看到结果(我尝试实现IsInDesignMode方法,但由于某种无法理解的原因,我的VS在UserControls上使用此方法时会崩溃,否则它完全正常),这绝对不好。
所以我在考虑根本不使用XAML,在代码后台完成所有工作。
你们觉得怎么样?

3
我认为你应该使用Expression Blend。 - Louis Kottmann
3
此外,与其扩展“UserControl”,不如为“Button”控件编写自己的“ControlTemplate”。 - McGarnagle
我只是一个新手,我想要做的就是学习。顺便说一下,我从未使用过Expression Blend,所以我不确定该怎么想。此外,我希望这个按钮可以高度自定义,所以我想标准的WPF编辑器会是我的选择。@Baboon - Andrei
正如dbaseman所建议的那样,ControlTemplate就是为此目的而设计的。它可以由任意数量的嵌套元素组成,因此您肯定可以通过自定义UserControl来实现任何您想要的功能,但不会出现一些与数据绑定相关的问题。 - XamlZealot
正确的方法是使用Expression Blend修改基本按钮的ControlTemplate。就这样。 - Louis Kottmann
1个回答

13

和你一样,当我刚开始学习并想要理解模板的运作方式时,需要进行大量的试错。希望我的研究和一些逐步组件可以帮助您定制自己喜欢的内容,并了解事物的来源。

首先,在尝试了解新的“模板风格”如何工作时,我为我的任意操纵样式创建了一个简单的独立WPF应用程序(“AMS”)。这样,我就不必等待主要项目和主题的试错过程中看到某些东西的样子。

从那里,我创建了一个名为“TestingStyles”的新的WPF窗口。保存/编译,运行,没有问题。

现在,在TestingStyles窗口的“查看代码”中,我放置了我正在处理的自定义类...为了帮助逐步展示,我创建了以下内容:

namespace AMS
{
    /// <summary>
    /// Interaction logic for TestingStyles.xaml
    /// </summary>
    public partial class TestingStyles : Window
    {
        public TestingStyles()
        {
            InitializeComponent();
        }
    }

    // Enumerator for a custom property sample...
    public enum HowToShowStatus
    {
        ShowNothing,
        ShowImage1
    }


    public class YourCustomButtonClass : Button
    {
        public YourCustomButtonClass()
        {
            // auto-register any "click" will call our own custom "click" handler
            // which will change the status...  This could also be done to simplify
            // by only changing visibility, but shows how you could apply via other
            // custom properties too.
            Click += MyCustomClick;
        }

        protected void MyCustomClick(object sender, RoutedEventArgs e)
        {
            if( this.ShowStatus == HowToShowStatus.ShowImage1 )
                this.ShowStatus = HowToShowStatus.ShowNothing;
            else
                this.ShowStatus = HowToShowStatus.ShowImage1;
        }


        public static readonly DependencyProperty ShowStatusProperty =
              DependencyProperty.Register("ShowStatus", typeof(HowToShowStatus),
              typeof(YourCustomButtonClass), new UIPropertyMetadata(HowToShowStatus.ShowNothing));

        public HowToShowStatus ShowStatus
        {
            get { return (HowToShowStatus)GetValue(ShowStatusProperty); }
            set { SetValue(ShowStatusProperty, value); }
        }
    }

}

如您所见,我在默认的TestingStyles: Window声明之外底部有一个自定义的“Button”类... 所以它们都在同一个“项目”中。

在这个XAML示例中,我引用了一个“TaskComplete.png”图像文件(仅供演示目的,可以直接添加到项目中... 即使是为了演示目的一个简单的笑脸)。 因此,请创建这样一个简单的.png文件... 甚至可以使用Microsoft Paint画一个有眼睛和微笑的圆圈。将其保存到根目录下的项目中(稍后再处理路径问题,先让它正常工作)。

保存并重新编译项目,以便项目公开知道新“类”(按钮)是什么,当您开始定义XAML模板时。

现在,回到TestingStyles设计师,并将其拆分成两个屏幕,以便您可以同时查看设计师和XAML标记... 然后只需替换为以下内容...

<Window x:Class="AMS.TestingStyles"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:AMS"
        Title="TestingStyles" Height="300" Width="300" >

    <Window.Resources>
        <!-- Build a "Style" based on an anticpated target control type of YourCustomButtonClass.
            per the "my:" reference, the "my" is an "alias" to the xmlsn:my in the declaration above,
            so the XAML knows which library to find such control.  In this case, I've included within
            the actual forms's 'View Code' as a class at the bottom.  

            As soon as you assign an "x:Key" reference, its like its telling XAML to make this a PRIVATE
            style so you don't reference it explicitly (yet)
        -->
        <Style TargetType="my:YourCustomButtonClass" x:Key="keyYourCustomButtonClass">
            <!-- put whatever normal "settings" you want for your common look / feel, color -->
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="Padding" Value="0,0,1,1"/>
            <Setter Property="Width" Value="100" />
            <Setter Property="Height" Value="30" />

            <!-- Now, for the template of the button.  Things can get really crazy here
              as you are now defining what you want the "button" to look like, borders, 
              content, etc. In this case, I have two borders to give the raise/sunken effect 
              of a button and it has its own colors -->
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button" >
                        <!-- The x:Name references used during triggers to know what it is "applying" changes to -->
                        <Border x:Name="BorderTopLeft"
                                BorderBrush="Gainsboro" 
                                BorderThickness="0,0,1.5,1.5">

                            <Border x:Name="BorderBottomRight"
                                BorderBrush="Gray" 
                                BorderThickness="1.5,1.5,0,0">
                                <!-- Now, what control  type do you want the button to have... 
                                    Ex: You could use a grid (as I have here), stack panels, etc -->
                                <Grid Background="LightBlue" >
                                    <!-- I'm defining as two columns wide, one row tall.  
                                        First column fixed width 20 pixels example for an image -->
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="20px" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <Grid.RowDefinitions>
                                        <RowDefinition />
                                    </Grid.RowDefinitions>

                                    <!-- Now, create the controls I want available within my "template".
                                        when assigned with "x:Name", thats like a property withing the template
                                        that triggers can associate and update to. -->
                                    <Image x:Name="btnImage" 
                                        Grid.Row="0" Grid.Column="0"
                                        Stretch="None"
                                        VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
                                        Source="TaskComplete.png"
                                        Visibility="Visible" />

                                    <!-- and also have the text for the button to show the user -->
                                    <TextBlock x:Name="txtNewBtn" 
                                        Grid.Row="0" Grid.Column="1"
                                        Padding="5"
                                        HorizontalAlignment="Left"
                                        VerticalAlignment="Center"
                                    Text="{TemplateBinding Content}" />
                                    <!-- The "{TemplateBinding Content}" means to set the text based on 
                                        the "CONTENT" property of the original button and not use a fixed value -->
                                </Grid>
                            </Border>
                        </Border>
                        <!-- Now, some triggers for the button itself... some can be property based, others data-based -->
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsPressed" Value="true">
                                <!-- What properties do we want to change when user CLICKS
                                    on the button, give the "EFFECT" of click down/up by
                                    changing the "Margin" and border thicknesses...  -->
                                <Setter Property="Margin" Value="1,1,0,0"/>
                                <!-- Notice the "TargetName" below referring to the x:Name I've applied in template above 
                                    so when the user clicks on the button, it changes the border thickness properties of
                                    each to give the effect of a normal button clicking.  I'm widening one border, shrinking other -->
                                <Setter TargetName="BorderTopLeft" Property="BorderThickness" Value="2.5,2.5,0,0"/>
                                <Setter TargetName="BorderBottomRight" Property="BorderThickness" Value="0,0,.5,.5"/>
                            </Trigger>

                            <!-- Here, I have a custome property on the class for "ShowStatus".  The binding is to itself
                                regardless of how many instances of this type of "button" are on a given form 
                                First trigger happens when the value is changed to "ShowNothing", but can also change 
                                when set to "ShowImage1" or other as you may need applicable
                            -->
                            <DataTrigger Binding="{Binding Path=ShowStatus, RelativeSource={RelativeSource Self}}" Value="ShowNothing">
                                <Setter TargetName="btnImage" Property="Visibility" Value="Hidden"/>
                            </DataTrigger>
                            <DataTrigger Binding="{Binding Path=ShowStatus, RelativeSource={RelativeSource Self}}" Value="ShowImage1">
                                <Setter TargetName="btnImage" Property="Visibility" Value="Visible"/>
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <!-- NOW, we can expose any instance of "YourCustomButtonClass" button to use the style based on definition above 
            any instance of such YourCustomButtonClass will automatically reflect this style / look -->
        <Style TargetType="my:YourCustomButtonClass" BasedOn="{StaticResource keyYourCustomButtonClass}" />

    </Window.Resources>

    <Grid>
        <my:YourCustomButtonClass Content="Button" VerticalAlignment="Top" ShowStatus="ShowImage1" />
    </Grid>
</Window>

这将为您定义自己的模板以及元素如何开始连接提供极大的起点。一旦运行此示例,当您更改任何颜色、边距、填充等到模板时,您将立即看到该组件对控件的视觉影响。祝您玩得开心,不要太过苦恼......顺便说一句,一旦这个工作完成,您就可以在style元素中使用它。
<Window.Resources>
</Window.Resources> 

将其放入Windows资源字典中,使其成为项目的全局资源,而不仅仅是此测试表单。

非常感谢。代码非常干净,我认为我可以很好地理解它。现在我正在尝试实现它...祝我好运 :) - Andrei
缺少了带有“BasedOn”的第二个样式 - 我搜索了几个小时才发现我的错误... :D - Florian Koch

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