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

继承自无外观的 WPF 控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2013年4月10日

CPOL

12分钟阅读

viewsIcon

44255

downloadIcon

1882

本文介绍如何通过继承利用无外观的 WPF 控件。

 

引言

我们都知道,开发具有美观 GUI 的软件通常需要我们使用现有的控件并在此基础上添加一些价值。基于 Windows Presentation Foundation (WPF) 和 Model View ViewModel (MVVM) 模式开发控件也不例外,只是我们可以使用一种技术,它为设计 GUI 提供了极大的灵活性,即使在软件开发人员完成工作后, GUI 也可以在很多方面进行更改。本文提出的解决方案基于 [1],并通过增强现有控件(我选择了 Combobox [2] 作为示例)来进一步改进,其中包含一个“陷阱”。

本文提出的解决方案可以应用于任何无外观的 WPF 控件。因此,您应该将本文视为将来对您可用的现有控件集进行增强的模板。提出的示例非常“简单”,您可以通过更改标准控件的控件模板来实现相同的功能。然而,在某些情况下,这还不够 [8],我们需要添加或更改比此处所示更多的内容。因此,您应该将此视为一个概念的介绍,您以后可以将其应用于其他更复杂的情况。

本文分为两部分,下一节将探讨我们入门所需的通用步骤。接下来的几节将在第一节的基础上进行,并提供具体的示例实现。  

开始 

本节包含一个通用指南,列出了实现继承自另一个 WPF 控件的控件所需的步骤。如果您熟悉通用概念,可以安全地跳到下一节。

第一步是在 Visual Studio 2010 或更高版本中创建一个名为“UnitCombobox”的新 WPF 应用程序项目。然后,将一个名为“UnitComboLib”的类库项目添加到此解决方案中。  

接下来,我们添加继承自 WPF ComboBox 类 [2] 的类。您可以使用 VS 2010 扩展来添加新的自定义控件 [3](这基本上会添加一个新的 XAML.cs 文件),或者您可以手动添加 XAML.cs 文件。无论哪种方式,您的控件都应命名为:UnitCombobox,对应的文件应命名为 UnitCombobox.xamlUnitCombobox.cs

UnitCombobox.xaml 文件应包含以下内容

<!-- You need to associates this file in your generic.xaml file -->

 <Style TargetType="{x:Type local:UnitCombobox}">
   <Setter Property="Template">
     <Setter.Value>
       <ControlTemplate TargetType="{x:Type local:UnitCombobox}">
         <Border Background="{TemplateBinding Background}"
             BorderBrush="{TemplateBinding BorderBrush}"
             BorderThickness="{TemplateBinding BorderThickness}">
         </Border>
       </ControlTemplate>
     </Setter.Value>
   </Setter>
 </Style>
</ResourceDictionary>

UnitCombobox.cs 文件应包含以下内容以供我们开始

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 UnitComboLib
{
  /// <summary>
  /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
  ///
  /// Step 1a) Using this custom control in a XAML file that exists in the current project.
  /// Add this XmlNamespace attribute to the root element of the markup file where it is 
  /// to be used:
  ///
  ///     xmlns:MyNamespace="clr-namespace:UnitComboLib"
  ///
  ///
  /// Step 1b) Using this custom control in a XAML file that exists in a different project.
  /// Add this XmlNamespace attribute to the root element of the markup file where it is 
  /// to be used:
  ///
  ///     xmlns:MyNamespace="clr-namespace:UnitComboLib;assembly=UnitComboLib"
  ///
  /// You will also need to add a project reference from the project where the XAML file lives
  /// to this project and Rebuild to avoid compilation errors:
  ///
  ///     Right click on the target project in the Solution Explorer and
  ///     "Add Reference"->"Projects"->[Browse to and select this project]
  ///
  ///
  /// Step 2)
  /// Go ahead and use your control in the XAML file.
  ///
  ///     <MyNamespace:UnitCombobox/>
  ///
  /// </summary>
  public class UnitCombobox : Control
  {
    static UnitCombobox()
    {
      DefaultStyleKeyProperty.OverrideMetadata(typeof(UnitCombobox), 
              new FrameworkPropertyMetadata(typeof(UnitCombobox)));
    }
  }
}

现在,如果从空的类库项目开始,您需要向“UnitComboLib”项目添加对标准 .NET Framework 程序集“WindowsBase”、“PresentationCore”、“PresentationFramework”和“System.XAML”的引用。

接下来,我们必须在“Themes/Generics.XAML”文件中引用新的 XAML 文件

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

  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="UnitComboLib;component/UnitCombobox.xaml" />
  </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

