65.9K
CodeProject 正在变化。 阅读更多。
Home

编写良好 WPF 控件的技巧

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (37投票s)

2007年12月10日

CPOL

10分钟阅读

viewsIcon

149704

downloadIcon

661

一些关于如何编写行为良好的WPF控件的技巧。

目录

引言

随着WPF的出现,微软的优秀开发者(以及我们其他人)现在能够完全改变我们看待Control视觉表示的方式。这是通过使用Templates和Styles来实现的。其中Styles很可能也包含Templates。但是有了这种自由,什么能阻止用户完全改变Control的VisualTree,使其与Control最初设计的功能完全不同呢?实际上,如果我们在WPF中编写一个CustomControl,根本就没有用户界面,并且假设UI将由Template提供。但是是哪种Template呢?它需要包含什么?

这就是本文的全部内容;它解释了几种关键技术,WPF开发人员可以使用这些技术,不仅可以确保自己的控件正常工作,而且可以以良好的方式将Templates和Styles应用于其他控件。

需要注意的是,我**不会**讨论自定义编写的Control的主题感知;这是一个完全不同的问题,可能会成为另一个有趣的文章。

正如我所说,本文的重点是确保控件按其预期工作。

这将是一篇相当短的文章,介绍几种有助于创建行为良好的可模板化和可样式化自定义控件的技术。本文还应该帮助您理解如何自定义其他现有控件甚至第三方WPF控件。

文章的核心内容

我认为最好的起点可能是.NET 3.0附带的一个标准Control的示例。让我们从一个简单易懂的例子开始,比如一个滚动条。我想每个人都知道它长什么样。

标准控件的剖析

滚动条看起来很简单。我使用的是Windows Vista和Aero主题。所以,如果你的滚动条看起来有点不同,请不要感到惊讶或担心,因为当前控件主题不是本文的重点。

我们可以通过Expression Blend,或者使用Charles Petzold的WPF书籍《Applications = Code + Markup: A Guide to the Microsoft® Windows® Presentation Foundation》中的附带下载项目DumpControlTemplate(该项目可在此处获取:这里)来查看构成滚动条的标准Control Template,或者您也可以在MSDN网站上查找Control Template

无论您选择哪种方式,生成的代码在功能上应该是一样的。我选择从MSDN网站获取Scrollbar Template的代码,该网站为大多数标准控件提供了默认模板列表。

<!-- Fill Brushes -->

<LinearGradientBrush x:Key="NormalBrush" 
         StartPoint="0,0" EndPoint="0,1">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#FFF" Offset="0.0"/>
      <GradientStop Color="#CCC" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="HorizontalNormalBrush" 
        StartPoint="0,0" EndPoint="1,0">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#FFF" Offset="0.0"/>
      <GradientStop Color="#CCC" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="LightBrush" 
           StartPoint="0,0" EndPoint="0,1">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#FFF" Offset="0.0"/>
      <GradientStop Color="#EEE" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="HorizontalLightBrush" 
          StartPoint="0,0" EndPoint="1,0">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#FFF" Offset="0.0"/>
      <GradientStop Color="#EEE" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="DarkBrush" 
           StartPoint="0,0" EndPoint="0,1">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#FFF" Offset="0.0"/>
      <GradientStop Color="#AAA" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="PressedBrush" 
          StartPoint="0,0" EndPoint="0,1">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#BBB" Offset="0.0"/>
      <GradientStop Color="#EEE" Offset="0.1"/>
      <GradientStop Color="#EEE" Offset="0.9"/>
      <GradientStop Color="#FFF" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />

<SolidColorBrush x:Key="DisabledBackgroundBrush" Color="#EEE" />

<SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF" />

<SolidColorBrush x:Key="SelectedBackgroundBrush" Color="#DDD" />

<!-- Border Brushes -->

