DataGrid是WPF提供的基础控件,它可以非常轻松的呈现出一张表格,本文章会按照从易到难的顺序依次将DataGrid的使用方法进行解说,除了MSDN上给出的最基本的例子之外,给出了三个比较常见的在真实使用场景下使用的例子,这三个例子已经基本覆盖了我们能够遇到的大部分使用场景了。
在MSDN上,可以非常轻松的找到有关于对DataGrid控件进行数据绑定的方法,微软提供的绑定方法是这样的:
MSDN源地址点击这里查看
1、有一个已经定义好的类型:
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Uri Email { get; set; }
public bool IsMember { get; set; }
}
2、编写一个XAML的DataGrid控件
在这里,DataGrid关联数据比较灵活,就不一一列举了,下边贴出的代码只使用了其中一种关联方式:
<DataGrid Name="DG1" ItemsSource="{Binding}" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn Header="Column1" Binding="{Binding FirstName}"/>
<DataGridTextColumn Header="Column2" Binding="{Binding LastName}" />
<DataGridHyperlinkColumn Header="Column3" Binding="{Binding Email}" />
<DataGridCheckBoxColumn Header="Column4" Binding="{Binding IsMember}" />
DataGrid.Columns>
DataGrid>
3、通过DataContext关联容器和DataGrid控件实例
ObservableCollection list = InitCustomerData.InitData(); // 向容器中添加数据
this.CustomerDataGrid.DataContext = list;
最终,通过DataContext将类型实例的容器与DataGrid的实例进行关联,效果如下,就可以在DataGrid当中显示出来(我这个截图稍微有点出入,多了个我做实验的Column5):
但是,这个用法有以下几个缺点:
注:以上代码的实例可以在WPF项目下的主界面逻辑处理InitaListToDataGridColumn函数当中实现(源代码地址在文章最后给出)
在应用实例1当中,解决了基础实例当中的两个问题:
实现的方法是使用“数据模板(DataTemplate)”来填充DataGrid的单元格,这个数据模型可以是多种多样的,比如:
1、前端使用DataTemplate定义一个单元格要显示的控件内容
在这里,可以看到DataGrid控件当中的前两列,编号和时间戳仍然是基础的使用方法,而第三列的消息内容,则需要有更多的功能,比如一个点击事件,这个点击事件可以查看消息更详细的内容,所以我为这个单元格设计了一个TextBlock控件显示消息的简略内容,以及一个按钮,这个按钮点击后,可以显示这个消息的详细内容
这个数据模板需要使用x:Type关联一个类型作为它的模型,Text显示内容可以是这个类型的其中一个属性,而按钮的点击事件则可以直接调用这个类型的成员函数或回调函数
<Border.Resources>
<DataTemplate x:Key="cellEditingTemplate" DataType="{x:Type model:MessageModel}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="15"/>
Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding m_content}"/>
<Button Grid.Column="1" Content="..." Command="{Binding ShowCommand}"/>
Grid>
DataTemplate>
<DataTemplate x:Key="cellDropBox" DataType="{x:Type model:MessageModel}">
<ComboBox ItemsSource="{Binding m_content_detail}" SelectedIndex="0" />
DataTemplate>
Border.Resources>
<DataGrid AutoGenerateColumns="False" x:Name="MsgDataGrid" ItemsSource="{Binding Path=.}" BeginningEdit="MsgDataGrid_BeginningEdit" >
<DataGrid.Columns>
<DataGridTextColumn Header="编号" Binding="{Binding m_No}"/>
<DataGridTextColumn Header="时间戳" Binding="{Binding m_time}"/>
<DataGridTemplateColumn Header="消息内容" CellTemplate="{StaticResource ResourceKey = cellEditingTemplate}"
CellEditingTemplate="{StaticResource ResourceKey = cellEditingTemplate}"/>
<DataGridTemplateColumn Header="消息详情" CellTemplate="{StaticResource ResourceKey=cellDropBox}"/>
DataGrid.Columns>
DataGrid>
2、定义一个与DataTemlate相关联的类型
在前端WPF代码中,DataTemplate当中,指定了类型MessageModel为这个模板的模型,并且,模板当中的第一个TextBlock使用了类型中的属性m_content,第二个Button按钮使用了ICommand回调函数,同时,这个MessageModel还是DataGrid另外两列的数据模板
public class MessageModel
{
///
/// 可触发的命令:显示消息内容;
///
private ICommand mShowCommand;
public ICommand ShowCommand
{
get
{
if (mShowCommand == null)
{
mShowCommand = new RelayCommand(() =>
{
ExecuteAction();
},
() => CanExecuteFunc());
}
return mShowCommand;
}
}
private bool CanExecuteFunc()
{
return true;
}
private void ExecuteAction()
{
Console.WriteLine("Content is" + this.m_content);
foreach(var iter in this.m_content_detail)
{
Console.WriteLine("Content Detail is " + iter);
}
}
///
/// 消息编号;
///
private string No;
public string m_No
{
get { return No; }
set { No = value; }
}
///
/// 时间戳;
///
private DateTime time;
public DateTime m_time
{
get { return time; }
set { time = value; }
}
///
/// 消息内容A;
///
private string content;
public string m_content
{
get { return content; }
set { content = value; }
}
private List<string> content_detail;
public List<string> m_content_detail
{
get { return content_detail; }
set { m_content_detail = value; }
}
///
/// 消息的源IP地址;
///
private string source;
public string m_source
{
get { return source; }
set { source = value; }
}
///
/// 消息的目的IP地址;
///
private string dest;
public string m_dest
{
get { return dest; }
set { dest = value; }
}
}
public class RelayCommand : ICommand
{
private Action mExecuteAction; // 执行命令;
private Func<bool> mCanExecuteFunc; // 命令是否可以执行;
public RelayCommand(Action executeAction, Func<bool> canExecuteFunc)
{
mExecuteAction = executeAction;
mCanExecuteFunc = canExecuteFunc;
}
public bool CanExecute(object parameter)
{
return mCanExecuteFunc.Invoke();
}
public void Execute(object parameter)
{
mExecuteAction.Invoke();
}
public event EventHandler CanExecuteChanged;
}
3、定义一个与DataGrid控件直接交互的VM层,用来控制控件的显示内容
这种设计思路是MVVM框架的思路,可以比较好的解耦前端WPF层(View层)与后端数据模型(Model层),在VM层(ViewModel),专门用一个List或者ObservableCollection容器保存想要在前端DataGrid中显示的容器列表
public class MessageVM
{
// 存放所有消息内容的地方;
private volatile ObservableCollection<MessageModel> m_messagelist;
public ObservableCollection<MessageModel> messagelist
{
get
{
return m_messagelist;
}
set
{
m_messagelist = value;
}
}
public MessageVM()
{
messagelist = new ObservableCollection<MessageModel>();
}
}
4、在主程序当中,将VM层与对应的DataGrid控件进行DataContext关联
这个就是处理逻辑了,没什么好说的,用这个处理逻辑为VM层添加一些实验数据,以便显示在前端的DataGrid当中
public MessageVM m_MessageVM = new MessageVM(); // MessageVM层,用来显示MessageModel的;
this.MsgDataGrid.DataContext = m_MessageVM.messagelist;
// 使用一个线程更新DataGrid控件
Task a = new Task(()=>
{
int temp = 0;
while(true)
{
temp = temp + 1;
Dispatcher.Invoke(DispatcherPriority.Normal, (Action)delegate
{
m_MessageVM.messagelist.Add(new MessageModel()
{
m_No = temp.ToString(),
m_time = DateTime.Now,
m_content = "content",
m_source = "172.27.0.1",
m_dest = "172.27.0.2"
});
});
Thread.Sleep(1233);
}
});
a.Start();
最终,呈现的效果如下,在点击消息内容单元格内的"…"按钮后,既可以触发查看内容的详细信息
这个方法的主要核心点就在于使用了DataTemplate数据模板,这个数据模板可以很好的帮助你在DataGrid表的单元格内放置任意你想要的形态,但这样做也会带来一些问题:
以上那种形式,是通过自定义单元格来解决大部分对表格操作的需求,但是,如同上例,简单操作单元格的话,用鼠标或者控件本身事件就好了,在每个单元格内都增加一个按钮也比较难看,况且自定义DataTemplate针对整张表格的操作也时无法实现。所以大部分的基本的操作,利用DataGrid本身自带的事件实现就好了
1、在前端定义一个表格
这里注意一点:将SelectionUnit=“Cell” SelectionMode="Single"这两个属性设定后,就可以对某个单元格进行操作了,默认情况下,点击DataGrid后,默认是选中整行的
<Border BorderBrush="Black" BorderThickness="1" Grid.Row="3" Margin="5,5,5,5">
<DataGrid x:Name="CustomerDataGrid_AddEvent" ItemsSource="{Binding Path = .}" AutoGenerateColumns="False" SelectionUnit="Cell" SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn Header="Column1" Binding="{Binding column1.name}"/>
<DataGridHyperlinkColumn Header="Column2" Binding="{Binding column2.name}" />
<DataGridCheckBoxColumn Header="Column3" Binding="{Binding column3.name}" />
DataGrid.Columns>
DataGrid>
Border>
2、 为这张表格的单元格定义一个数据类型
为了能够让单元格中具有更多的操作行为,我为这个单元格单独定义了类型,在C#后台填充单元格的时候,直接填充类型即可,然后在WPF前端,就可以访问这个类型对应的某个属性显示在DataGrid控件当中了,就如同第一步{Binding column2.name}这样,实则它访问的就是GridCell类型的name
public class GridCell
{
public string name { get; set; }
public void EditingCalback()
{
Console.WriteLine("GridCell Editing Callback:" + name);
}
}
3、为这张表格定义一个VM数据层
这个VM数据层就是为了保存这张表所有要呈现的数据,及其操作表格时对应的函数逻辑
public class DataGridWithEvent
{
public GridCell column1 { get; set; } // 向单元格填写自定义个类型;
public GridCell column2 { get; set; } // 向单元格填写自定义个类型;
public GridCell column3 { get; set; } // 向单元格填写自定义个类型;
// 当表格控件被编辑时,会调用单元格自身实例对应的函数;
public void JudegePropertyCall_CellEditing(string colHeader)
{
switch(colHeader)
{
case "Column1":
this.column1.EditingCalback();
break;
case "Column2":
this.column2.EditingCalback();
break;
case "Column3":
this.column3.EditingCalback();
break;
default:
break;
}
}
}
4、最后在逻辑处理代码中填充数据和对应的事件
ObservableCollection<DataGridWithEvent> list = InitDataGridWithEventData.InitData();
this.CustomerDataGrid_AddEvent.DataContext = list;
// 以下是表格事件;
this.CustomerDataGrid_AddEvent.BeginningEdit += CustomerDataGrid_AddEvent_BeginningEdit; // 事件一:单元格开始编辑事件;
this.CustomerDataGrid_AddEvent.SelectionChanged += CustomerDataGrid_AddEvent_SelectionChanged; // 事件二:单元格选择出现变化时;
this.CustomerDataGrid_AddEvent.GotFocus += CustomerDataGrid_AddEvent_GotFocus; // 事件三:DataGrid表格点击单元格获取焦点时;
// 以下是鼠标事件;
this.CustomerDataGrid_AddEvent.MouseMove += CustomerDataGrid_AddEvent_MouseMove; // 事件四:鼠标移动到某个单元格上时触发(实验函数增加了鼠标拖动效果);
this.CustomerDataGrid_AddEvent.GotMouseCapture += CustomerDataGrid_AddEvent_GotMouseCapture; // 事件五:使用这个事件事件鼠标拖拽更加稳定;
this.CustomerDataGrid_AddEvent.MouseLeftButtonDown += CustomerDataGrid_AddEvent_MouseLeftButtonDown; // 事件六:鼠标左键点击事件,这个事件只针对DataGrid整个表格;
this.CustomerDataGrid_AddEvent.MouseEnter += CustomerDataGrid_AddEvent_MouseEnter; // 事件七:鼠标进入整个表格时触发,且只触发一次;
// 另一个元素接收鼠标拖拽事件;
this.ReceiveDataLabel.AllowDrop = true;
this.ReceiveDataLabel.Drop += ReceiveDataLabel_Drop;
由于MSDN上边,有关于事件列表中的描述都比较晦涩难懂,所以在此列出一些比较常见的
以上两个鼠标事件的作用区域见下图:即有真实填充数据的区域
应用场景1:
MouseMove和GotMouseCapture可以用来实现鼠标拖拽事件
使用DragDrop.DoDragDrop就可以实现鼠标拖拽的起点,使用[控件名称].Drop += [funcCallback]实现接收鼠标拖拽的事件。具体可参见GitHub中的源码
MouseEnter鼠标事件的作用区域为DataGrid整表,见下图:
MouseLeftButtonDown鼠标事件的作用区域为DataGrid没有填充单元格的区域,见下图:
以上两种实例的做法的核心思想是:每一个DataGrid都对应一个数据模型,这样做的好处就是操作数据模型就相当于操作DataGrid表格了,但是这样做有一个缺陷,如果数据模型增加到一定的数量,比如几千个,这样你就需要在前台声明几千个表格,后台代码也需要有对应几千个数据模型,再进一步,表格的内容如果无法确定是动态加载的,这样的方法就不具备任何可行性了
解决问题的思路:
实现这个的思路其实非常淳朴简单,使用dynamic动态类型,在运行时动态的加载表格的ViewModel层。这样一来,我们就可以在运行时为某一个DataGrid动态的生成与其对应的类型了。具体Dynamic类型在此就不展开说了,可参见MSDN
1、声明一个动态类型的VM层
这个VM层中保存了所有生成一个类型的数据,包括:
public class DyDataDridModel : DynamicObject
{
// 用来保存这个动态类型的所有属性;
// string为属性的名字;
// object为属性的值(同时也包含了类型);
Dictionary<string, object> Properties = new Dictionary<string, object>();
// 用来保存中文列名与属性的对应关系;
Dictionary<string, string> ColName_Property = new Dictionary<string, string>();
// 为动态类型动态添加成员;
public override bool TrySetMember(SetMemberBinder binder, object value)
{
if (!Properties.Keys.Contains(binder.Name))
{
Properties.Add(binder.Name, value);
}
return true;
}
// 为动态类型动态添加方法;
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
// 可以通过调用方法的手段添加属性;
if (binder.Name == "AddProperty" && binder.CallInfo.ArgumentCount == 3)
{
string name = args[0] as string;
if (name == null)
{
//throw new ArgumentException("name");
result = null;
return false;
}
// 向属性列表添加属性及其值;
object value = args[1];
Properties.Add(name, value);
// 添加列名与属性列表的映射关系;
string column_name = args[2] as string;
ColName_Property.Add(column_name, name);
result = value;
return true;
}
if(binder.Name == "JudgePropertyName_StartEditing" && binder.CallInfo.ArgumentCount == 1)
{
string columnname = args[0] as string;
if(columnname == null)
{
result = null;
return false;
}
// 在当前列名于属性列表中查找,看是否有匹配项;
if(ColName_Property.ContainsKey(columnname))
{
string key = ColName_Property[columnname];
if(Properties.ContainsKey(key))
{
object property = Properties[key];
}
}
else
{
}
}
return base.TryInvokeMember(binder, args, out result);
}
// 获取属性;
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
return Properties.TryGetValue(binder.Name, out result);
}
}
2、在前端声明一个没有任何列定义的DataGrid表格:
3、在控制类中,可以使用DyDataDridModel动态添加一个类及其属性
动态为DyDataDridModel添加多个属性以及对应的值,这些属性就相当于表格中的列,值就相当于每行对应的数据,下边的例子就增加了一行数据(Grid上文中已经列出过了,这里使用同样的Gridcell)
// 支持动态添加内容的类型
dynamic model = new DyDataDridModel();
// 向单元格内添加内容,这里是添加了一整行内容;
model.AddProperty("property2", new GridCell() { name = "343" }, "列2");
model.AddProperty("property0", new GridCell() { name = "123" }, "列0");
model.AddProperty("property1", new GridCell() { name = "321" }, "列1");
list.Add(model);
// 定义每一列显示的内容以及Binding的对象
for (int i = 0; i <= 2; i++)
{
DataGridTextColumn column = new DataGridTextColumn();
column.Header = "列" + i;
column.Binding = new Binding("property" + i + ".name");
this.MsgDataGrid_AutoGenCol.Columns.Add(column);
}
this.MsgDataGrid_AutoGenCol.ItemsSource = list;
把上边的语句代码翻译回方式一或方式二的模式,是这个样子的:
// VM层模型
class VModel
{
public GridCell property2;
public GridCell property0;
public GridCell property1;
}
// 数据模型
public class GridCell
{
public string name { get; set; }
public void EditingCalback()
{
}
}
<!--view层-->
<DataGrid x:Name="CustomerDataGrid" ItemsSource="{Binding Path = .}" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn Header="列2" Binding="{Binding property2.name}"/>
<DataGridTextColumn Header="列0" Binding="{Binding property0.name}" />
<DataGridHyperlinkColumn Header="列1" Binding="{Binding property1.name}" />
</DataGrid.Columns>
</DataGrid>
// 逻辑层:
ObservableCollection<VModel> list = new ObservableCollection<VModel>();
list.add(new GridCell(){
property2.name = "343",
property0.name = "123",
property1.name = "321"
});
CustomerDataGrid.Datacontext = list;
动态加载表的最终效果如下:
以上,就是目前所有我对WPF的DataGrid控件使用的归纳,总结下来,需要掌握的知识点并不难。如果想要对DataGrid进行灵活应用的话,需要对MVVM架构有一个大致的认识,并且对动态类型有一定的了解,熟悉Python的同学,对动态类型认识起来,肯定就轻松很多了。
另外,以上三种实现方式,都可以在我的一个项目中找到:
https://github.com/visiontrail/CSharpKnowledge
本文有关DataGrid的内容都在WPF项目当中