当前位置: 首页 > news >正文

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(); }}}
}

http://www.dtcms.com/a/350500.html

相关文章:

  • Docker:网络连接
  • python面试题目100个(更新中预计10天更完)
  • 深度学习(二):数据集定义、PyTorch 数据集定义与使用(分板块解析)
  • 决策树原理与 Sklearn 实战
  • 【动手学深度学习】7.1. 深度卷积神经网络(AlexNet)
  • 0825 http梳理作业
  • 【慕伏白】CTFHub 技能树学习笔记 -- Web 之信息泄露
  • Linux多线程[生产者消费者模型]
  • python项目中pyproject.toml是做什么用的
  • 【Canvas与标牌】维兰德汤谷公司logo
  • Hadoop MapReduce Task 设计源码分析
  • java-代码随想录第十七天| 700.二叉搜索树中的搜索、617.合并二叉树、98.验证二叉搜索树
  • C++ STL 专家容器:关联式、哈希与适配器
  • 《微服务架构下API网关流量控制Bug复盘:从熔断失效到全链路防护》
  • 精准测试的密码:解密等价类划分,让Bug无处可逃
  • 【C语言16天强化训练】从基础入门到进阶:Day 11
  • 朴素贝叶斯算法总结
  • 互联网大厂Java面试实录:Spring Boot与微服务架构解析
  • cmd命令行删除文件夹
  • rk3566编译squashfs报错解决
  • QT5封装的日志记录函数
  • 算法练习-遍历对角线
  • 开源夜莺里如何引用标签和注解变量
  • VTK开发笔记(四):示例Cone,创建圆锥体,在Qt窗口中详解复现对应的Demo
  • 使用Cloudflare的AI Gateway代理Google AI Studio
  • 论文阅读:Code as Policies: Language Model Programs for Embodied Control
  • Redis的单线程和多线程
  • Linux_用 `ps` 按进程名过滤线程,以及用 `pkill` 按进程名安全杀进程
  • 记一次RocketMQ消息堆积
  • (二十二)深入了解AVFoundation-编辑:视频变速功能-实战在Demo中实现视频变速