基于 ComboBox 的简单 WPF 画笔选择器控件






4.33/5 (3投票s)
本文介绍了使用WPF创建通过ComboBox选择SolidColorBrush控件的几种技术,并进行了比较和对比。

引言
在我编写的一个WPF程序中,我需要一个简单的控件来从一个集合中选择一个SolidColorBrush
。实际上,我需要多个实例访问不同的集合。期望的格式是一个ComboBox
,当展开时,颜色以网格形式显示。考虑到这些需求,我知道需要某种形式的通用控件。因此,我决定尝试Style
和UserControl
。本文将介绍这两种方法和几个可用的实现。
Using the Code
有4个相关文件
- MainWindow.xaml - 包含基于Style的方法和整体UI
- BrushSelUserControl.xaml - User Control的XAML元素
- BrushSelUserControl.xaml.cs - User Control的C#元素
- BrushesToList.cs - 将
System.Windows.Media.Brushes
转换为SolidColorBrush
的集合
让我们深入了解MainWindow.xaml
<Window x:Class="BrushSelector.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:BrushSelector"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="ComboBox" x:Key="BrushSelector">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate DataType=
"{x:Type SolidColorBrush}">
<Rectangle Width="18"
Height="{Binding RelativeSource=
{RelativeSource Mode=Self},
Path=Width}" Margin="2"
Fill="{Binding}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBox" x:Key="BrushesSelector"
BasedOn="{StaticResource BrushSelector}">
<Setter Property="ItemsSource"
Value="{x:Static src:BrushesToList.Brushes}"/>
</Style>
<x:Array x:Key="SomeBrushes" Type="{x:Type SolidColorBrush}">
<SolidColorBrush>Red</SolidColorBrush>
<SolidColorBrush>Green</SolidColorBrush>
<SolidColorBrush>Blue</SolidColorBrush>
</x:Array>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0"
VerticalAlignment="Center">Any Brush Selector</Label>
<ComboBox Grid.Column="1" Grid.Row="0" Name="RGBBrushSel"
VerticalAlignment="Center" Style="{StaticResource BrushSelector}"
ItemsSource="{StaticResource SomeBrushes}" SelectedIndex="0"/>
<Ellipse Grid.Column="2" Grid.Row="0" Width="120" Height="60"
Fill="{Binding ElementName=RGBBrushSel, Path=SelectedItem}"/>
<Label Grid.Column="0" Grid.Row="1"
VerticalAlignment="Center">Brushes Selector</Label>
<ComboBox Grid.Column="1" Grid.Row="1" Name="BrushSel"
VerticalAlignment="Center" Style="{StaticResource BrushesSelector}"
SelectedIndex="0"/>
<Ellipse Grid.Column="2" Grid.Row="1" Margin="5" Width="120"
Height="60" VerticalAlignment="Center"
Fill="{Binding ElementName=BrushSel, Path=SelectedItem}"/>
<Label Grid.Column="0" Grid.Row="2" VerticalAlignment="Center">
User Control</Label>
<src:BrushSelUserControl Grid.Column="1" Grid.Row="2"
x:Name="UserCtrl" VerticalAlignment="Center" SelectedIndex="77"/>
<Ellipse Grid.Column="2" Grid.Row="2" Margin="5" Width="120"
Height="60" VerticalAlignment="Center"
Fill="{Binding ElementName=UserCtrl, Path=SelectedItem}"/>
</Grid>
</Window>
首先,跳到最后定义Grid
的地方。这是一个简单的3x3网格。每一行包含一个标签,描述正在显示的控件类型,然后是画笔选择器控件本身,最后是一个120x60的椭圆。颜色(或者说用来填充它的Brush
)绑定到画笔选择器的SelectedItem
。
介绍中提到了几种机制?是的,那么为什么会有三个控件呢?答案是前两个是基于Style的,第二个在前一个的基础上构建,而第三个是实际的User Control。
样式
第一种方法是创建一个Style
。它定义在Windows.Resources
部分。它是一个Style
,通过资源键命名为BrushSelector。因为它有名字,所以它**不会**自动应用于它所针对的类型,即由TargetType
属性指定的ComboBox
。因此,很容易意外地使用这个Style
。相反,它只能用于ComboBox
,并且只能在直接引用键时使用。通过查找第一个名为RGBBrushSel的ComboBox
可以看到这一点。除非打算普遍应用Style
,否则我认为最好将其设计成只能显式使用。
由于需要自定义ComboBox
的显示,Style
包含两个设置器。一个用于更改项目显示的Panel
,另一个用于更改实际项目的外观。
前者需要设置ItemsPanelTemplate
。这被简单地设置为一个UniformGrid
。这种Panel
的优点在于它能以一种保持布局均匀的方式分配行和列。这在顶部的图片中有所展示。
在指定了网格之后,我们希望每种颜色都显示为一个小的矩形。这是通过创建一个DataTemplate
来实现的,该模板应用于ItemTemplate
属性。它只是绘制一个18x18的Rectangle
。这里的巧妙之处在于Rectangle
是用当前选定的画笔填充的。这很容易通过(数据)绑定到自身来实现。绑定声明如此简洁的原因是,i.e.
Fill="{Binding}"
是由于DataTemplate
被类型化为SolidColorBrush
,这相当于它的数据上下文,并且没有指定路径,因为数据*就是*一个SolidColorBrush
,这正是Rectangle
的FillProperty
所需的类型。这其中还有更多的魔法:此模板既用于在UniformGrid
中显示项目,也用于显示SelectedItem
,即ComboBox
处于折叠状态时。
除了在RGBBrushSel
的定义中指定Style
之外,还指定了ItemsSource
属性。这是控件应显示的SolidColorBrush
集合。因此,Style
不绑定到任何特定的SolidColorBrush
集合。这一点很重要,以便允许多个具有此Style
的ComboBox
实例,但显示不同的集合。在本例中,示例使用了定义在Window
资源部分的一个SolidColorBrush
数组,该数组包含红色、绿色和蓝色的画笔。
带有已设置集合的Style
使用此控件的程序的早期开发阶段尚未包含创建和管理不同SolidColorBrush
集合的代码,作为临时措施,仅从System.Windows.Media.Brushes
创建一个集合就足够了。不幸的是,该类通过单个static
属性提供画笔,这些属性返回SolidColorBrush
的适当实例,而不是集合。这意味着它不能像前面的示例那样与ItemsSource
参数一起使用。解决方案是创建一个新类来完成这项工作。
public static class BrushesToList
{
public static IEnumerable<SolidColorBrush> Brushes { get; private set; }
static BrushesToList()
{
List<SolidColorBrush> brushes = new List<SolidColorBrush>();
foreach (PropertyInfo propInfo in typeof(System.Windows.Media.Brushes).
GetProperties(BindingFlags.Public | BindingFlags.Static))
if (propInfo.PropertyType == typeof(SolidColorBrush))
brushes.Add((SolidColorBrush)propInfo.GetValue
(null, null));
Brushes = brushes;
}
}
使用反射来查找所有static
和public
属性方法。然后检查这些属性是否确实返回SolidColorBrush
的实例。如果是这种情况,它们将被添加到本地列表中。因此,这只需要在类的static
构造函数中完成一次。然后将列表分配给一个只能通过IEnunerable<SolidColorBrush>
属性访问的属性,这样任何使用者都不能修改其内容,意味着它可以被任何需要此集合的其他代码安全地共享。当首次引用Brushes
属性时,会调用static
构造函数。
这就是此Style与第一个Style不同之处,即不将ItemsSource
属性设置在控件的使用处,而是此Style
预先定义了它。这是唯一的真正区别。与其复制粘贴BrushSelector
Style XAML,不如第二个示例Style命名为BrushesSelector
,它构建在原始Style之上。这类似于C++/C#继承。这可以通过令人愉悦的简单XAML轻松实现。
<Style TargetType="ComboBox" x:Key="BrushesSelector"
BasedOn="{StaticResource BrushSelector}">
<Setter Property="ItemsSource" Value="{x:Static src:BrushesToList.Brushes}"/>
</Style>
BasedOn
属性基本上表示复制此现有的Style
,而其他所有内容都是对此的添加和修改。在本例中,有一个额外的Setter
用于ItemsSource
,它引用了辅助类上的static
属性。正是这一点导致static
构造函数被运行,集合被创建和填充。
这是此Style的一个示例,显示在中间的名为BrushSel
的控件中。第一个示例仅在简单的2x2网格中显示红色、绿色和蓝色的矩形,而这个示例则在一个12x12的网格中显示了更多画笔。这显示了UniformGrid
控件的有用性,并且在没有任何额外代码的情况下,它会根据内容进行缩放——很巧妙!
此时,您可能会问:“如果像第一个示例一样在BrushSel
上设置了ItemsSource
属性,会发生什么?”它似乎会覆盖Style
中设置的值。有趣的是,行为略有不同,这取决于SelectedIndex
属性是在其之前还是之后设置。如果是在之后,ComboBox
显示一个红色矩形;如果是在之前,则不显示选定的颜色矩形。以这种方式排序,我怀疑最初SelectedItem
被应用于来自Style的值,但当它被内联指定的值替换时,SelectedItem
的值将重置为其默认值-1
。
用户控件
尝试的另一种方法是创建一个User Control。首先,我们来看一下相关的XAML。这不过是构成BrushSelector
Style的相同代码,但用作User Control的内容。如果Style
已在应用程序的资源字典中定义,那么最可能只需要指定ComboBox
以及Style
键。
<UserControl x:Class="BrushSelector.BrushSelUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:src="clr-namespace:BrushSelector"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ComboBox ItemsSource="{x:Static src:BrushesToList.Brushes}">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid/>
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type SolidColorBrush}">
<Rectangle Width="18" Height="
{Binding RelativeSource={RelativeSource Mode=Self},
Path=Width}" Margin="2" Fill="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</UserControl>
主要区别在于,这实际上是定义了一个新类,如下面的摘录所示,这是BrushSelector
命名空间中BrushSelUserControl
类的定义。
<UserControl x:Class="BrushSelector.BrushSelUserControl"
这就引出了代码隐藏。这是一个partial
类,因为它是XAML中定义的类的延续。此代码处理的主要方面是启用对User Control内容中嵌入的ComboBox
的SelectedIndex
和SelectedItem
属性的访问。这是必需的,以便其他控件可以绑定到SelectedItem
并设置初始值,如示例所示。
public partial class BrushSelUserControl : UserControl
{
public static readonly DependencyProperty SelectedIndexProperty;
public static readonly DependencyProperty SelectedItemProperty;
static BrushSelUserControl()
{
SelectedIndexProperty = DependencyProperty.Register
("SelectedIndex", typeof(int), typeof(BrushSelUserControl));
SelectedItemProperty = DependencyProperty.Register
("SelectedItem", typeof(SolidColorBrush), typeof(BrushSelUserControl));
}
public int SelectedIndex
{
get { return (int)GetValue(SelectedIndexProperty); }
set { SetValue(SelectedIndexProperty, value); }
}
public object SelectedItem
{
get
{
return GetValue(SelectedItemProperty) as SolidColorBrush;
}
set { SetValue(SelectedItemProperty, value); }
}
public BrushSelUserControl()
{
InitializeComponent();
Binding selectedIndexBinding = new Binding("SelectedIndex");
selectedIndexBinding.Source = Content;
selectedIndexBinding.Mode = BindingMode.TwoWay;
this.SetBinding(BrushSelUserControl.SelectedIndexProperty, selectedIndexBinding);
Binding selectedItemBinding = new Binding("SelectedItem");
selectedItemBinding.Source = Content;
selectedItemBinding.Mode = BindingMode.TwoWay;
this.SetBinding(BrushSelUserControl.SelectedItemProperty, selectedItemBinding);
}
通过定义两个具有与ComboBox
中的属性相同名称和类型的新依赖项属性来实现这一点。它们可以有不同的名称,但考虑到属性的意图,这似乎意义不大。如果User Control更具体,那么这将是更好的选择。为了使它们与ComboBox
上的底层依赖项属性同步,在每对依赖项属性之间建立了双向绑定。没有这一点,对ComboBox
所做的更改将不会在Ellipse
中生效,因为由它创建的绑定将目标定为User Control上的依赖项属性,而不是ComboBox
。绑定意味着当底层SelectedItem
属性更改时,它会同步到User Control中相同的依赖项属性,然后通知Ellipse
。
为了公开底层ComboBox
的属性而付出这么多努力(我不得不承认确实如此),这似乎有点过头,并引出了“为什么不直接在主XAML中使用点表示法来访问它们?”的问题。首先,这破坏了封装,因为User Control的消费者不应该知道它是由什么组成的,并且访问Content
属性非常混乱。其次,它并不完全有效
<src:BrushSelUserControl Grid.Column="1" Grid.Row="2"
x:Name="UserCtrl" VerticalAlignment="Center" Content.SelectedIndex="77"/>
<Ellipse Grid.Column="2" Grid.Row="2" Margin="5" Width="120" Height="60"
VerticalAlignment="Center" Fill="{Binding ElementName=UserCtrl,
Path=Content.SelectedItem}"/>
第二行的绑定是可以的,因为在指定绑定路径时,它不会在编译时进行检查(我假设是通过反射),而在第一行中,使用Content.SelectedIndex="77"
(上面划线部分)会导致编译错误。这是因为只能设置依赖项属性,即使这映射到了依赖项属性,它在编译时也是未知的。我猜测这可以替换为绑定到一个设置为77
的int
的static
实例,并且它会起作用。要更深入地了解依赖项属性以及如何从User Controls访问它们,请查看我撰写的关于这个问题的文章。
结论
我学到的第一件事是,不要对简单的自定义使用User Controls,尤其是当涉及依赖项属性时。鉴于WPF编程是高度数据绑定式的,尤其是在使用MVVM时,这是不可避免的。如果创建的控件确实是独一无二的,并拥有自己的一组依赖项属性(即使其中一些是镜像的),那么使用User Control可能是正确的,但如果需要简单的自定义,那么样式化要简单得多。虽然User Control似乎是需要多个实例的理想载体,但WPF的样式化支持也使这变得轻而易举。
参考文献
历史
- 2011年7月17日:初始版本