<LinearGradientBrush x:Key="NormalBorderBrush" 
            StartPoint="0,0" EndPoint="0,1">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#CCC" Offset="0.0"/>
      <GradientStop Color="#444" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="HorizontalNormalBorderBrush" 
   StartPoint="0,0" EndPoint="1,0">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#CCC" Offset="0.0"/>
      <GradientStop Color="#444" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="DefaultedBorderBrush" 
             StartPoint="0,0" EndPoint="0,1">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#777" Offset="0.0"/>
      <GradientStop Color="#000" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="PressedBorderBrush" 
           StartPoint="0,0" EndPoint="0,1">
  <GradientBrush.GradientStops>
    <GradientStopCollection>
      <GradientStop Color="#444" Offset="0.0"/>
      <GradientStop Color="#888" Offset="1.0"/>
    </GradientStopCollection>
  </GradientBrush.GradientStops>
</LinearGradientBrush>

<SolidColorBrush x:Key="DisabledBorderBrush" Color="#AAA" />

<SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />

<SolidColorBrush x:Key="LightBorderBrush" Color="#AAA" />

<!-- Miscellaneous Brushes -->
<SolidColorBrush x:Key="GlyphBrush" Color="#444" />

<SolidColorBrush x:Key="LightColorBrush" Color="#DDD" />


<Style x:Key="ScrollBarLineButton" TargetType="{x:Type RepeatButton}">
  <Setter Property="SnapsToDevicePixels" Value="True"/>
  <Setter Property="OverridesDefaultStyle" Value="true"/>
  <Setter Property="Focusable" Value="false"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type RepeatButton}">
        <Border 
          Name="Border"
          Margin="1" 
          CornerRadius="2" 
          Background="{StaticResource NormalBrush}"
          BorderBrush="{StaticResource NormalBorderBrush}"
          BorderThickness="1">
          <Path 
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Fill="{StaticResource GlyphBrush}"
            Data="{Binding Path=Content,
        RelativeSource={RelativeSource TemplatedParent}}" />
        </Border>
        <ControlTemplate.Triggers>
          <Trigger Property="IsPressed" Value="true">
            <Setter TargetName="Border" Property="Background" 
        Value="{StaticResource PressedBrush}" />
          </Trigger>
          <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Foreground" 
        Value="{StaticResource DisabledForegroundBrush}"/>
          </Trigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

<Style x:Key="ScrollBarPageButton" TargetType="{x:Type RepeatButton}">
  <Setter Property="SnapsToDevicePixels" Value="True"/>
  <Setter Property="OverridesDefaultStyle" Value="true"/>
  <Setter Property="IsTabStop" Value="false"/>
  <Setter Property="Focusable" Value="false"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type RepeatButton}">
        <Border Background="Transparent" />
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

<Style x:Key="ScrollBarThumb" TargetType="{x:Type Thumb}">
  <Setter Property="SnapsToDevicePixels" Value="True"/>
  <Setter Property="OverridesDefaultStyle" Value="true"/>
  <Setter Property="IsTabStop" Value="false"/>
  <Setter Property="Focusable" Value="false"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type Thumb}">
        <Border 
          CornerRadius="2" 
          Background="{TemplateBinding Background}"
          BorderBrush="{TemplateBinding BorderBrush}"
          BorderThickness="1" />
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

<ControlTemplate x:Key="VerticalScrollBar" 
                        TargetType="{x:Type ScrollBar}">
  <Grid >
    <Grid.RowDefinitions>
      <RowDefinition MaxHeight="18"/>
      <RowDefinition Height="0.00001*"/>
      <RowDefinition MaxHeight="18"/>
    </Grid.RowDefinitions>
    <Border
      Grid.RowSpan="3"
      CornerRadius="2" 
      Background="#F0F0F0" />
    <RepeatButton 
      Grid.Row="0"                           
      Style="{StaticResource ScrollBarLineButton}"
      Height="18"
      Command="ScrollBar.LineUpCommand"
      Content="M 0 4 L 8 4 L 4 0 Z" />
    <Track 
      Name="PART_Track"
      Grid.Row="1"
      IsDirectionReversed="true">
      <Track.DecreaseRepeatButton>
        <RepeatButton 
          Style="{StaticResource ScrollBarPageButton}"
          Command="ScrollBar.PageUpCommand" />
      </Track.DecreaseRepeatButton>
      <Track.Thumb>
        <Thumb 
          Style="{StaticResource ScrollBarThumb}" 
          Margin="1,0,1,0"  
          Background="{StaticResource HorizontalNormalBrush}"
          BorderBrush="{StaticResource HorizontalNormalBorderBrush}" />
      </Track.Thumb>
      <Track.IncreaseRepeatButton>
        <RepeatButton 
          Style="{StaticResource ScrollBarPageButton}"
          Command="ScrollBar.PageDownCommand" />
      </Track.IncreaseRepeatButton>
    </Track>
    <RepeatButton 
      Grid.Row="3" 
      Style="{StaticResource ScrollBarLineButton}"
      Height="18"
      Command="ScrollBar.LineDownCommand"
      Content="M 0 0 L 4 4 L 8 0 Z"/>
  </Grid>
