WPF 中的多选 ComboBox






4.92/5 (46投票s)
逐步创建多选组合框用户控件。
- 下载 MultiselectCombo_MVVM_noexe - 45.4 KB
- 下载 MultiselectCombo_MVVM - 91.4 KB
- 下载 MultiselectCombo_Codebehind_noexe - 40.4 KB
- 下载 MultiselectCombo_Codebehind - 84.8 KB
引言
最近,在我们的项目中,我们希望允许用户在列表中选择多个值。但列表应该填充在网格行内。所以我们不想使用列表框,我们也不对第三方工具感兴趣。取而代之的是,我们想使用一个多选组合框。当我浏览各种博客、论坛等时,我得到了一些不错的代码,但没有一个代码与 MVVM 模式一起工作。在那些文章中,大多数数据源绑定都是在代码隐藏中完成的。所以我对现有的代码进行了一些更改以支持 MVVM。在本文中,我将逐步解释如何创建多选组合框用户控件。本文还将帮助最近开始学习 WPF 的人,因为我解释了如何创建样式和依赖属性。
Using the Code
创建一个新的 WPF 用户控件库。将用户控件重命名为 MultiSelectComboBox
。
为了创建多选组合框,我们需要分析构建这样一个控件需要什么。我们需要一个组合框,并且组合框下拉列表中的每个项目都需要添加一个复选框。由于我们将为组合框项目编写自定义数据模板,因此我们不能直接使用组合框。我们可以做的是,除了定义项目模板之外,还要精心设计组合框的模板。
我们的组合框应该看起来像下面的样子
为了实现这一点,正如我在上图中所提到的,我们需要一个切换按钮来确定是打开/关闭下拉列表,以及显示选定的值。我们需要一个弹出控件,我们在其中显示所有带有复选框的项目。所有这些组合在一起就构成了我们的自定义多选组合框控件。
我现在将把文章分成三个部分
- 定义样式和创建 XAML 文件
- 在用户控件的代码隐藏中添加依赖属性和其他对象
- 在其他 XAML 应用程序中使用此多选组合框 DLL
定义样式和模板
删除网格标签并在用户控件中添加组合框。
<ComboBox
x:Name="MultiSelectCombo"
SnapsToDevicePixels="True"
OverridesDefaultStyle="True"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="True"
IsSynchronizedWithCurrentItem="True"
>
为上述组合框添加一个项目模板。项目模板应该是一个复选框。将复选框的内容属性绑定到某个属性 Title
。请记住,我们还没有开始编写代码隐藏。我们必须在代码隐藏中设置此属性。将复选框的 Ischecked
属性绑定到组合框的 IsSelected
属性。
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Title}"
IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
Tag="{RelativeSource FindAncestor,
AncestorType={x:Type ComboBox}}"
/>
</DataTemplate>
</ComboBox.ItemTemplate>
在组合框的控件模板中添加一个网格,并包含一个切换按钮和一个弹出窗口,如下所示
<ComboBox.Template>
<ControlTemplate TargetType="ComboBox">
<Grid >
<ToggleButton
x:Name="ToggleButton"
Grid.Column="2" IsChecked="
{Binding Path=IsDropDownOpen,Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}"
Focusable="false"
ClickMode="Press" HorizontalContentAlignment="Left" >
<ToggleButton.Template>
<ControlTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="18"/>
</Grid.ColumnDefinitions>
<Border
x:Name="Border"
Grid.ColumnSpan="2"
CornerRadius="2"
Background="White"
BorderBrush="Black"
BorderThickness="1,1,1,1" />
<Border
x:Name="BorderComp"
Grid.Column="0"
CornerRadius="2"
Margin="1"
Background="White"
BorderBrush="Black"
BorderThickness="0,0,0,0" >
<TextBlock Text="
{Binding Path=Text,RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=UserControl}}"
Background="White"
Padding="3" />
</Border>
<Path
x:Name="Arrow"
Grid.Column="1"
Fill="Black"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 0 L 4 4 L 8 0 Z"/>
</Grid>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<Popup
Name="Popup"
Placement="Bottom"
AllowsTransparency="True"
Focusable="False" >
<Grid
Name="DropDown"
SnapsToDevicePixels="True"
<Border
x:Name="DropDownBorder"
BorderThickness="1" Background="White"
BorderBrush="Black"/>
<ScrollViewer Margin="4,6,4,6"
SnapsToDevicePixels="True"
DataContext="{Binding}">
<StackPanel IsItemsHost="True"
KeyboardNavigation.DirectionalNavigation=
"Contained" />
</ScrollViewer>
</Grid>
</Popup>
</Grid>
</ControlTemplate>
</ComboBox.Template>
我们已经添加了组合框模板,但尚未设置弹出窗口和切换按钮之间的关系。我们将在此处使用模板绑定概念。
TemplateBinding
类似于普通数据绑定,但它只能用于模板。这里切换按钮充当模板父项,我们将通过为弹出窗口的 IsOpen
属性指定模板绑定来将切换按钮的 IsChecked
属性绑定到弹出窗口。一旦您看到下面的代码,您将对其有更深入的了解。
在切换按钮中添加以下属性
IsChecked="{Binding Path=IsDropDownOpen,
Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
在弹出窗口中添加以下代码
IsOpen="{TemplateBinding IsDropDownOpen}"
PopupAnimation="Slide"
我们使用一个通用属性 IsDropDownOpen
来设置两个对象的绑定,并在切换按钮中将其设置为双向,以便每当弹出窗口关闭时,切换按钮的 IsChecked
属性将设置为 false
。此外,我们将 PopupAnimation
设置为 Slide
。
我们现在还需要做一件事。弹出窗口的宽度应与组合框的宽度匹配,并且下拉列表的高度也应设置。
在弹出窗口中的网格中也添加以下属性。
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
现在,在控件模板关闭标签之前,让我们为组合框添加一些触发器。
- 每当组合框中没有项目时,我们都需要为弹出窗口设置一些最小高度。
- 为下拉列表弹出窗口设置一些圆角半径
<ControlTemplate.Triggers>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="DropDownBorder"
Property="MinHeight" Value="95"/>
</Trigger>
</ControlTemplate.Triggers>
<Trigger SourceName="Popup"
Property="Popup.AllowsTransparency" Value="true">
<Setter TargetName="DropDownBorder"
Property="CornerRadius" Value="4"/>
<Setter TargetName="DropDownBorder"
Property="Margin" Value="0,2,0,0"/>
</Trigger>
现在我们基本完成了 XAML 页面。如果需要任何样式更改,我们将返回 XAML。
第二步是在代码隐藏中添加依赖属性。大多数 WPF 开发人员已经了解依赖属性。但对于初学者来说,依赖属性是一种特殊的属性,当我们调用 getvalue()
方法时,它会动态地从依赖对象获取值。当我们为依赖属性设置值时,它不会存储在对象中的字段里,而是存储在依赖对象基类提供的键的字典中。
我将添加四个依赖属性
ItemSource
SelectedItems
DefaultText
文本
这四个属性就足够了,但如果您需要任何其他依赖属性,您可以添加自己的。
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(Dictionary<string, object>),
typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems",
typeof(Dictionary<string, object>),
typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text",
typeof(string), typeof(MultiSelectComboBox),
new UIPropertyMetadata(string.Empty));
public static readonly DependencyProperty DefaultTextProperty =
DependencyProperty.Register("DefaultText", typeof(string),
typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public Dictionary<string, object> ItemsSource
{
get { return (Dictionary<string,
object>)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
}
}
public Dictionary<string, object> SelectedItems
{
get { return (Dictionary<string,
object>)GetValue(SelectedItemsProperty); }
set
{
SetValue(SelectedItemsProperty, value);
}
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public string DefaultText
{
get { return (string)GetValue(DefaultTextProperty); }
set { SetValue(DefaultTextProperty, value); }
}
注意:我将 ItemSource
和 SelectedItems
属性都设置为 dictionary
对象,因为对于组合框的绑定,简单的键值对就足够了。但是您可以使用对象列表而不是 dictionary
。
在同一个命名空间中创建一个名为“Node
”的类,其中包含两个属性
标题
IsSelected
请记住,我已经告诉您,我们将复选框的内容属性绑定到 Title
。
public class Node
{
public Node(string title)
{
Title = title;
}
public string Title { get; set; }
public bool IsSelected { get; set; }
}
现在,添加一个名为 _nodelist
的类的一个 Observablecollection
。
private ObservableCollection<Node> _nodeList;
在上面的字段的构造函数中设置值。
_nodeList = new ObservableCollection<Node>();
添加一个名为 DisplayInControl
的方法。此方法将显示依赖属性 ItemsSource
中的项目。
private void DisplayInControl()
{
_nodeList.Clear();
if (this.ItemsSource.Count > 0)
_nodeList.Add(new Node("All"));
foreach (KeyValuePair<string, object> keyValue in this.ItemsSource)
{
Node node = new Node(keyValue.Key);
_nodeList.Add(node);
}
MultiSelectCombo.ItemsSource = _nodeList;
}
每当有多个项目时,我们都会添加一个额外的值 All
。对于 ItemsSource
中的每个项目,我们都将键添加到 nodelist。最后,我们将此 nodelist 设置为我们命名为 MultiSelectCombo
的组合框的 ItemsSource
。但是我们如何使用这个方法呢?所以,在依赖属性中,我们必须设置 valuechanged
属性。像下面这样重写 ItemSource
的依赖属性
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(Dictionary<string,
object>), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(MultiSelectComboBox.OnItemsSourceChanged)));
在 itemsSourceChanged
事件中,调用 DisplayInControl
方法。
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox control = (MultiSelectComboBox)d;
control.DisplayInControl();
}
我们已经添加了一个 PropertyChanged
事件。下一步将是每当下拉列表中选中一个项目时。我们必须将其设置为 SelectedItems
,并且还必须在组合框中显示它。如果有多个值,我们必须以逗号分隔的格式显示该项目,如果选择了所有值,我们将 All
设置为下拉列表中的文本,并选中所有项目。
在复选框中添加一个 Click
事件
<CheckBox Content="{Binding Title}"
IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
Tag="{RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}"
Click="CheckBox_Click" />
在代码隐藏中,添加逻辑以将 nodelist 中每个节点的 IsSelected
属性设置为 true
(如果它们被选中)。
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
CheckBox clickedBox = (CheckBox)sender;
if (clickedBox.Content == "All")
{
if (clickedBox.IsChecked.Value)
{
foreach (Node node in _nodeList)
{
node.IsSelected = true;
}
}
else
{
foreach (Node node in _nodeList)
{
node.IsSelected = false;
}
}
}
else
{
int _selectedCount = 0;
foreach (Node s in _nodeList)
{
if (s.IsSelected && s.Title != "All")
_selectedCount++;
}
if (_selectedCount == _nodeList.Count - 1)
_nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = true;
else
_nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = false;
}
SetSelectedItems();
}
添加以下方法来设置选定的项目
private void SetSelectedItems()
{
if (SelectedItems == null)
SelectedItems = new Dictionary<string, object>();
SelectedItems.Clear();
foreach (Node node in _nodeList)
{
if (node.IsSelected && node.Title != "All")
{
if (this.ItemsSource.Count > 0)
SelectedItems.Add(node.Title, this.ItemsSource[node.Title]);
}
}
}
即使我们为每个节点都设置了 IsSelected =true
,在 UI 中,当您选中“All”选项时,其他复选框仍然不会被选中。这是因为我们没有为 Node 类中的属性设置属性更改通知事件。现在我们重新访问 Node
类并实现 INotifyPropertyChanged
接口。
public class Node : INotifyPropertyChanged
{
private string _title;
private bool _isSelected;
#region ctor
public Node(string title)
{
Title = title;
}
#endregion
#region Properties
public string Title
{
get
{
return _title;
}
set
{
_title = value;
NotifyPropertyChanged("Title");
}
}
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
_isSelected = value;
NotifyPropertyChanged("IsSelected");
}
}
#endregion
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
现在我们的控件还剩两个任务。在组合框中显示选定的项目,并且当我们加载页面并有一些选定的值时,我们默认必须选中它们以通知用户。
下面的方法将在切换按钮内容中设置文本
private void SetText()
{
if (this.SelectedItems != null)
{
StringBuilder displayText = new StringBuilder();
foreach (Node s in _nodeList)
{
if (s.IsSelected == true && s.Title == "All")
{
displayText = new StringBuilder();
displayText.Append("All");
break;
}
else if (s.IsSelected == true && s.Title != "All")
{
displayText.Append(s.Title);
displayText.Append(',');
}
}
this.Text = displayText.ToString().TrimEnd(new char[] { ',' });
}
// set DefaultText if nothing else selected
if (string.IsNullOrEmpty(this.Text))
{
this.Text = this.DefaultText;
}
}
我们必须在复选框点击事件的末尾调用上面的方法(SetText
),以便每当 SelectedItems
更改时,都会调用该方法。
现在添加一个方法,根据 SelectedItems
设置每个节点的 IsSelected
属性。我们需要这个方法来预填充页面加载时的选定项目。
private void SelectNodes()
{
foreach (KeyValuePair<string, object> keyValue in SelectedItems)
{
Node node = _nodeList.FirstOrDefault(i => i.Title == keyValue.Key);
if (node != null)
node.IsSelected = true;
}
}
我们需要修改 SelectedItemsProperty
以包含属性更改事件,并在事件中调用 selectNodes
方法和 SetText
方法。
所以我们的依赖属性将变为
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register
("SelectedItems", typeof(Dictionary<string, object>),
typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback
(MultiSelectComboBox.OnSelectedItemsChanged)));
private static void OnSelectedItemsChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox control = (MultiSelectComboBox)d;
control.SelectNodes();
control.SetText();
}
现在我们已经添加了大部分基本功能,是时候在应用程序中使用该控件了。
添加一个新的 WPF 应用程序项目,并命名为 MultiSelectDemo
。我在这里不解释 MVVM 模式。您可以找到关于使用 MVVM 创建简单 WPF 应用程序的各种文章。我将跳过这部分,直接带您到我们的控件实现。添加一个具有 InotifyProperyChanged
接口实现的 viewmodelbase
。添加一个继承自 ViewModelBase
的 viewmodel
,它应该有一个 Items
和一个 SelectedItems
字典属性。为了测试我们的控件,我在构造函数中添加了以下代码
Items = new Dictionary<string, object>();
Items.Add("Chennai", "MAS");
Items.Add("Trichy", "TPJ");
Items.Add("Bangalore", "SBC");
Items.Add("Coimbatore", "CBE");
SelectedItems = new Dictionary<string, object>();
SelectedItems.Add("Chennai", "MAS");
SelectedItems.Add("Trichy", "TPJ");
现在将 WPF 用户控件库项目添加为该应用程序的引用,这样您就可以在此处看到用户控件库项目的命名空间。在视图中,我包含了一个对用户控件库的引用。
xmlns:control="clr-namespace:
MultiSelectComboBox;assembly=MultiSelectComboBox"
在网格内部,我添加了我们的控件,并带有以下属性
ItemsSource
SelectedItems
宽度
高度
请记住,我们在控件中没有设置任何 Width
和 Height
属性。这是因为在不同的地方我们需要不同的宽度和高度。所以我总是建议您将其添加到视图中。
<control:MultiSelectComboBox
Width="200" Height="30"
ItemsSource="{Binding Items}"
SelectedItems="{Binding SelectedItems}"
x:Name="MC" />
现在我们可以看到我们的 MultiSelectCombobox
工作正常,并且我们可以选择不同的项目。我们也可以通过绕过 MVVM 的代码隐藏来使用此控件。我还将向您展示一个如何从 XAML 文件代码隐藏中使用它的示例。我们必须从 XAML 中删除 ItemsSource
和 SelectedItems
属性。在代码隐藏构造函数中添加相同的测试数据。
最后,将 ItemsSource
和 SelectedItems
属性设置如下
public MainWindow()
{
InitializeComponent();
Items = new Dictionary<string, object>();
Items.Add("Chennai", "MAS");
Items.Add("Trichy", "TPJ");
Items.Add("Bangalore", "SBC");
Items.Add("Coimbatore", "CBE");
SelectedItems = new Dictionary<string, object>();
SelectedItems.Add("Chennai", "MAS");
SelectedItems.Add("Trichy", "TPJ");
MC.ItemsSource = Items;
MC.SelectedItems = SelectedItems;
}
扩展 - 1 (工具提示)
如果组合框的宽度很小,并且您选择了多个项目,那么您将无法在文本中看到所有项目。所以我们必须设置工具提示,以便当用户将鼠标悬停在组合框上时,他/她应该能够看到所有选定的项目。在下面的代码中,我将组合框的文本绑定到工具提示。
ToolTip="{Binding Path=Text, RelativeSource={RelativeSource Self}}"
XAML 中的组合框控件现在看起来像这样
<control:MultiSelectComboBox Width="100" Height="30"
ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems}"
x:Name="MC" ToolTip="{Binding Path=Text, RelativeSource={RelativeSource Self}}"/>
请检查附件的演示文件。希望这份文档和附件能帮到您。请与您的朋友分享这份文档,并提供您宝贵的反馈。
扩展 - 2 取消选中组合框中的“All”项
当您单击“All”选项复选框时,该组合框中的所有项目都应被选中。但是反向场景,即取消选中“All”应该取消选中所有选定的项目,而事实并非如此。 我通过在 checkbox_click 方法中使用以下代码解决了这个问题。
如果 clickedbox 被选中,我们需要选中所有节点,如果 clickedbox 被取消选中,我们需要取消选中所有节点。
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
CheckBox clickedBox = (CheckBox)sender;
if (clickedBox.Content == "All" )
{
if (clickedBox.IsChecked.Value)
{
foreach (Node node in _nodeList)
{
node.IsSelected = true;
}
}
else
{
foreach (Node node in _nodeList)
{
node.IsSelected = false;
}
}
}
else
{
int _selectedCount = 0;
foreach (Node s in _nodeList)
{
if (s.IsSelected && s.Title != "All")
_selectedCount++;
}
if (_selectedCount == _nodeList.Count - 1)
_nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = true;
else
_nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = false;
}
SetSelectedItems();
SetText();
}