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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (3投票s)

2011 年 7 月 18 日

CPOL

9分钟阅读

viewsIcon

28109

downloadIcon

904

本文介绍了使用WPF创建通过ComboBox选择SolidColorBrush控件的几种技术,并进行了比较和对比。

Sample Image - maximum width is 600 pixels

引言

在我编写的一个WPF程序中,我需要一个简单的控件来从一个集合中选择一个SolidColorBrush。实际上,我需要多个实例访问不同的集合。期望的格式是一个ComboBox,当展开时,颜色以网格形式显示。考虑到这些需求,我知道需要某种形式的通用控件。因此,我决定尝试StyleUserControl。本文将介绍这两种方法和几个可用的实现。

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,并且只能在直接引用键时使用。通过查找第一个名为RGBBrushSelComboBox可以看到这一点。除非打算普遍应用Style,否则我认为最好将其设计成只能显式使用。

由于需要自定义ComboBox的显示,Style包含两个设置器。一个用于更改项目显示的Panel,另一个用于更改实际项目的外观。

前者需要设置ItemsPanelTemplate。这被简单地设置为一个UniformGrid。这种Panel的优点在于它能以一种保持布局均匀的方式分配行和列。这在顶部的图片中有所展示。

在指定了网格之后,我们希望每种颜色都显示为一个小的矩形。这是通过创建一个DataTemplate来实现的,该模板应用于ItemTemplate属性。它只是绘制一个18x18的Rectangle。这里的巧妙之处在于Rectangle是用当前选定的画笔填充的。这很容易通过(数据)绑定到自身来实现。绑定声明如此简洁的原因是,i.e.

Fill="{Binding}"

是由于DataTemplate被类型化为SolidColorBrush,这相当于它的数据上下文,并且没有指定路径,因为数据*就是*一个SolidColorBrush,这正是RectangleFillProperty所需的类型。这其中还有更多的魔法:此模板既用于在UniformGrid中显示项目,也用于显示SelectedItem,即ComboBox处于折叠状态时。

除了在RGBBrushSel的定义中指定Style之外,还指定了ItemsSource属性。这是控件应显示的SolidColorBrush集合。因此,Style不绑定到任何特定的SolidColorBrush集合。这一点很重要,以便允许多个具有此StyleComboBox实例,但显示不同的集合。在本例中,示例使用了定义在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;
	}
}

使用反射来查找所有staticpublic属性方法。然后检查这些属性是否确实返回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内容中嵌入的ComboBoxSelectedIndexSelectedItem属性的访问。这是必需的,以便其他控件可以绑定到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"(上面划线部分)会导致编译错误。这是因为只能设置依赖项属性,即使这映射到了依赖项属性,它在编译时也是未知的。我猜测这可以替换为绑定到一个设置为77intstatic实例,并且它会起作用。要更深入地了解依赖项属性以及如何从User Controls访问它们,请查看我撰写的关于这个问题的文章

结论

我学到的第一件事是,不要对简单的自定义使用User Controls,尤其是当涉及依赖项属性时。鉴于WPF编程是高度数据绑定式的,尤其是在使用MVVM时,这是不可避免的。如果创建的控件确实是独一无二的,并拥有自己的一组依赖项属性(即使其中一些是镜像的),那么使用User Control可能是正确的,但如果需要简单的自定义,那么样式化要简单得多。虽然User Control似乎是需要多个实例的理想载体,但WPF的样式化支持也使这变得轻而易举。

参考文献

历史

  • 2011年7月17日:初始版本
© . All rights reserved.