老高

优秀的产品由一行靠谱的代码开始

2017 - 2026

theme by MuteG base on mute.

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] 修饰的枚举,它的值是可以叠加多个枚举项的,这种情况下就不能用单选下拉框的方式编辑了,但已不在本次讨论范围之内了。 通过本文章,相信可以了解创建自定义控件最基本的方式,希望可以帮助到工作中有需要的人。

Tags:
comments powered by Disqus