有关WPF中DataGrid控件的基础应用总结

基础说明

DataGrid是WPF提供的基础控件,它可以非常轻松的呈现出一张表格,本文章会按照从易到难的顺序依次将DataGrid的使用方法进行解说,除了MSDN上给出的最基本的例子之外,给出了三个比较常见的在真实使用场景下使用的例子,这三个例子已经基本覆盖了我们能够遇到的大部分使用场景了。

基础实例:MSDN上,使用DataGrid绑定一个数据模型

在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关联数据比较灵活,就不一一列举了,下边贴出的代码只使用了其中一种关联方式:

  1. 可以像例子里一样:先将ItemsSource使用Binding空出path属性,然后使用DataContext指定数据源
  2. 也可以在前端不指定ItemsSource,然后直接在C#代码中指定ItemsSource:this.CustomerDataGrid.ItemsSource = list;
  3. 也可以直接在XAML前端直接指定容器
<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<Customer> list = InitCustomerData.InitData();  // 向容器中添加数据
this.CustomerDataGrid.DataContext = list;

最终,通过DataContext将类型实例的容器与DataGrid的实例进行关联,效果如下,就可以在DataGrid当中显示出来(我这个截图稍微有点出入,多了个我做实验的Column5):

但是,这个用法有以下几个缺点

  1. DataGrid内部只有DataGrid预设的几种数据类型,没有办法扩展
  2. 只有显示功能,没有对DataGrid中的数据有任何操作
  3. DataGrid数据结构在前端被写死,如果存在很多张表的话,就需要对应数量的前端代码

注:以上代码的实例可以在WPF项目下的主界面逻辑处理InitaListToDataGridColumn函数当中实现(源代码地址在文章最后给出)

实际应用

应用实例1:通过数据模板扩展表格单元格的显示内容

在应用实例1当中,解决了基础实例当中的两个问题:

  1. DataGrid可以填充任意的数据
  2. DataGrid可以通过模板添加按钮等带有事件的控件来控制单元格

实现的方法是使用“数据模板(DataTemplate)”来填充DataGrid的单元格,这个数据模型可以是多种多样的,比如:

  1. Image控件显示图片
  2. 一个TextBlock加button按钮的组合
  3. 等等~~~

执行步骤:

1、前端使用DataTemplate定义一个单元格要显示的控件内容

在这里,可以看到DataGrid控件当中的前两列,编号和时间戳仍然是基础的使用方法,而第三列的消息内容,则需要有更多的功能,比如一个点击事件,这个点击事件可以查看消息更详细的内容,所以我为这个单元格设计了一个TextBlock控件显示消息的简略内容,以及一个按钮,这个按钮点击后,可以显示这个消息的详细内容

这个数据模板需要使用x:Type关联一个类型作为它的模型,Text显示内容可以是这个类型的其中一个属性,而按钮的点击事件则可以直接调用这个类型的成员函数或回调函数

<!-- 一个数据模板,使用在后边DataGrid的最后一个单元格 -->
<Border.Resources>
    <!-- 这个数据模板使用MessageModel这个类型作为数据模型 -->
    <DataTemplate x:Key="cellEditingTemplate" DataType="{x:Type model:MessageModel}">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="15"/>
            </Grid.ColumnDefinitions>
            <!-- 控件类型是一个TextBlock和一个Button控件的组合 -->
            <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>

<!-- 表格,用来显示MessageVM中所有的消息,使用ItemsSource指定数据源 -->
<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
{
    /// <summary>
    /// 可触发的命令:显示消息内容;
    /// </summary>
    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);
        }
    }

    /// <summary>
    /// 消息编号;
    /// </summary>
    private string No;
    public string m_No
    {
        get { return No; }
        set { No = value; }
    }

    /// <summary>
    /// 时间戳;
    /// </summary>
    private DateTime time;
    public DateTime m_time
    {
        get { return time; }
        set { time = value; }
    }

    /// <summary>
    /// 消息内容A;
    /// </summary>
    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; }
    }
    
    /// <summary>
    /// 消息的源IP地址;
    /// </summary>
    private string source;
    public string m_source
    {
        get { return source; }
        set { source = value; }
    }

    /// <summary>
    /// 消息的目的IP地址;
    /// </summary>
    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表的单元格内放置任意你想要的形态,但这样做也会带来一些问题

  1. 如果只是简单的编辑表格,或者对表格本身进行操作,DataTemplate就无能为力了:
    1. 比如编辑单元格
    2. 比如鼠标滑过表格或单元格时,有悬浮窗提示信息
    3. 等等这样针对表格,某个单元格本身,而不是针对某个单元格中的内容操作
  2. 这个表格仍然没有解决前端代码被写死,Model和View必须一一对应的问题

