WPF 参数设置界面按模型字段自动生成设置界面

目录

1.定义参数数据模型

2.定义按类型返回控件的转换类

3.按数据模型来生成自定义表格列


有时候程序在做参数界面时,需要对参数模型中的字段一一定义控件,来做数据绑定,再进行参数设置的保存。每当新增参数字段时,都需要去修改参数界面的控件。

实现按参数字段自动生成界面元素的思路是:类的反射机制,再通过特性来标定参数的类型,再返回对应的数据控件,加载到数据表格中。这样当我修改参数模型时,界面加载时也会自动的按数据模型来生成控件,就不需要再去修改界面的样式了。

目前支持的控件模式,可扩展:

cs 复制代码
   /// <summary>
   /// 配置输入类型
   /// 按类型动态添加控件
   /// </summary>
   public enum ConfigInputType
   {
       TextBox = 0,
       Combox = 1,
       CheckBox = 2,
       IP = 3,
       Int=4,
   }

界面的样子:

期间使用到的一个控件包:

Nuget 搜索 Rotion 就都可以找到

1.定义参数数据模型

首先,定义数据特性和支持的控件类型:

有参数名,是否隐藏(隐藏的话就不再界面显示出来),还有就是输入类型

cs 复制代码
   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,
   }

再来定义参数对应的数据模型:

cs 复制代码
    /// <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,然后再赋值的时候,需要给下拉数据源添加上(下面代码会介绍)

cs 复制代码
 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)

cs 复制代码
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>

XML 复制代码
<Page
    x: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>
            <StackPanel
                Grid.Row="0"
                Margin="10,0,0,0"
                HorizontalAlignment="Left"
                Orientation="Horizontal">
                <uc:CommonButton Command="{Binding SaveCommand}" Content="保  存 参 数" />
            </StackPanel>

            <Metro:AduDataGrid
                x: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>
                    <DataGridTextColumn
                        Width="150"
                        Binding="{Binding PropertyName}"
                        Header="属性名" />
                    <DataGridTextColumn
                        Width="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根据属性名返回对应的下拉数据源

cs 复制代码
       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);

cs 复制代码
        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代码:

cs 复制代码
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(); }
        }


    }
}