…并在“UnitComboLib”库的 AssemblyInfo.cs 文件中添加 ThemeInfo 引用

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located

    // (used if a resource is not found in the page, or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly // where the generic resource dictionary is located

  // (used if a resource is not found in the page, 
  // app, or any theme specific resource dictionaries)
)]

您可能已经从 [1](第 2 部分)了解到,需要进行这两项添加,以便 .NET Framework 知道我们正在创建一个包含一个或多个无外观控件定义的程序集。该框架将使用此信息查找“Themes/Generics.xaml”文件,以查找至少一个控件的标准通用定义,如果所有其他方法都失败(例如:我们错过了在应用程序的资源字典文件中定义 XAML)。

现在,这就是在 WPF 中定义无外观控件(不带继承)所需的所有内容。那么,让我们测试一下到目前为止的进展情况

UnitCombobox 项目添加对 UnitComboLib 项目的引用。打开 MainWindow.xaml 文件并输入以下内容

<Window x:Class="UnitCombobox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        
        xmlns:unitcmb="clr-namespace:UnitComboLib;assembly=UnitComboLib"
        >
    <Grid>
    <unitcmb:UnitCombobox HorizontalAlignment="Left" VerticalAlignment="Center"
                          MinWidth="100" MinHeight="23"
                          BorderBrush="Black" BorderThickness="1"/>
  </Grid>
</Window>

…这应该等同于

…结果应该看起来像这样

到目前为止一切顺利。现在,您可能想知道为什么这个控件如此空洞,根本不像一个组合框。这是因为 XAML 是空的。可以说是它的“皮肤”除了可以通过 MainWindow.XAML 文件中的代码激活的标准边框之外,什么都没有。

下一小节列出了通过实现对标准 WPF 控件的继承来为控件注入更多生命所需的通用步骤。

实现继承

继承自无外观 WPF 控件当然包括 C# 语言中简单的继承构造,我们可以将其添加到 UnitCombobox.cs 文件中。所以,只需将 Control 语句替换为 ComboBox 语句

public class UnitCombobox : Control 
public class UnitCombobox : ComboBox

现在,让我们仔细看看 [2],并评估其中的 Syntax 部分,尤其是 TemplatePartAttribute 标签

[TemplatePartAttribute(Name = "PART_Popup", Type = typeof(Popup))]
[LocalizabilityAttribute(LocalizationCategory.ComboBox)]
[TemplatePartAttribute(Name = "PART_EditableTextBox", Type = typeof(TextBox))]
[StyleTypedPropertyAttribute(Property = "ItemContainerStyle", StyleTargetType = typeof(ComboBoxItem))]
public class ComboBox : Selector

上述标签应添加到派生类的定义中。

这些标签定义了 ComboBox 控件的任何实现都必须至少定义一个“PART_Popup”和一个“PART_EditableTextBox”组件。它们的特定类型由“PART_”命名约定指示。

现在,您可能会想,我从哪里可以得到这些,以及这有多难?实际上,现在还不算太难。只需查看 [4] 中的 XAML,然后以相反的顺序将两个 XAML 部分粘贴到 UnitCombobox.xaml 文件中。最终的解决方案应如 下载 UnitCombobox_Step1.zip 中所示,结果应该看起来像这样

是的,这看起来更像一个组合框。关于继承就到这里了。简而言之,继承无外观 WPF 控件就是这些。希望这个小指南已经帮助您入门。欢迎随时回来开始制作另一个很酷的控件。本文的其余部分将解释我如何扩展 WPF 组合框控件,以添加一个单元弹出控件,该控件可用于根据给定单元建议标准值。

UnitCombobox 控件 - 第 1 步

我最近开始实现一个类似于 Visual Studio 2010/2012 底部左侧角落的缩放控件。我注意到(即使在 VS 2012 中),该控件的文本部分有一个百分号,而且它似乎有些多余。显然,用户只需输入一个整数,控件就会将其转换回我们现在从 Office 和无数其他缩放应用程序中习惯的百分比视图。

但我的观点是:为什么“%”字符是文本部分的一部分,而 WPF 提供了一种简单的机制,例如,在完全相同的位置显示一个标签?显然,每个人都能从中找到改进之处,因为开发人员不必担心特殊的解析例程,用户也可以输入他们喜欢的任何数字。答案(很可能)是历史。我们已经习惯了这些,以至于我们停止质疑——尚未完全实现 WPF,并且经常看不到像这样的微小改进。