应用实例2:使用DataGrid自带的事件对表格进行操作

以上那种形式,是通过自定义单元格来解决大部分对表格操作的需求,但是,如同上例,简单操作单元格的话,用鼠标或者控件本身事件就好了,在每个单元格内都增加一个按钮也比较难看,况且自定义DataTemplate针对整张表格的操作也时无法实现。所以大部分的基本的操作,利用DataGrid本身自带的事件实现就好了

执行步骤:

1、在前端定义一个表格

这里注意一点:将SelectionUnit=“Cell” SelectionMode="Single"这两个属性设定后,就可以对某个单元格进行操作了,默认情况下,点击DataGrid后,默认是选中整行的

<!-- 对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
    1. 当鼠标进入到DataGrid控件真实填充过的单元格后,鼠标每移动到某个元素的时候,都会触发这个事件
    2. 带有两个入参,几个比较重要的可以获取的参数:
      1. object sender:DataGrid本身
      2. MouseEventArgs e:e.OriginalSource is DataGridCell
      3. 获取表格当中的元素类型:(e.OriginalSource as DataGridCell).DataContext

  2. GotMouseCapture
    1. 当鼠标进入到DataGrid控件真实填充过的单元格后,鼠标每点击某个单元格的时候,就会触发这个事件
    2. 带有两个入参,几个比较重要的可以获取的参数:
      1. object sender:DataGrid本身
      2. e.OriginalSource is DataGrid
      3. 获取表格当中的元素类型:foreach (var iter in (e.OriginalSource as DataGrid).SelectedCells)

以上两个鼠标事件的作用区域见下图:即有真实填充数据的区域

在这里插入图片描述

应用场景1:
MouseMove和GotMouseCapture可以用来实现鼠标拖拽事件

使用DragDrop.DoDragDrop就可以实现鼠标拖拽的起点,使用[控件名称].Drop += [funcCallback]实现接收鼠标拖拽的事件。具体可参见GitHub中的源码

在这里插入图片描述

  1. MouseEnter
    1. 当鼠标进入到整个DataGrid表格的时候,会触发一次,且不会重复触发,直到鼠标离开DataGrid控件后,再次进入之后,才会再次触发
    2. 带有两个入参,三个可以获取的比较重要的参数:
      1. sender:DataGrid本身
      2. e.Source is DataGrid本身
      3. e.OriginalSource is DataGrid本身

MouseEnter鼠标事件的作用区域为DataGrid整表,见下图:

在这里插入图片描述

  1. MouseLeftButtonDown
    1. 当鼠标在DataGrid没有填充数据的单元格处,发生了鼠标左键按下的时候触发的事件
    2. 带有两个参数,两个可以获取的比较重要的参数:
      1. sender is DataGrid本身
      2. e.Source is DataGrid本身

MouseLeftButtonDown鼠标事件的作用区域为DataGrid没有填充单元格的区域,见下图:

在这里插入图片描述

应用实例3:通过动态类型实现一个动态DataGrid表格(即可以填充任何数据类型)

以上两种实例的做法的核心思想是:每一个DataGrid都对应一个数据模型,这样做的好处就是操作数据模型就相当于操作DataGrid表格了,但是这样做有一个缺陷,如果数据模型增加到一定的数量,比如几千个,这样你就需要在前台声明几千个表格,后台代码也需要有对应几千个数据模型,再进一步,表格的内容如果无法确定是动态加载的,这样的方法就不具备任何可行性了

解决问题的思路:

实现这个的思路其实非常淳朴简单,使用dynamic动态类型,在运行时动态的加载表格的ViewModel层。这样一来,我们就可以在运行时为某一个DataGrid动态的生成与其对应的类型了。具体Dynamic类型在此就不展开说了,可参见MSDN

在这里插入图片描述

执行步骤:

1、声明一个动态类型的VM层

这个VM层中保存了所有生成一个类型的数据,包括:

  • Properties:属性名称和属性类型的对应关系
    • key:类型中属性的名字
    • value:类型中属性的类型的实例
  • colName_Property:列名和属性名的对应关系
    • key:DataGrid表的列名称
    • value:Property的属性名称
  • AddProperty:动态方法,用于动态给Properties和ColName_Property添加对应关系
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表格:

<DataGrid AutoGenerateColumns="False" x:Name="MsgDataGrid_AutoGenCol"/>

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项目当中

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页