</ControlTemplate>

<ControlTemplate x:Key="HorizontalScrollBar" 
                      TargetType="{x:Type ScrollBar}">
  <Grid >
    <Grid.ColumnDefinitions>
      <ColumnDefinition MaxWidth="18"/>
      <ColumnDefinition Width="0.00001*"/>
      <ColumnDefinition MaxWidth="18"/>
    </Grid.ColumnDefinitions>
    <Border
      Grid.ColumnSpan="3"
      CornerRadius="2" 
      Background="#F0F0F0" />
    <RepeatButton 
      Grid.Column="0"                           
      Style="{StaticResource ScrollBarLineButton}"
      Width="18"
      Command="ScrollBar.LineLeftCommand"
      Content="M 4 0 L 4 8 L 0 4 Z" />
    <Track 
      Name="PART_Track"
      Grid.Column="1"
      IsDirectionReversed="False">
      <Track.DecreaseRepeatButton>
        <RepeatButton 
          Style="{StaticResource ScrollBarPageButton}"
          Command="ScrollBar.PageLeftCommand" />
      </Track.DecreaseRepeatButton>
      <Track.Thumb>
        <Thumb 
          Style="{StaticResource ScrollBarThumb}" 
          Margin="0,1,0,1"  
          Background="{StaticResource NormalBrush}"
          BorderBrush="{StaticResource NormalBorderBrush}" />
      </Track.Thumb>
      <Track.IncreaseRepeatButton>
        <RepeatButton 
          Style="{StaticResource ScrollBarPageButton}"
          Command="ScrollBar.PageRightCommand" />
      </Track.IncreaseRepeatButton>
    </Track>
    <RepeatButton 
      Grid.Column="3" 
      Style="{StaticResource ScrollBarLineButton}"
      Width="18"
      Command="ScrollBar.LineRightCommand"
      Content="M 0 0 L 4 4 L 0 8 Z"/>
  </Grid>
</ControlTemplate>

<Style x:Key="{x:Type ScrollBar}" TargetType="{x:Type ScrollBar}">
  <Setter Property="SnapsToDevicePixels" Value="True"/>
  <Setter Property="OverridesDefaultStyle" Value="true"/>
  <Style.Triggers>
    <Trigger Property="Orientation" Value="Horizontal">
      <Setter Property="Width" Value="Auto"/>
      <Setter Property="Height" Value="18" />
      <Setter Property="Template" 
        Value="{StaticResource HorizontalScrollBar}" />
    </Trigger>
    <Trigger Property="Orientation" Value="Vertical">
      <Setter Property="Width" Value="18"/>
      <Setter Property="Height" Value="Auto" />
      <Setter Property="Template" 
        Value="{StaticResource VerticalScrollBar}" />
    </Trigger>
  </Style.Triggers>
</Style>

现在,如果我们查看我在Expression Blend中开始编辑的ScrollBar控件,我们可以看到构成ScrollBar控件默认Template的默认VisualTree。如下所示

有些东西看起来很奇怪;嗯,至少对我来说是这样。这个PART_Track到底是什么鬼?它看起来很奇怪。

如前一节所述,控件的默认外观,包括可能改变其外观或行为的任何触发器,由ControlTemplate定义,该模板由代表控件不同部分的各种元素组成,其中一些元素是处理特定控件行为所必需的。缺少这些模板部件将导致控件行为不符合预期。

嗯……,我以为我们可以用自定义控件做我们想做的事情。结果并非如此。我们并没有我们想象的那么自由;有一些限制强加给我们。当然,我们不必遵守这些限制,但这样做可能是个好主意,因为我们希望即使控件看起来不同,它也应该能正常工作。它仍然应该工作。

