PropertyGrid是一个很强大的控件,使用该控件做属性设置面板的一个好处就是你只需要专注于代码而无需关注UI的呈现,PropertyGrid会默认根据变量类型选择合适的控件显示。但是这也带来了一个问题,就是控件的使用变得不是特别灵活,主要表现在你无法根据你的需求很好的选择控件,比如当你需要用Slider控件来设置int型变量时,PropertyGrid默认的模板选择器是不支持的。网上找了许多资料基本都是介绍WinForm的实现方式,主要用到了IWindowFromService这个接口,并未找到合适的适合WPF的Demo,后来在参考了DEVExpress的官方Demo之后我做了一个基于WPF和DEV 16.2的PropertyGrid Demo,基本实现了上述功能。

为了实现这一点,需要自定义一个DataTemplateSeletor类,这也是本文的核心代码。
1.创建一个CustomPropertyGrid自定义控件:
<UserControl
x:Class="PropertyGridDemo.PropertyGridControl.CustomPropertyGrid"
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:dxprg="http://schemas.devexpress.com/winfx/2008/xaml/propertygrid"
xmlns:local="clr-namespace:PropertyGridDemo.PropertyGridControl"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- 资源字典 -->
<ResourceDictionary Source="../PropertyGridControl/DynamicallyAssignDataEditorsResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<!-- PropertyDefinitionStyle:定义属性描述的风格模板 -->
<!-- PropertyDefinitionTemplateSelector:定义一个模板选择器,对应一个继承自DataTemplateSelector的类 -->
<!-- PropertyDefinitionsSource:定义一个获取数据属性集合的类,对应一个自定义类(本Demo中对应DataEditorsViewModel) -->
<dxprg:PropertyGridControl
x:Name="PropertyGridControl"
Margin="24"
DataContextChanged="PropertyGridControl_DataContextChanged"
ExpandCategoriesWhenSelectedObjectChanged="True"
PropertyDefinitionStyle="{StaticResource DynamicallyAssignDataEditorsPropertyDefinitionStyle}"
PropertyDefinitionTemplateSelector="{StaticResource DynamicallyAssignDataEditorsTemplateSelector}"
PropertyDefinitionsSource="{Binding Path=Properties, Source={StaticResource DemoDataProvider}}"
ShowCategories="True"
ShowDescriptionIn="Panel" />
</Grid>
</UserControl>
该控件使用的资源字典如下:
<ResourceDictionary
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:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid"
xmlns:dxprg="http://schemas.devexpress.com/winfx/2008/xaml/propertygrid"
xmlns:local="clr-namespace:PropertyGridDemo.PropertyGridControl"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<local:DynamicallyAssignDataEditorsTemplateSelector x:Key="DynamicallyAssignDataEditorsTemplateSelector" />
<local:DataEditorsViewModel x:Key="DemoDataProvider" />
<DataTemplate x:Key="DescriptionTemplate">
<RichTextBox
x:Name="descriptionRichTextBox"
MinWidth="150"
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
Foreground="{Binding Path=(TextElement.Foreground), RelativeSource={RelativeSource TemplatedParent}}"
IsReadOnly="True"
IsTabStop="False" />
</DataTemplate>
<DataTemplate x:Key="descriptionTemplate">
<RichTextBox
x:Name="descriptionRichTextBox"
MinWidth="150"
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
Foreground="{Binding Path=(TextElement.Foreground), RelativeSource={RelativeSource TemplatedParent}}"
IsReadOnly="True"
IsTabStop="False" />
</DataTemplate>
<!-- 设置控件的全局样式和数据绑定 -->
<Style x:Key="DynamicallyAssignDataEditorsPropertyDefinitionStyle" TargetType="dxprg:PropertyDefinition">
<Setter Property="Path" Value="{Binding Name}" />
<!--<Setter Property="Header" Value="{Binding Converter={StaticResource PropertyDescriptorToDisplayNameConverter}}"/>-->
<Setter Property="Description" Value="{Binding}" />
<Setter Property="DescriptionTemplate" Value="{StaticResource descriptionTemplate}" />
</Style>
<Style x:Key="DescriptionContainerStyle" TargetType="dxprg:PropertyDescriptionPresenterControl">
<Setter Property="ShowSelectedRowHeader" Value="False" />
<Setter Property="MinHeight" Value="70" />
</Style>
<Style TargetType="Slider">
<Setter Property="Margin" Value="2" />
</Style>
<Style TargetType="dxe:ComboBoxEdit">
<Setter Property="IsTextEditable" Value="False" />
<Setter Property="ApplyItemTemplateToSelectedItem" Value="True" />
<Setter Property="Margin" Value="2" />
</Style>
<!-- 测试直接从DataTemplate获取控件 -->
<DataTemplate x:Key="SliderTemplate" DataType="local:SliderExtend">
<!--<dxprg:PropertyDefinition>
<dxprg:PropertyDefinition.CellTemplate>-->
<!--<DataTemplate>-->
<StackPanel x:Name="Root">
<Slider
Maximum="{Binding Path=Max}"
Minimum="{Binding Path=Min}"
Value="{Binding Path=Value}" />
<TextBlock Text="{Binding Path=Value}" />
</StackPanel>
<!--</DataTemplate>-->
<!--</dxprg:PropertyDefinition.CellTemplate>
</dxprg:PropertyDefinition>-->
</DataTemplate>
<DataTemplate x:Key="ComboBoxEditItemTemplate" DataType="Tuple">
<TextBlock
Height="20"
Margin="5,3,0,0"
VerticalAlignment="Center"
Text="{Binding Item1}" />
</DataTemplate>
</ResourceDictionary>
2.编写对应的模板选择类 DynamicallyAssignDataEditorsTemplateSelector:
using DevExpress.Xpf.Editors;
using DevExpress.Xpf.PropertyGrid;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace PropertyGridDemo.PropertyGridControl
{
public class DynamicallyAssignDataEditorsTemplateSelector : DataTemplateSelector
{
private PropertyDescriptor _property = null;
private RootPropertyDefinition _element = null;
private PropertyDataContext _propertyDataContext => App.PropertyGridDataContext;
/// <summary>
/// 当重写在派生类中,返回根据自定义逻辑的 <see cref="T:System.Windows.DataTemplate" /> 。
/// </summary>
/// <param name="item">数据对象可以选择模板。</param>
/// <param name="container">数据对象。</param>
/// <returns>
/// 返回 <see cref="T:System.Windows.DataTemplate" /> 或 null。默认值为 null。
/// </returns>
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
_element = (RootPropertyDefinition)container;
DataTemplate resource = TryCreateResource(item);
return resource ?? base.SelectTemplate(item, container);
}
/// <summary>
/// Tries the create resource.
/// </summary>
/// <param name="item">The item.</param>
/// <returns></returns>
private DataTemplate TryCreateResource(object item)
{
if (!(item is PropertyDescriptor)) return null;
PropertyDescriptor pd = (PropertyDescriptor)item;
_property = pd;
var customUIAttribute = (CustomUIAttribute)pd.Attributes[typeof(CustomUIAttribute)];
if (customUIAttribute == null) return null;
var customUIType = customUIAttribute.CustomUI;
return CreatePropertyDefinitionTemplate(customUIAttribute);
}
/// <summary>
/// Gets the data context.
/// </summary>
/// <param name="dataContextPropertyName">Name of the data context property.</param>
/// <returns></returns>
private object GetDataContext(string dataContextPropertyName)
{
PropertyInfo property = _propertyDataContext?.GetType().GetProperty(dataContextPropertyName);
if (property == null) return null;
return property.GetValue(_propertyDataContext, null);
}
/// <summary>
/// Creates the slider data template.
/// </summary>
/// <param name="customUIAttribute">The custom UI attribute.</param>
/// <returns></returns>
private DataTemplate CreateSliderDataTemplate(CustomUIAttribute customUIAttribute)
{
DataTemplate ct = new DataTemplate();
ct.VisualTree = new FrameworkElementFactory(typeof(StackPanel));
ct.VisualTree.SetValue(StackPanel.DataContextProperty, GetDataContext(customUIAttribute.DataContextPropertyName));
FrameworkElementFactory sliderFactory = new FrameworkElementFactory(typeof(Slider));
sliderFactory.SetBinding(Slider.MaximumProperty, new Binding(nameof(SliderUIDataContext.Max)));
sliderFactory.SetBinding(Slider.MinimumProperty, new Binding(nameof(SliderUIDataContext.Min)));
sliderFactory.SetBinding(Slider.SmallChangeProperty, new Binding(nameof(SliderUIDataContext.SmallChange)));
sliderFactory.SetBinding(Slider.LargeChangeProperty, new Binding(nameof(SliderUIDataContext.LargeChange)));
sliderFactory.SetBinding(Slider.ValueProperty, new Binding(nameof(SliderUIDataContext.Value)));
ct.VisualTree.AppendChild(sliderFactory);
FrameworkElementFactory textFacotry = new FrameworkElementFactory(typeof(TextBlock), "TextBlock");
textFacotry.SetValue(TextBlock.TextProperty, new Binding(nameof(SliderUIDataContext.Value)));
//textBoxFactory.AddHandler(TextBox.IsVisibleChanged, new DependencyPropertyChangedEventHandler(SearchBoxVisibleChanged));
ct.VisualTree.AppendChild(textFacotry);
ct.Seal();
return ct;
}
/// <summary>
/// Creates the ComboBox edit template.
/// </summary>
/// <param name="customUIAttribute">The custom UI attribute.</param>
/// <returns></returns>
private DataTemplate CreateComboBoxEditTemplate(CustomUIAttribute customUIAttribute)
{
DataTemplate template = new DataTemplate();
template.VisualTree = new FrameworkElementFactory(typeof(DockPanel));
template.VisualTree.SetValue(DockPanel.DataContextProperty, GetDataContext(customUIAttribute.DataContextPropertyName));
FrameworkElementFactory textFactory = new FrameworkElementFactory(typeof(TextBlock)) ;
textFactory.SetValue(TextBlock.TextProperty, new Binding(nameof(ComboBoxEditDataContext.Name)));
template.VisualTree.AppendChild(textFactory);
FrameworkElementFactory comboBoxEditFactory = new FrameworkElementFactory(typeof(ComboBoxEdit));
comboBoxEditFactory.SetBinding(ComboBoxEdit.ItemsSourceProperty, new Binding(nameof(ComboBoxEditDataContext.ItemSource)));
comboBoxEditFactory.SetBinding(ComboBoxEdit.EditValueProperty, new Binding(nameof(ComboBoxEditDataContext.EditValue)));
comboBoxEditFactory.SetBinding(ComboBoxEdit.SelectedIndexProperty, new Binding(nameof(ComboBoxEditDataContext.SelectedIndex)));
comboBoxEditFactory.SetValue(ComboBoxEdit.ItemTemplateProperty, (DataTemplate)_element.TryFindResource("ComboBoxEditItemTemplate"));
template.VisualTree.AppendChild(comboBoxEditFactory);
template.Seal();
return template;
}
/// <summary>
/// Creates the property definition template.
/// </summary>
/// <param name="customUIAttribute">The custom UI attribute.</param>
/// <returns></returns>
private DataTemplate CreatePropertyDefinitionTemplate(CustomUIAttribute customUIAttribute)
{
DataTemplate dataTemplate = new DataTemplate();
DataTemplate cellTemplate = null;//单元格模板
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(PropertyDefinition));
dataTemplate.VisualTree = factory;
switch (customUIAttribute.CustomUI)
{
case CustomUITypes.Slider:
cellTemplate = CreateSliderDataTemplate(customUIAttribute); break;
//cellTemplate = (DataTemplate)_element.TryFindResource("SliderTemplate");break;
case CustomUITypes.ComboBoxEit:
cellTemplate = CreateComboBoxEditTemplate(customUIAttribute);break;
}
if (cellTemplate != null)
{
factory.SetValue(PropertyDefinition.CellTemplateProperty, cellTemplate);
dataTemplate.Seal();
}
else
{
return null;
}
return dataTemplate;
}
}
}
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace PropertyGridDemo.PropertyGridControl
{
/// <summary>
///初始化所有属性并调用模板选择器进行匹配
/// </summary>
public class DataEditorsViewModel
{
public IEnumerable<PropertyDescriptor> Properties { get { return TypeDescriptor.GetProperties(typeof(TestPropertyGrid)).Cast<PropertyDescriptor>(); } }
}
}
3.编写一个可用于构建模板的属性 CustomUIType:
using System;
namespace PropertyGridDemo.PropertyGridControl
{
public class CustomUIType
{
}
public enum CustomUITypes
{
Slider,
ComboBoxEit,
SpinEdit,
CheckBoxEdit
}
[AttributeUsage(AttributeTargets.Property)]
internal class CustomUIAttribute : Attribute
{
public string DataContextPropertyName { get; set; }
public CustomUITypes CustomUI { get; set; }
/// <summary>
/// 自定义控件属性构造函数
/// </summary>
/// <param name="uiTypes">The UI types.</param>
/// <param name="dataContextPropertyName">Name of the data context property.</param>
internal CustomUIAttribute(CustomUITypes uiTypes, string dataContextPropertyName)
{
CustomUI = uiTypes;
DataContextPropertyName = dataContextPropertyName;
}
}
}
4.编写对应的DataContext类 TestPropertyGrid:
using DevExpress.Mvvm.DataAnnotations;
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Timers;
using System.Windows;
namespace PropertyGridDemo.PropertyGridControl
{
[MetadataType(typeof(DynamicallyAssignDataEditorsMetadata))]
public class TestPropertyGrid : PropertyDataContext
{
private double _count = 0;
private SliderUIDataContext _countSource = null;
private ComboBoxEditDataContext _comboSource = null;
private double _value=1;
public TestPropertyGrid()
{
Password = "1111111";
Notes = "Hello";
Text = "Hello hi";
}
[Browsable(false)]
public SliderUIDataContext CountSource
{
get
{
if (_countSource != null)
{
return _countSource;
}
else
{
_countSource = new SliderUIDataContext(0, 100, Count, 0.1, 1);
_countSource.PropertyChanged += (object o, PropertyChangedEventArgs e) =>
{
this.Count = _countSource.Value;
};
return _countSource;
}
}
}
[Browsable(false)]
public ComboBoxEditDataContext ComboSource
{
get
{
if(_comboSource==null)
{
_comboSource =new ComboBoxEditDataContext(ComboBoxEditItemSource.TestItemSource,Value);
_comboSource.PropertyChanged += (object o, PropertyChangedEventArgs e) =>
{
this.Value =Convert.ToDouble(_comboSource.EditValue.Item2);
};
}
return _comboSource;
}
}
[Display(Name = "SliderEdit", GroupName = "CustomUI")]
[CustomUI(CustomUITypes.Slider, nameof(CountSource))]
public double Count
{
get => _count;
set
{
_count = value;
CountSource.Value = value;
RaisePropertyChanged(nameof(Count));
}
}
[Display(Name = "ComboBoxEditItem", GroupName = "CustomUI")]
[CustomUI(CustomUITypes.ComboBoxEit, nameof(ComboSource))]
public double Value
{
get => _value;
set
{
if (_value == value) return;
_value = value;
//ComboSource.Value = value;
RaisePropertyChanged(nameof(Value));
}
}
[Display(Name = "Password", GroupName = "DefaultUI")]
public string Password { get; set; }
[Display(Name = "TextEdit", GroupName = "DefaultUI")]
public string Text { get; set; }
[Display(Name = "Notes", GroupName = "DefaultUI")]
public string Notes { get; set; }
[Display(Name = "Double", GroupName = "DefaultUI")]
[DefaultValue(1)]
public double TestDouble { get; set; }
[Display(Name = "Items", GroupName = "DefaultUI")]
[DefaultValue(Visibility.Visible)]
public Visibility TestItems { get; set; }
}
public static class DynamicallyAssignDataEditorsMetadata
{
public static void BuildMetadata(MetadataBuilder<TestPropertyGrid> builder)
{
builder.Property(x => x.Password)
.PasswordDataType();
builder.Property(x => x.Notes)
.MultilineTextDataType();
}
}
}
该类中用到的其他类主要有以下几个,以下几个类主要用于数据绑定:
namespace PropertyGridDemo.PropertyGridControl
{
public class SliderUIDataContext:PropertyDataContext
{
private double _value = 0;
private double _max = 0;
private double _min = 0;
private double _smallChange = 1;
private double _largeChange=1;
public SliderUIDataContext()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SliderUIDataContext"/> class.
/// </summary>
/// <param name="min">The minimum.</param>
/// <param name="max">The maximum.</param>
/// <param name="value">The value.</param>
/// <param name="smallChange">The small change.</param>
/// <param name="largeChange">The large change.</param>
public SliderUIDataContext(double min, double max, double value,double smallChange=0.01,double largeChange=0.1)
{
SmallChange = smallChange;
LargeChange = largeChange;
Max = max;
Min = min;
Value = value;
}
/// <summary>
/// Gets or sets the small change.
/// </summary>
/// <value>
/// The small change.
/// </value>
public double SmallChange
{
get => _smallChange;
set
{
if (value == _min) return;
_min = value;
RaisePropertyChanged(nameof(SmallChange));
}
}
/// <summary>
/// Gets or sets the large change.
/// </summary>
/// <value>
/// The large change.
/// </value>
public double LargeChange
{
get => _largeChange;
set
{
if (Value == _largeChange) return;
_largeChange = value;
RaisePropertyChanged(nameof(LargeChange));
}
}
/// <summary>
/// Gets or sets the maximum.
/// </summary>
/// <value>
/// The maximum.
/// </value>
public double Max
{
get => _max;
set
{
if (value == _max) return;
_max = value;
RaisePropertyChanged(nameof(Max));
}
}
/// <summary>
/// Gets or sets the minimum.
/// </summary>
/// <value>
/// The minimum.
/// </value>
public double Min
{
get => _min;
set
{
if (value == _min) return;
_min = value;
RaisePropertyChanged(nameof(Min));
}
}
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>
/// The value.
/// </value>
public double Value
{
get => _value;
set
{
if (value == _value) return;
_value = value;
RaisePropertyChanged(nameof(Value));
}
}
}
}
using System;
using System.Linq;
namespace PropertyGridDemo.PropertyGridControl
{
public class ComboBoxEditDataContext:PropertyDataContext
{
private Tuple<string, object>[] _itemSource;
private Tuple<string, object> _editValue;
private int _selectedIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ComboBoxEditDataContext"/> class.
/// </summary>
/// <param name="itemSource">The item source.</param>
/// <param name="editValue">The edit value.</param>
public ComboBoxEditDataContext(Tuple<string,object>[] itemSource,Tuple<string,object> editValue)
{
_itemSource = itemSource;
_editValue = _itemSource.FirstOrDefault(x => x?.Item1.ToString() == editValue?.Item1.ToString() && x?.Item2?.ToString() == x?.Item2?.ToString());
}
/// <summary>
/// Initializes a new instance of the <see cref="ComboBoxEditDataContext" /> class.
/// </summary>
/// <param name="itemSource">The item source.</param>
/// <param name="value">The value.</param>
public ComboBoxEditDataContext(Tuple<string, object>[] itemSource, object value)
{
_itemSource = itemSource;
_editValue = _itemSource.FirstOrDefault(x => x?.Item2.ToString() == value.ToString() );
}
public string Name
{
get;set;
}
/// <summary>
/// Gets or sets the item source.
/// </summary>
/// <value>
/// The item source.
/// </value>
public Tuple<string,object>[] ItemSource
{
get => _itemSource;
set
{
//if (_itemSource == value) return;
_itemSource = value;
RaisePropertyChanged(nameof(ItemSource));
}
}
/// <summary>
/// Gets or sets the edit value.
/// </summary>
/// <value>
/// The edit value.
/// </value>
public Tuple<string,object> EditValue
{
get => _editValue;
set
{
if (_editValue == value) return;
_editValue = value;
RaisePropertyChanged(nameof(EditValue));
}
}
public object Value
{
set
{
EditValue = ItemSource.FirstOrDefault(x => x.Item2.Equals(value));
}
}
/// <summary>
/// Gets or sets the index of the selected.
/// </summary>
/// <value>
/// The index of the selected.
/// </value>
public int SelectedIndex
{
get => _selectedIndex;
set
{
if (_selectedIndex == value || value==-1) return;
_selectedIndex = value;
EditValue = ItemSource[value];
RaisePropertyChanged(nameof(SelectedIndex));
}
}
}
}
using System.ComponentModel;
namespace PropertyGridDemo.PropertyGridControl
{
public class PropertyDataContext:INotifyPropertyChanged
{
/// <summary>
/// 在更改属性值时发生。
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 触发属性变化
/// </summary>
/// <param name="propertyName"></param>
public virtual void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
using System;
namespace PropertyGridDemo.PropertyGridControl
{
internal static class ComboBoxEditItemSource
{
internal static Tuple<string, object>[] TestItemSource = new Tuple<string, object>[] {
new Tuple<string, object>("1",1),
new Tuple<string, object>("2",2),
new Tuple<string, object>("3",3)
};
}
}
5.将以上的CustomPropertyGrid丢进容器中即可,这里我直接用Mainwindow来演示:
<Window
x:Class="PropertyGridDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:PropertyGridControl="clr-namespace:PropertyGridDemo.PropertyGridControl"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PropertyGridDemo"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="525"
Height="350"
WindowState="Maximized"
mc:Ignorable="d">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="259*" />
<ColumnDefinition Width="259*" />
</Grid.ColumnDefinitions>
<TextBox
x:Name="OutputBox"
Grid.ColumnSpan="1"
HorizontalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="True" />
<PropertyGridControl:CustomPropertyGrid x:Name="PropertyGrid" Grid.Column="1" />
</Grid>
</Window>
运行示意图:
以上就是自定义PropertyGrid控件的实现代码,本人只实现了简单的Slider和ComboBoxEdit控件,实际上可以根据自己的需要仿照以上的方法扩展到其他控件,这个就看需求了。
个人感觉以上方案还是有所欠缺,主要是自定义控件的模板是由代码生成的,如果可以直接从资源文件中读取将会更加方便,不过本人尝试了几次并不能成功的实现数据的绑定,如果大家有什么好的解决方案欢迎在评论区留言,也欢迎大家在评论区进行讨论。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
# PropertyGrid
# 控件
# C++ 关于 CMFCPropertyGridCtrl 的使用方法
# C#实现ProperTyGrid自定义属性的方法
# jQuery EasyUI API 中文文档 - PropertyGrid属性表格
# ExtJS PropertyGrid中使用Combobox选择值问题
# ExtJs扩展之GroupPropertyGrid代码
# 自定义
# 几个
# 绑定
# 选择器
# 自己的
# 类中
# 都是
# 是一个
# 实现了
# 有什么
# 很好
# 将会
# 是由
# 几次
# 当你
# 这也
# 带来了
# 可以直接
# 找了
# 欢迎大家
相关文章:
上海网站制作网站建设公司,建筑电工证网上查询系统入口?
单页制作网站有哪些,朋友给我发了一个单页网站,我应该怎么修改才能把他变成自己的呢,请求高手指点迷津?
建站之星微信建站一键生成小程序+多端营销系统
已有域名如何快速搭建专属网站?
C++中的Pimpl idiom是什么,有什么好处?(隐藏实现)
如何选择网络建站服务器?高效建站必看指南
如何在万网ECS上快速搭建专属网站?
如何通过PHP快速构建高效问答网站功能?
视频网站制作教程,怎么样制作优酷网的小视频?
如何用搬瓦工VPS快速搭建个人网站?
建站主机选购指南与交易推荐:核心配置解析
网站制作多少钱一个,建一个论坛网站大约需要多少钱?
香港网站服务器数量如何影响SEO优化效果?
建站主机无法访问?如何排查域名与服务器问题
英语简历制作免费网站推荐,如何将简历翻译成英文?
建站之星安全性能如何?防护体系能否抵御黑客入侵?
宁波免费建站如何选择可靠模板与平台?
建站之星后台密码遗忘或太弱?如何重置与强化?
php json中文编码为null的解决办法
建站主机解析:虚拟主机配置与服务器选择指南
家族网站制作贴纸教程视频,用豆子做粘帖画怎么制作?
完全自定义免费建站平台:主题模板在线生成一站式服务
早安海报制作网站推荐大全,企业早安海报怎么每天更换?
如何在Golang中使用encoding/gob序列化对象_存储和传输数据
建站之星如何优化SEO以实现高效排名?
如何配置IIS站点权限与局域网访问?
如何在阿里云虚拟机上搭建网站?步骤解析与避坑指南
建站之星安装失败:服务器环境不兼容?
浅谈Javascript中的Label语句
如何选购建站域名与空间?自助平台全解析
javascript中的try catch异常捕获机制用法分析
如何在局域网内绑定自建网站域名?
无锡营销型网站制作公司,无锡网选车牌流程?
如何设置并定期更换建站之星安全管理员密码?
如何用IIS7快速搭建并优化网站站点?
建站之星CMS建站配置指南:模板选择与SEO优化技巧
如何在宝塔面板中创建新站点?
官网网站制作腾讯审核要多久,联想路由器newifi官网
如何访问已购建站主机并解决登录问题?
如何用PHP快速搭建高效网站?分步指南
北京的网站制作公司有哪些,哪个视频网站最好?
建站之星如何保障用户数据免受黑客入侵?
,网页ppt怎么弄成自己的ppt?
建站之星伪静态规则如何设置?
Android自定义控件实现温度旋转按钮效果
已有域名和空间,如何快速搭建网站?
建站上市公司网站建设方案与SEO优化服务定制指南
制作网站怎么制作,*游戏网站怎么搭建?
网站制作公司哪里好做,成都网站制作公司哪家做得比较好,更正规?
临沂网站制作企业,临沂第三中学官方网站?
*请认真填写需求信息,我们会在24小时内与您取得联系。