WPF 自定义枚举编辑控件
动机
GUI 开发的必备技能之一肯定是自定义控件,那么在 WPF 开发中,如何编写自定义控件呢?通过本文章,我将分享:
技术难度等级
初级。这是一篇入门级文章,旨在介绍如何快速方便的定义一个符合自己需要的简单控件,它甚至不需要一个 ViewModel。
实践
目标
生成一个枚举编辑控件,当该控件绑定到某个 ViewModel 的某个枚举类型的属性(Property)时,该控件可以自动遍历该枚举的全部项目,并填充到一个下拉框中。用户可以通过选择下拉框内的项目,完成对枚举类型的编辑。
前期准备
为枚举项准备一个修饰特性
为了使枚举项可以拥有更好的可读性,我们定义一个特性(Attribute)。
csharp
1
2
3
4
5
6
7
8
9
10
|
[AttributeUsage(AttributeTargets.Field)]
public class DisplayTextAttribute : Attribute
{
public string Text { get; }
public DisplayTextAttribute(string text)
{
Text = text;
}
}
|
定义下拉框项目的数据对象
csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public class EnumItem : INotifyPropertyChanged
{
private bool _isSelected;
public object Value { get; set; }
public string DisplayText { get; set; }
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
|
为枚举提供一个封装成下拉框数据对象的扩展方法
csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public static class EnumExtensions
{
public static List<EnumItem> ToEnumItems(this Type enumType)
{
if (enumType == null || !enumType.IsEnum)
{
return new List<EnumItem>();
}
var items = new List<EnumItem>();
var fields = enumType.GetFields(BindingFlags.Public | BindingFlags.Static);
foreach (var field in fields)
{
var value = field.GetValue(null);
var displayAttr = field.GetCustomAttribute<DisplayTextAttribute>();
var text = displayAttr?.Text ?? field.Name;
var item = new EnumItem
{
Value = value,
DisplayText = text
};
items.Add(item);
}
return items;
}
}
|
创建自定义控件
可以简单的通过 IDE 的菜单内的“添加”功能,添加一个用户控件(UserControl)。
XAML 文件
控件构成很单纯,只包含一个 ComboBox,但不包含项目定义,因为我们要动态添加它们。
xml
1
2
3
4
5
6
7
8
9
10
11
12
|
<UserControl x:Class="WpfStudy.EnumEditor"
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"
mc:Ignorable="d"
d:DesignHeight="30" d:DesignWidth="150">
<ComboBox x:Name="EnumComboBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
</ComboBox>
</UserControl>
|
CS 文件
csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfStudy;
public partial class EnumEditor : UserControl
{
private bool _isUpdating;
private Type _enumType;
public EnumEditor()
{
InitializeComponent();
}
}
|
添加自定义属性
csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public static readonly DependencyProperty EnumValueProperty = DependencyProperty.Register(
nameof(EnumValue),
typeof(object),
typeof(EnumEditor),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnEnumValueChanged));
public object EnumValue
{
get => GetValue(EnumValueProperty);
set => SetValue(EnumValueProperty, value);
}
private static void OnEnumValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var enumEditor = (EnumEditor)d;
enumEditor.UpdateItems(); // 当值发生改变时,更新选项状态。
}
private void UpdateItems()
{
if (_isUpdating || EnumValue == null) return;
var type = EnumValue.GetType();
if (!type.IsEnum) return;
// 当枚举类型发生变化时(比如第一次赋值),初始化下拉框内的选项。
if (_enumType != type)
{
_enumType = type;
InitializeEnumItems();
}
SyncSelection();
}
|
初始化选项
遍历枚举的所有项目,生成下拉框选项,并在选项被选中后,将当前控件的值设置为选中的枚举项。
csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
private void InitializeEnumItems()
{
var items = _enumType.ToEnumItems();
EnumComboBox.ItemsSource = items;
// 直接显示文字
var template = new DataTemplate();
var factory = new FrameworkElementFactory(typeof(TextBlock));
factory.SetBinding(TextBlock.TextProperty, new Binding("DisplayText"));
template.VisualTree = factory;
EnumComboBox.ItemTemplate = template;
// 关键:设置 TextPath,这样 ComboBox 才知道选中 EnumItem 后,
// 文本框里应该显示 EnumItem 的哪个属性。
TextSearch.SetTextPath(EnumComboBox, nameof(EnumItem.DisplayText));
EnumComboBox.SelectionChanged += OnSingleSelectionChanged;
}
private void OnSingleSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!_isUpdating && EnumComboBox.SelectedItem is EnumItem selectedItem)
{
_isUpdating = true;
EnumValue = selectedItem.Value;
_isUpdating = false;
}
}
|
同步选项
csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private void SyncSelection()
{
_isUpdating = true;
var items = EnumComboBox.ItemsSource as IEnumerable<EnumItem>;
if (items == null)
{
_isUpdating = false;
return;
}
var selected = items.FirstOrDefault(i => Equals(i.Value, EnumValue));
EnumComboBox.SelectedItem = selected;
_isUpdating = false;
}
|
验证
定义一个测试用的枚举
csharp
1
2
3
4
5
6
7
8
9
|
public enum Status
{
[DisplayText("未开始")]
NotStarted,
[DisplayText("进行中")]
InProgress,
[DisplayText("已完成")]
Completed
}
|
定义一个测试用 ViewModel
csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class MainViewModel : INotifyPropertyChanged
{
private Status _currentStatus = Status.NotStarted;
public Status CurrentStatus
{
get => _currentStatus;
set { _currentStatus = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
|
创建一个测试窗体
XAML 文件
xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<Window x:Class="WpfStudy.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfStudy"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel Margin="20">
<TextBlock Text="单选枚举 (Status):" Margin="0,0,0,5"/>
<local:EnumEditor EnumValue="{Binding CurrentStatus}" Margin="0,0,0,20"/>
<TextBlock Text="当前状态:" FontWeight="Bold"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Status: "/>
<TextBlock Text="{Binding CurrentStatus}"/>
</StackPanel>
</StackPanel>
</Window>
|
CS 文件
csharp
1
2
3
4
5
6
7
8
|
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
|
动作确认
窗体启动后,可以看到一个下拉框,当点击下拉框后,可以看到里面我们定义的三个枚举项,当我们选中其中一个,在“当前状态”下面会显示当前被选中的枚举项。
结语
本文章所分享的只是最简单的枚举编辑器,对于复杂情况则需要更多的代码来完善逻辑,例如以 [Flags] 修饰的枚举,它的值是可以叠加多个枚举项的,这种情况下就不能用单选下拉框的方式编辑了,但已不在本次讨论范围之内了。
通过本文章,相信可以了解创建自定义控件最基本的方式,希望可以帮助到工作中有需要的人。
comments powered by