微软已经考虑过这种情况,并没有解决它(他们也无法解决,因为他们不知道用户会如何处理ControlTemplate),但提供了一种基本机制来确保控件能够正确运行。这种机制称为模板部件(Template Parts)。

将模板部件看作控件与其Template之间的一种松散契约。命名约定是Part_XXXX,其中PART_XXX名称应用于Template中**必须**提供才能使控件工作的项。

那么,我们怎么知道需要提供哪些PART呢?这取决于控件作者做好了工作,并提供了一个文档属性,叫做TemplatePartAttribute。别担心,我们稍后会更深入地探讨这个。重要的是,您需要理解,在某些情况下,Template应该包含什么是有指导原则的。

例如,我之前的CodeProject文章MyFriends使用了一家名为Xceed的公司提供的WPF第三方数据网格。如果我们查阅他们的文档,可以看到他们的数据网格的ControlTemplate需要提供以下控制部件

别担心这个 XCeed 例子,它只是为了向您展示一个大型控件可能期望用户提供什么,但我们只是简单地回到我们的简单 ScrollBar Template。我们再进一步分析一下,好吗?以看似简单(乍一看)的滚动条为例,可以看出滚动条控件实际上是由以下部分组成的

正如我上面所述,默认模板包含一个模板部分,该部分预期被称为PART_Track,如下所示

<Track 
  Name="PART_Track"
  Grid.Row="1"
  IsDirectionReversed="true">
  <Track.DecreaseRepeatButton>
    <RepeatButton 
      Style="{StaticResource ScrollBarPageButton}"
      Command="ScrollBar.PageUpCommand" />
  </Track.DecreaseRepeatButton>
  <Track.Thumb>
    <Thumb 
      Style="{StaticResource ScrollBarThumb}" 
      Margin="1,0,1,0"  
      Background="{StaticResource HorizontalNormalBrush}"
      BorderBrush="{StaticResource HorizontalNormalBorderBrush}" />
  </Track.Thumb>
  <Track.IncreaseRepeatButton>
    <RepeatButton 
      Style="{StaticResource ScrollBarPageButton}"
      Command="ScrollBar.PageDownCommand" />
  </Track.IncreaseRepeatButton>
</Track>

正如我已经演示的,Track被期望是ScrollBar控件所使用的TemplateStyle的一部分。事实上,如果我们没有一个名为PART_TrackTrack,控件将无法按预期工作。

为了证明这一点,我附带的演示应用程序中包含了两个应用了StyleScrollBar。其中一个,我为Track提供了PART_Track名称,而另一个,我完全省略了Track的命名。你猜怎么着,一个ScrollBar工作正常,而另一个则不工作。这就是强大的PART_XXX名称的力量。

事实上,我觉得现在是时候展示一下附带的演示应用程序的小截图了。正如我所说,这是一篇短文,所以没什么花哨的。

它基本上分为四个区域。每个区域执行不同的功能,如下所示

  • 左上角:托管自定义控件,并使用Template_Parts使其工作。
  • 右上角:托管自定义控件,并使用Commands使其工作。
  • 左下角:有一个正常工作的Styled ScrollBar,它包含正确命名的PART_Track Template可视化树对象。
  • 右下角:有一个不工作的Styled ScrollBar,它**不**包含PART_Track命名的Template可视树对象,因此**不能**正常工作;试试看就知道了。

我想我已经谈到了底部的行,在我们上面讨论的内容中,我提到在某些情况下,期望作为应用Template的一部分提供某种元素类型和名称。但是,控件的用户应该如何处理这些信息,以及控件作者应该如何做才能确保他们的控件编写良好,并在用户不提供这些预期的控件部件时行为正确?

好吧,根据我读过的资料,确保控件行为良好以及控件用户知道如何确保这种和谐安排的选项实际上只有几个。本文的其余部分将致力于讨论自定义控件作者可以对他们的控件做些什么。

选项 1