因此,我的第一个想法是将“%”字符显示在文本部分旁边的标签中,然后就完成了。但这个想法很快被另一个想法扩展了,我想让标签具有交互性,并让用户选择他们是想按字体大小(点)还是按 12 点字体大小的百分比来缩放显示。我脑海中的控件看起来像这样

因此,我实现了一些初步的草图,以便更好地理解这个想法。

这让我进行了一次小插曲,我研究了单位使用和转换。我曾短暂地想过实现一个通用的工具,让你可以在一个系统的不同单位(例如长度)之间进行选择,并使用和转换公制和英制单位系统等。但我很快就推迟了这个想法,转而追求我最初的目标。

这个初步评估表明,在简单鼠标悬停时弹出上下文菜单(而不是单击)是行不通的,因为如果有人在文本区域输入文本,并将鼠标光标移到下拉部分选择默认值,可能会非常烦人。这种工作流程可能不经常发生,但它在测试中已经足够烦人了,让我相信最好是在左键单击时提供额外的弹出上下文菜单。这让我进一步研究了这些想法(其中我最喜欢 Web 链接的外观和感觉): 

 

我为上述解决方案开发了一个 ContextMenuBehaviour 行为 [5] 类。此行为可以附加到任何 FrameworkElement,并与在鼠标单击时显示的自定义上下文菜单连接。因此,这个小扩展允许我们在 WPF 元素上使用上下文菜单,例如,标签,这些标签本身就不是为了在左键单击时显示菜单而设计的,-更改鼠标悬停时的鼠标光标或标签的背景颜色是可以在项目 View/DistanceConvert.xaml 文件中找到的“简单”XAML 样式。

 

例如,这是基于标签内文本块的 Web 链接的 XAML 代码

<TextBlock Text="{Binding SelectedItem.DisplayNameShort}" Name="LabelTextBlock">
<TextBlock.Style>
  <Style TargetType="{x:Type TextBlock}">
    <Style.Triggers>
      <Trigger Property="IsMouseOver" Value="true">
        <Setter Property="TextDecorations" Value="Underline" />
        <Setter Property="Cursor" Value="Hand" />
      </Trigger>
      </Style.Triggers>
    </Style>
  </TextBlock.Style>
</TextBlock>

上述解决方案中的 CommandUnitViewwModel 程序包包含著名的 RelayCommand 类 [6]、一组单元转换类以及驱动标签-上下文菜单控件所需的 ViewModel 类。您将在最终解决方案中看到类似的程序包,因此,如果您想分析最终项目中难以理解的细节,您可能想重新访问该项目。

上述解决方案包含一个转换器,用于在文本框中输入的字符串和 UnitViewModel 类中的后端 double 值之间进行转换。后来发现我们不需要这个转换器,因为 WPF 框架会在没有提供时注入一个标准的转换器。

我最终删除了转换器解决方案,因为事实证明,通过转换器实现用户输入特定值时的错误反馈指导过于复杂。因此,最终解决方案遵循了此方法 [7],该方法建议在 ViewModel 中使用 IDataErrorInfo 接口实现逻辑。

那么,让我们在下一节中看看最终的解决方案。

UnitCombobox 控件 - 第 2 步

 

最终解决方案包含一个带有可编辑文本部分的组合框和一个可单击的标签,该标签显示了上一节中描述的上下文菜单。

此控件的一个重要假设是,应用程序的后端实际上使用的是摄氏度而不是任何其他温度单位。因此,此摄氏度值显示在测试应用程序中,并在 UnitViewModel 中计算(您可能想使用不同的单位)。

public double TemperatureCelsius
{
  get
  {
    if (this.SelectedItem != null)
    {
      double d = this.mUnitConverter.Convert(this.SelectedItem.Key,
                                             this.mValue, Itemkey.TemperaturCelsius);

      return d;
    }

    // Fallback to default if all else fails
    return Unit.Temperature.ConverterTemperature.DefaultTemperatureCelsius;
  }
}

从头开始理解此控件的关键在于 Themes/Generic.xaml 文件。系统在查找控件的通用外观(如果未提供其他外观)时会评估此文件 [1]。Generic.xaml 文件包含 UnitCombobox.xaml 文件,其中包含派生组合框控件的实际外观。XAML 与 ControlTemplate 示例 [4] 几乎相同。此解决方案中有趣且独特的部分是标签定义及其文本块(以及之前描述的触发器)。

