惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

老高的博客

2025年度开发趋势有感 - 老高的博客 2025 年终总结 - 老高的博客 MVVM 模式是什么? - 老高的博客 2024 年终总结 - 老高的博客 《飞驰人生2》观后感 - 老高的博客 2023 年终总结 - 老高的博客 项目总结:前后端分离的公司主页站点 - 老高的博客 C# 版本特性记录 - 老高的博客 2022 年终总结 - 老高的博客 在树莓派的 openSUSE 上安装 Nginx - 老高的博客 在树莓派的 openSUSE 上安装 .NET 6 2021 年终总结 - 老高的博客 从零开始学Python - 02.容器类型 - 老高的博客 让文章更生动 - 老高的博客 2020 年终总结 - 老高的博客 2019 年终总结 - 老高的博客 从零开始学Python - 01.基本类型 - 老高的博客 npm入门 - 老高的博客 为站点添加把锁 - 老高的博客
WPF 自定义枚举编辑控件 - 老高的博客
2026-01-08 · via 老高的博客

动机

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