作为控件的设计者,您应该决定哪些部分是控件内部运作的重要部分,这些部分应该被指定为模板部件。请记住,模板部件是控件与其模板之间的一种松散契约。在您决定将哪些元素指定为模板部件后,您应该为它们选择一个名称。约定是“PART_XXX”。然后,您应该使用TemplatePartAttribute(每个部件一个)来记录每个部件的存在。WPF实际上不会对TemplatePartAttribute做任何事情,但它将在某些XAML工具(如Expression Blend)的文档中使用。

下一步是处理指定的模板部件,这应该在继承自FrameworkElementOnApplyTemplate方法的重写中完成。此方法在模板应用时随时调用,因此它提供了优雅处理动态模板更改的机会。要检索控件模板内部任何元素的实例,可以调用同样继承自FrameworkElementGetTemplateChild方法。让我们看一个示例。在此示例中,我创建了一个CustomControl,我希望它包含一个Button和一个ImageButton允许用户浏览Image。是的,它是一个简单的图像浏览器。

我们来看看相关的代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace CustomControlsAndTemplates
{
    [TemplatePart(Name = "Part_OpenImage", Type = typeof(Button))]
    [TemplatePart(Name = "Part_Image", Type = typeof(Image))]
    public class OpenImageCustomControlUsingRoutedEvents : Control
    {
        static OpenImageCustomControlUsingRoutedEvents()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(OpenImageCustomControlUsingRoutedEvents), 
                new FrameworkPropertyMetadata(
        typeof(OpenImageCustomControlUsingRoutedEvents)));
        }

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

            Button openImageButton = base.GetTemplateChild("Part_OpenImage") as Button;
            Image selectedImage = base.GetTemplateChild("Part_Image") as Image;
            if (openImageButton != null && selectedImage != null)
                openImageButton.Click += new RoutedEventHandler(openImageButton_Click);
        }

        private void openImageButton_Click(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.OpenFileDialog ofd = new Microsoft.Win32.OpenFileDialog();
            ofd.AddExtension = false;
            ofd.Multiselect = false;
            ofd.DefaultExt = ".jpg";
            ofd.Filter = "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;" + 
                         "*.JPG;*.GIF|All files (*.*)|*.*";
            ofd.InitialDirectory=System.Environment.GetFolderPath(
                Environment.SpecialFolder.MyPictures);

            if (ofd.ShowDialog().HasValue)
            {
                if (!string.IsNullOrEmpty(ofd.FileName))
                {
                    OpenImageCustomControlUsingRoutedEvents parent = 
                        (sender as Button).TemplatedParent as 
                        OpenImageCustomControlUsingRoutedEvents;
                    Image selectedImage = parent.Template.FindName("Part_Image", parent) 
                        as Image;
                    if (selectedImage != null)
                        selectedImage.Source = new BitmapImage(new Uri(ofd.FileName));
                }
            }
        }
    }
}

而且,这是在XAML中使用控件,并提供了正确的Part_ImagePart_OpenImage

<local:OpenImageCustomControlUsingRoutedEvents 
        x:Name="openImageControlUsingRoutedEvents" Width="80" 
        Height="80" Background="Yellow" Margin="0,5,0,0">
    <local:OpenImageCustomControlUsingRoutedEvents.Template>
        <ControlTemplate>
            <StackPanel Orientation="Vertical" 
                        Background="{TemplateBinding Background}">
                <Border BorderBrush="Black"
                       BorderThickness="2,2,2,2" Height="60">
                    <Image x:Name="Part_Image" 
                       Margin="0" Stretch="Fill" />
                </Border>
                <Button x:Name="Part_OpenImage" 
                    Width="Auto" Content="Browse" 
                    Height="20"/>
            </StackPanel>
        </ControlTemplate>
    </local:OpenImageCustomControlUsingRoutedEvents.Template>
</local:OpenImageCustomControlUsingRoutedEvents>

请注意,此实现可以优雅地处理省略Part_OpenImageTemplate,导致openImageButton变量为null。这是推荐的方法。毕竟,此控件的用户很可能并且相当合理地提供不包含Part_OpenImage Button元素的Template。此实现解决了这个问题。如果提供了Part_OpenImage Button元素,则使用其Click路由事件,否则不执行任何操作。

虽然这个选项很好,但它不够灵活;我们不仅期望有一个Button,而且还需要它有一个特定的名称。有没有更好的方法?嗯,有的。命令(Commands)。这是选项2。