<Label Grid.Column="0" Grid.Row="1"
       Padding="0"
       Margin="1,0,3,0"
       BorderThickness="0"
       ToolTip="{Binding SelectedItem.DisplayNameLong}"
       HorizontalAlignment="Left"
       VerticalAlignment="Center" VerticalContentAlignment="Center"
       behav:ContextMenuBehaviour.MenuList="{Binding ElementName=contextMenuOnUniLabel1}">
  <Label.ContextMenu>
    <ContextMenu Name="contextMenuOnUniLabel1" ItemsSource="{Binding Path=UnitList}" Placement="Bottom">
      <ContextMenu.ItemContainerStyle>
        <Style TargetType="{x:Type MenuItem}">
          <Setter Property="Command" 
               Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}, 
                          Path=DataContext.SetSelectedItemCommand}"/>
          <Setter Property="CommandParameter" Value="{Binding Key}"/>
          <Setter Property="Header" Value="{Binding DisplayNameLongWithShort}" />
        </Style>
      </ContextMenu.ItemContainerStyle>
    </ContextMenu>
  </Label.ContextMenu>

  <TextBlock Text="{Binding SelectedItem.DisplayNameShort}" Name="LabelTextBlock">
  <TextBlock.Style>
    <Style TargetType="{x:Type TextBlock}">
      <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="true">
          <Setter Property="TextDecorations" Value="Underline" />
          <Setter Property="Cursor" Value="Hand" />
        </Trigger>
        </Style.Triggers>
      </Style>
    </TextBlock.Style>
  </TextBlock>
</Label>

当在 MainWindow.xaml 类(如下面的列表所示)中实例化控件时,此标签绑定到 UnitViewModel 对象。SelectedItem 属性是基于 ListItem 类的相应属性。上下文菜单项从 UnitViewModel 对象中的 UnitList 集合填充。如前所述,行为驱动标签上的左键单击,文本块部分中的样式向用户指示其悬停在可能可单击的内容上。

<unit:UnitCombobox DataContext="{Binding SizeUnitLabel}"
                   Margin="1"
                   Padding="0"
                   BorderThickness="1"
                   BorderBrush="Transparent"
                   ItemsSource="{Binding SelectedItem.DefaultValues}"
                   ToolTip="{Binding ValueTip}"
                   IsEditable="True"
                   HorizontalAlignment="Stretch" VerticalAlignment="Top"
                   Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
    <unit:UnitCombobox.Text>
    <Binding Path="StringValue" Mode="TwoWay" ValidatesOnDataErrors="True"/>
  </unit:UnitCombobox.Text>
</unit:UnitCombobox>

上面的代码几乎是使用组合框的标准 XAML。这里有趣的是 ToolTip 绑定,它用于在输入方面为用户提供初步的提示。而 ValidatesOnDataErrors="True" 部分告诉 WPF,可以为字符串属性 StringValue 分配一个值,但在每次编辑时都必须对其进行验证,正如 Josh Smith 在他的文章 [7] 中所记录的那样。验证中的错误将以 WPF 标准的红色边框和相应的工具提示突出显示。

UnitViewModel 类是 UnitCombobox 控件的主要 ViewModel 类。它通过 UnitCombobox 测试应用程序项目中的 AppViewModel 类进行实例化。

UnitViewModel 类是 UnitCombobox 控件的主要 ViewModel 类。它通过 UnitCombobox 测试应用程序项目中的 AppViewModel 类进行实例化。

UnitViewModel 类中的 SetSelectedItemCommand ICommand 属性触发了一个命令,该命令在用户通过上下文菜单项更改显示单位时执行。此命令执行 SetSelectedItemExecuted 方法,该方法又

  • this.mUnitList 集合中查找下一个选定的单位,
  • 通过 Unit 程序包(由 mUnitConverter 字段表示)计算当前双精度值的转换,
  • 并将当前单位设置为选定单位(this.mSelectedItem = li;)。

UnitViewModel 类中的 IsDoubleWithinRange 方法检查当前值是否正常,并执行相应的操作。如前所述 [7],它通过 IDataErrorInfo 接口调用。

结论

本项目展示了 WPF 在有趣的用户界面解决方案中的另一种应用方式。事实证明,通过重新定义 XAML 外观并提供相应的 ViewModel,可以派生自现有 WPF 控件并增强其功能。当然,这并非没有限制,如果需要,我们始终可以添加依赖属性和其他“陷阱”,但看到基于这些相对“简单”的扩展的可能性是令人兴奋的。

我最近学会了如何通过继承来扩展 WPF 控件,并想将此传达给编码人员。请给我您的反馈,以便找到并纠正可能的错误。让我知道您对这里提出的控件的看法。

参考文献

历史

  • 2013 年 4 月 10 日 首次创建。
© . All rights reserved.