WPF 参数设置界面按模型字段自动生成设置界面
目录
1.定义参数数据模型
2.定义按类型返回控件的转换类
3.按数据模型来生成自定义表格列
有时候程序在做参数界面时,需要对参数模型中的字段一一定义控件,来做数据绑定,再进行参数设置的保存。每当新增参数字段时,都需要去修改参数界面的控件。
实现按参数字段自动生成界面元素的思路是:类的反射机制,再通过特性来标定参数的类型,再返回对应的数据控件,加载到数据表格中。这样当我修改参数模型时,界面加载时也会自动的按数据模型来生成控件,就不需要再去修改界面的样式了。
目前支持的控件模式,可扩展:
/// <summary>/// 配置输入类型/// 按类型动态添加控件/// </summary>public enum ConfigInputType{TextBox = 0,Combox = 1,CheckBox = 2,IP = 3,Int=4,}
界面的样子:
期间使用到的一个控件包:
Nuget 搜索 Rotion 就都可以找到
1.定义参数数据模型
首先,定义数据特性和支持的控件类型:
有参数名,是否隐藏(隐藏的话就不再界面显示出来),还有就是输入类型
public class ConfigDescribeAttribute : ValidationAttribute{private ConfigAttributeModel _model = new ConfigAttributeModel();public ConfigDescribeAttribute(string name, bool isHidden = false, ConfigInputType inputType = ConfigInputType.TextBox){_model.Name = name;_model.IsHidden = isHidden;_model.InputType = inputType;}public ConfigAttributeModel GetConfigAttribute(){return _model;}public class ConfigAttributeModel{/// <summary>/// 名称/// </summary>public string Name { get; set; }/// <summary>/// 是否隐藏/// </summary>public bool IsHidden { get; set; } = false;/// <summary>/// 数据输入类型/// </summary>public ConfigInputType InputType { get; set; } = ConfigInputType.TextBox;}}/// <summary>/// 配置输入类型/// 按类型动态添加控件/// </summary>public enum ConfigInputType{TextBox = 0,Combox = 1,CheckBox = 2,IP = 3,Int=4,}
再来定义参数对应的数据模型:
/// <summary>/// 配置文件/// </summary>public class P_Environment{/// <summary>/// 主题色 十六进制/// </summary>[ConfigDescribe("主题色", true)]public string ThemeColor { get; set; }/// <summary>/// 是否开机自启/// </summary>[ConfigDescribe("开机自启", inputType: ConfigInputType.CheckBox)]public bool IsAutoStart { get; set; } = false;/// <summary>/// 延时启动/// </summary>[ConfigDescribe("启动延时", inputType: ConfigInputType.Int)]public int DelayStart { get; set; } = 0;/// <summary>/// 产品配方/// </summary>[ConfigDescribe("ProductSpec", true, inputType: ConfigInputType.Combox)]public string ProductSpec { get; set; }/// <summary>/// PLC IP/// </summary>[ConfigDescribe("PLC-IP", inputType: ConfigInputType.IP)]public string PLCIP { get; set; } = "";/// <summary>/// PLC 端口/// </summary>[ConfigDescribe("PLC-端口", inputType: ConfigInputType.Int)]public int PLCPort { get; set; } = 502;/// <summary>/// OPCAU IP/// </summary>[ConfigDescribe("OPCAU-IP", inputType: ConfigInputType.IP)]public string OPCAUIP { get; set; } = "192.168.3.1";/// <summary>/// OPCAU 端口/// </summary>[ConfigDescribe("OPCAU-端口", inputType: ConfigInputType.Int)]public int OPCAUPort { get; set; } = 4840;/// <summary>/// Camera IP/// </summary>[ConfigDescribe("扫码枪-IP", inputType: ConfigInputType.IP)]public string CameraIP { get; set; } = "192.168.1.92";/// <summary>/// Camera 端口/// </summary>[ConfigDescribe("扫码枪-端口", inputType: ConfigInputType.Int)]public int CameraPort { get; set; } = 2001;/// <summary>/// Camera Trigger/// </summary>[ConfigDescribe("扫码枪-触发字符")]public string CameraTrigger { get; set; } = "Start";/// <summary>/// MES 系统的请求地址/// </summary>[ConfigDescribe("MES 系统的请求地址")]public string MESUrl { get; set; }/// <summary>/// 线体编号/// </summary>[ConfigDescribe("线体编号")]public string ClientCode { get; set; }/// <summary>/// 清理内存间隔时间 单位分/// </summary>[ConfigDescribe("清理内存间隔时间 单位分", inputType: ConfigInputType.Int)]public int ClearMemoryTime { get; set; } = 1800;/// <summary>/// 数据库名称/// </summary>[ConfigDescribe("数据库名称")]public string DB_Name { get; set; } = "pz250521c";/// <summary>/// 数据库连接的IP/// </summary>[ConfigDescribe("数据库IP", inputType: ConfigInputType.IP)]public string DB_IP { get; set; } = "127.0.0.1";/// <summary>/// 数据库连接的端口/// </summary>[ConfigDescribe("数据库端口")]public string DB_Port { get; set; } = "3306";/// <summary>/// 数据库连接的用户名/// </summary>[ConfigDescribe("数据库用户名")]public string DB_User { get; set; } = "root";/// <summary>/// 数据库连接的用户名/// </summary>[ConfigDescribe("数据库密码")]public string DB_Password { get; set; } = "123qwe";}
还需要再定义一个 特性对应的数据模型,也就是设置界面表格对应的ItemSource,就是在读取特性数据后,保存下来进行显示的。按需自行扩展,就比如我增加了一个Combox类型的输入控件,那它需要有个数据源字段,就又增加了Combox_ItemSource,然后再赋值的时候,需要给下拉数据源添加上(下面代码会介绍)
public class ConfigSettingModel{/// <summary>/// 属性名称/// </summary>public string PropertyName { get; set; }/// <summary>/// 显示名称/// </summary>public string Name { get; set; }/// <summary>/// 值/// </summary>public object Value { get; set; }/// <summary>/// 数据输入类型/// </summary>public ConfigInputType InputType { get; set; } = ConfigInputType.TextBox;/// <summary>/// 下拉框类型的话,需要赋值下拉数据源/// </summary>public ObservableCollection<DropDownModel> Combox_ItemSource { get; set; } = new ObservableCollection<DropDownModel>();}
2.定义按类型返回控件的转换类
数据类型定义好后,界面需要一个转换器,根据不同的输入类型,返回不同的控件类型
比如 ConfigInputType.TextBox 就显示MetroTextBox控件来显示
ConfigInputType.CheckBox 就显示 LSCheckBox
所以在扩展了ConfigInputType的时候,这个转换器也需要添加对应的返回控件的实现代码,否则默认使用文本的方式显示(MetroTextBox)
using AduSkin.Controls.Metro;
using LS.WPFControlLibrary;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using WPFClient.Models.Configs;namespace WPFClient.UCControls
{public class InputTypeToControlConverter : IValueConverter{public object Convert(object value, Type targetType, object parameter, CultureInfo culture){//if (value == null) return null;ConfigInputType inputType = (ConfigInputType)value;FrameworkElement control = null;switch (inputType){case ConfigInputType.Int:case ConfigInputType.TextBox:default:var textBox = new MetroTextBox(); // 替换为实际MetroTextBox控件textBox.Width = 300;textBox.SetBinding(TextBox.TextProperty, new Binding("Value"){Mode = BindingMode.TwoWay,UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged});control = textBox;break;case ConfigInputType.CheckBox:var checkBox = new LSCheckBox(); // 替换为实际LSCheckBox控件checkBox.SetBinding(CheckBox.IsCheckedProperty, new Binding("Value"){Mode = BindingMode.TwoWay,UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged});control = checkBox;break;case ConfigInputType.IP:var ipControl = new IPControl(); // 替换为实际IPControl控件ipControl.SetBinding(IPControl.IPProperty, new Binding("Value"){Mode = BindingMode.TwoWay,UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged});control = ipControl;break;case ConfigInputType.Combox:var combox = new CommonCombox(); // 替换为实际CommonCombox控件combox.DisplayMemberPath = "Name";combox.SelectedValuePath = "Code";combox.Width = 300;combox.SetBinding(ItemsControl.ItemsSourceProperty, new Binding("Combox_ItemSource"));combox.SetBinding(Selector.SelectedValueProperty, new Binding("Value"){Mode = BindingMode.TwoWay,UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged});control = combox;break;}// 统一设置控件对齐方式if (control is Control ctrl){ctrl.HorizontalAlignment = HorizontalAlignment.Left;ctrl.VerticalAlignment = VerticalAlignment.Center;}return control;}public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){// 单向转换无需实现(控件通过绑定直接更新Value)throw new NotImplementedException();}}}
3.按数据模型来生成自定义表格列
接下来就是参数设置界面了
首先添加DataGrid作为参数数据的呈现:
先引入InputTypeConverter
然后再添加Metro:AduDataGrid 数据表格
最后再添加输入列 ,使用模板列
<DataGridTemplateColumn Width="*" Header="值">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding InputType, Converter={StaticResource InputTypeConverter}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<Pagex:Class="WPFClient.Views.Setting.SettingPage"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:Metro="clr-namespace:AduSkin.Controls.Metro;assembly=AduSkin"xmlns:cfg="clr-namespace:WPFClient.Models.Configs"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:local="clr-namespace:WPFClient.Views.Setting"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:uc="clr-namespace:LS.WPFControlLibrary;assembly=LS.WPFControlLibrary"xmlns:wpfUc="clr-namespace:WPFClient.UCControls"Title="SettingPage"d:DesignHeight="1080"d:DesignWidth="1645"Background="Snow"FontSize="23"mc:Ignorable="d"><Page.Resources><wpfUc:InputTypeToControlConverter x:Key="InputTypeConverter" /></Page.Resources><Grid><Grid><Grid.RowDefinitions><RowDefinition Height="70" /><RowDefinition Height="*" /></Grid.RowDefinitions><StackPanelGrid.Row="0"Margin="10,0,0,0"HorizontalAlignment="Left"Orientation="Horizontal"><uc:CommonButton Command="{Binding SaveCommand}" Content="保 存 参 数" /></StackPanel><Metro:AduDataGridx:Name="prd_night_table"Grid.Row="1"AutoGenerateColumns="False"CanUserAddRows="False"CanUserResizeRows="False"CanUserSortColumns="False"EnableColumnVirtualization="True"EnableRowVirtualization="True"ItemsSource="{Binding ConfigList}"ScrollViewer.CanContentScroll="True"VirtualizingPanel.IsVirtualizing="True"VirtualizingPanel.VirtualizationMode="Recycling"><Metro:AduDataGrid.Columns><DataGridTextColumnWidth="150"Binding="{Binding PropertyName}"Header="属性名" /><DataGridTextColumnWidth="300"Binding="{Binding Name}"Header="名称" /><DataGridTemplateColumn Width="*" Header="值"><DataGridTemplateColumn.CellTemplate><DataTemplate><ContentPresenter Content="{Binding InputType, Converter={StaticResource InputTypeConverter}}" /></DataTemplate></DataGridTemplateColumn.CellTemplate></DataGridTemplateColumn></Metro:AduDataGrid.Columns></Metro:AduDataGrid></Grid></Grid></Page>
下面就是VM中的数据绑定实现:
DataGrid 的数据源为: ItemsSource="{Binding ConfigList}" =》 ConfigList
页面加载后,把特性数据和对应的值加载出来
通过反射的方式,获取特性内容和数据模型的值
下拉框的输入方式的话,需要在GetComboxItemSource根据属性名返回对应的下拉数据源
public override void LoadData(){try{var cfg = GlobalData.ConfigParams;ConfigList.Clear();foreach (var propertyInfo in cfg.GetType().GetProperties()){if (propertyInfo.IsDefined(typeof(ConfigDescribeAttribute)))//如果属性上有定义该属性,此步没有构造出实例{var attribute = propertyInfo.GetCustomAttributes(typeof(ConfigDescribeAttribute))?.FirstOrDefault();if (attribute != null){var cfgAb = attribute as ConfigDescribeAttribute;var model = cfgAb.GetConfigAttribute();if (model != null && !model.IsHidden){ConfigSettingModel item = new ConfigSettingModel();item.PropertyName = propertyInfo.Name;item.Value = propertyInfo.GetValue(cfg, null);item.Name = model.Name;item.InputType = model.InputType;if (item.InputType == ConfigInputType.Combox){item.Combox_ItemSource = GetComboxItemSource(item.PropertyName);}ConfigList.Add(item);}}}}OnPropertyChanged(nameof(ConfigList));}catch (Exception ex){LogOperate.Error("LoadData 发生异常", ex);}}/// <summary>/// 根据属性名返回相应的下拉框数据源/// </summary>/// <param name="propertyName"></param>/// <returns></returns>private ObservableCollection<DropDownModel> GetComboxItemSource(string propertyName){ObservableCollection<DropDownModel> source = new ObservableCollection<DropDownModel>();try{switch (propertyName){case "ProductSpec":foreach (var p in GlobalData.FormulaDatas){source.Add(new DropDownModel(){//自由定义和赋值 Name对应的就是下拉显示的值 Code对应就是保存到配置文件的值Name = p.Name,//显示文本Code = p.ID, //实际保存的值});}break;default:break;}}catch (Exception ex){LogOperate.Error("GetComboxItemSource", ex);}return source;}
保存数据:
数据源ConfigList 在界面修改数据时,双向绑定后,也会更新到ConfigList对象中
所以还是根据反射的方式,将ConfigList中的数据保存到参数对象中,
pro.SetValue(cfg, prop.Value);
private void Save(object obj){try{var cfg = GlobalData.ConfigParams;var pros = cfg.GetType().GetProperties().ToList();foreach (var prop in ConfigList){var pro = pros.Find(x => x.Name == prop.PropertyName);if (pro != null){var attribute = pro.GetCustomAttributes(typeof(ConfigDescribeAttribute))?.FirstOrDefault();if (attribute != null){var cfgAb = attribute as ConfigDescribeAttribute;var model = cfgAb.GetConfigAttribute();if (model != null){if (model.InputType == ConfigInputType.Int){pro.SetValue(cfg, Convert.ToInt32(prop.Value));}else{pro.SetValue(cfg, prop.Value);}}}else{try{pro.SetValue(cfg, prop.Value);}catch (Exception ex){VM_MainWindow.Popup($"保存异常,{ex.Message}");}}}}GlobalData.ConfigParams = cfg;//开机自启操作if (GlobalData.ConfigParams.IsAutoStart){StartupManager startupManager = new StartupManager();if (!startupManager.IsStartupEnabled()){startupManager.EnableStartup();}}else{StartupManager startupManager = new StartupManager();if (startupManager.IsStartupEnabled()){startupManager.DisableStartup();}}var res = ConfigParamOperation.SaveConfigParam(GlobalData.ConfigParams);if (res){VM_MainWindow.Popup("保存成功");}else{VM_MainWindow.Popup($"保存失败,{res.Message}");}}catch (Exception ex){VM_MainWindow.Popup($"保存失败,{ex.Message}");LogOperate.Error("SaveCommand", ex);}}
下面是完整的VM代码:
using AduSkin.Controls.Metro;
using LS.WPF.MVVM;
using LS.WPF.MVVM.Command;
using LS.WPF.MVVM.StandardModel;
using LS.WPFControlLibrary;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using WPFClient.Models.Configs;
using WPFClient.Operation;
using WPFClient.Tools;
using WPFClient.Views.Setting;namespace WPFClient.ViewModels.Setting
{public class VM_SettingPage : BaseViewModel{public VM_SettingPage() : base(typeof(SettingPage)) { }protected override void Page_Loaded(object sender, RoutedEventArgs e){base.Page_Loaded(sender, e);}protected override void Page_Unloaded(object sender, RoutedEventArgs e){base.Page_Unloaded(sender, e);}public override void LoadData(){try{var cfg = GlobalData.ConfigParams;ConfigList.Clear();foreach (var propertyInfo in cfg.GetType().GetProperties()){if (propertyInfo.IsDefined(typeof(ConfigDescribeAttribute)))//如果属性上有定义该属性,此步没有构造出实例{var attribute = propertyInfo.GetCustomAttributes(typeof(ConfigDescribeAttribute))?.FirstOrDefault();if (attribute != null){var cfgAb = attribute as ConfigDescribeAttribute;var model = cfgAb.GetConfigAttribute();if (model != null && !model.IsHidden){ConfigSettingModel item = new ConfigSettingModel();item.PropertyName = propertyInfo.Name;item.Value = propertyInfo.GetValue(cfg, null);item.Name = model.Name;item.InputType = model.InputType;if (item.InputType == ConfigInputType.Combox){item.Combox_ItemSource = GetComboxItemSource(item.PropertyName);}ConfigList.Add(item);}}}}OnPropertyChanged(nameof(ConfigList));}catch (Exception ex){LogOperate.Error("LoadData 发生异常", ex);}}/// <summary>/// 根据属性名返回相应的下拉框数据源/// </summary>/// <param name="propertyName"></param>/// <returns></returns>private ObservableCollection<DropDownModel> GetComboxItemSource(string propertyName){ObservableCollection<DropDownModel> source = new ObservableCollection<DropDownModel>();try{switch (propertyName){case "ProductSpec":foreach (var p in GlobalData.FormulaDatas){source.Add(new DropDownModel(){//自由定义和赋值 Name对应的就是下拉显示的值 Code对应就是保存到配置文件的值Name = p.Name,//显示文本Code = p.ID, //实际保存的值});}break;default:break;}}catch (Exception ex){LogOperate.Error("GetComboxItemSource", ex);}return source;}public DelegateCommand SaveCommand{get { return new DelegateCommand(Save); }}private void Save(object obj){try{var cfg = GlobalData.ConfigParams;var pros = cfg.GetType().GetProperties().ToList();foreach (var prop in ConfigList){var pro = pros.Find(x => x.Name == prop.PropertyName);if (pro != null){var attribute = pro.GetCustomAttributes(typeof(ConfigDescribeAttribute))?.FirstOrDefault();if (attribute != null){var cfgAb = attribute as ConfigDescribeAttribute;var model = cfgAb.GetConfigAttribute();if (model != null){if (model.InputType == ConfigInputType.Int){pro.SetValue(cfg, Convert.ToInt32(prop.Value));}else{pro.SetValue(cfg, prop.Value);}}}else{try{pro.SetValue(cfg, prop.Value);}catch (Exception ex){VM_MainWindow.Popup($"保存异常,{ex.Message}");}}}}GlobalData.ConfigParams = cfg;//开机自启操作if (GlobalData.ConfigParams.IsAutoStart){StartupManager startupManager = new StartupManager();if (!startupManager.IsStartupEnabled()){startupManager.EnableStartup();}}else{StartupManager startupManager = new StartupManager();if (startupManager.IsStartupEnabled()){startupManager.DisableStartup();}}var res = ConfigParamOperation.SaveConfigParam(GlobalData.ConfigParams);if (res){VM_MainWindow.Popup("保存成功");}else{VM_MainWindow.Popup($"保存失败,{res.Message}");}}catch (Exception ex){VM_MainWindow.Popup($"保存失败,{ex.Message}");LogOperate.Error("SaveCommand", ex);}}private ObservableCollection<ConfigSettingModel> _cfgList = new ObservableCollection<ConfigSettingModel>();/// <summary>/// 配置数据集/// </summary>public ObservableCollection<ConfigSettingModel> ConfigList{get { return _cfgList; }set { _cfgList = value; OnPropertyChanged(); }}}
}