选项 2

将逻辑附加到模板片段的一种更灵活的方法是定义和使用命令。这不仅避免了对特殊名称的需求,而且元素触发器甚至不再需要是Button

下面是OpenImage CustomControl的重写,它已更改为使用命令。在此示例中,我仍然使用Button,但可以将其替换为任何支持命令的控件。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace CustomControlsAndTemplates
{
    [TemplatePart(Name = "Part_OpenImage", Type = typeof(Button))]
    [TemplatePart(Name = "Part_Image", Type = typeof(Image))]
    public class OpenImageCustomControlUsingCommands : Control
    {
        private static RoutedUICommand openImageCommand = new
            RoutedUICommand("Opem File", "OpenImageCommand", 
            typeof(OpenImageCustomControlUsingCommands));

        static OpenImageCustomControlUsingCommands()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(OpenImageCustomControlUsingCommands), 
                new FrameworkPropertyMetadata(
        typeof(OpenImageCustomControlUsingCommands)));

            //Attach the command to the custom logic


            CommandManager.RegisterClassCommandBinding(
                typeof(OpenImageCustomControlUsingCommands),
                new CommandBinding(OpenImageCommand, 
                    new ExecutedRoutedEventHandler(openImageButton_Click)));

        }

        public static RoutedUICommand OpenImageCommand
        {
            get { return openImageCommand; }
        }

        private static void openImageButton_Click(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.OpenFileDialog ofd = new Microsoft.Win32.OpenFileDialog();
            ofd.AddExtension = false;
            ofd.Multiselect = false;
            ofd.DefaultExt = ".jpg";
            ofd.Filter = "Image Files(*.BMP;*.JPG;*.GIF)|
        *.BMP;*.JPG;*.GIF|All files (*.*)|*.*";
            ofd.InitialDirectory=System.Environment.GetFolderPath(
                Environment.SpecialFolder.MyPictures);

            if (ofd.ShowDialog().HasValue)
            {
                if (!string.IsNullOrEmpty(ofd.FileName))
                {
                    OpenImageCustomControlUsingCommands parent = 
                        sender as OpenImageCustomControlUsingCommands;
                    Image selectedImage = parent.Template.FindName(
                        "selectedImage", parent) as Image;
                    if (selectedImage != null)
                        selectedImage.Source = new BitmapImage(new Uri(ofd.FileName));
                }
            }
        }
    }
}

这是在XAML中使用并连接命令的控件

<local:OpenImageCustomControlUsingCommands x:Name="openImageCustomControlUsingCommands" 
        Width="80" Height="80" Background="Yellow" Margin="0,5,0,0">
    <local:OpenImageCustomControlUsingCommands.Template>
        <ControlTemplate>
            <StackPanel Orientation="Vertical" Background="{TemplateBinding Background}">
                <Border BorderBrush="Black" BorderThickness="2,2,2,2" Height="60">
                    <Image x:Name="selectedImage" Margin="0" Stretch="Fill" />
                </Border>
                <Button Command="{x:Static 
        local:OpenImageCustomControlUsingCommands.OpenImageCommand}" 
        Width="Auto" Content="Browse" Height="20"/>
            </StackPanel>
        </ControlTemplate>
    </local:OpenImageCustomControlUsingCommands.Template>
</local:OpenImageCustomControlUsingCommands>

我想,这种方法的唯一问题是,你必须知道有可用的命令可以完成你想要的事情。

好了,本文到此结束。我告诉过你它很短。但我希望它能在某种程度上有所帮助。我认为它相当奇怪,所以应该还在你的脑海中。至少我的思维方式是这样的。我忘记了简单的事情,而奇怪和可怕的事情我记得很清楚。

参考文献

以下是我为本文查阅并(在某些情况下)使用和修改的代码列表

您怎么看?

我想请求一下,如果你喜欢这篇文章,请投它一票,并留下一些评论,这样我就知道这篇文章的水平是否合适,以及它是否包含了人们需要了解的内容。

结论

尽管这篇文章相当短,但我希望它能帮助那些可能正在考虑编写自定义WPF控件,或者正在处理第三方WPF控件的人。

© . All rights reserved.