将公式单元格应用于DataGridView

14

我想在 DataGridView 中添加公式单元格。是否有任何自定义的 DataGridView 可以实现这一点?

示例:

grid[4, column].Text = string.Format("=MAX({0}6:{0}{1})", columnAsString, grid.RowCount);

如果您正在寻找自定义控件,请查看免费的.NET电子表格控件。它支持公式 - Reza Aghaei
你可以使用支持表达式DataTable 并将其绑定到 DataGridView - Alexander Petrov
4个回答

8
在许多情况下,数据实际上并不在 DataGridView 中,而是在其他地方,比如一个 DataTable 或某种集合(如 List<T>)。控件只是向用户呈现数据的视图。
有几种方法可以实现类似于您想要的功能。对于这两种方法,数据实际上存储在一个 DataTable 中。

表达式

可以为 DataColumn 分配一个 表达式。请查阅链接以获取支持的表达式类型、关键字、运算符和函数。以下示例将创建一个基于表达式的列,用于计算某些行的 Quantity * Price 值:
dtSample = new DataTable();
dtSample.Columns.Add(new DataColumn("Item", typeof(string)));
dtSample.Columns.Add(new DataColumn("Quantity", typeof(int)));
dtSample.Columns.Add(new DataColumn("Price", typeof(decimal)));
dtSample.Columns.Add(new DataColumn("Sale", typeof(decimal)));

// assign expression using the col names
dtSample.Columns[3].Expression = "(Quantity * Price)";

在添加一些随机数据和空行之后,DataTable 将为您维护这些列。这将按照您希望的方式工作:如果用户(或代码)更改了QuantityPrice 单元格的值,则Sale 列内容将自动更新。(第二个方法后面有一张图片) Expressions 在行级别上起作用。没有一个类似于总计行的所有行/表级计数器部分 - 这是因为数据通常来自DataSource。添加计算行可能会意外地向该源添加新数据(如 DB)。但在代码中做到这一点并不难:

事件驱动的计算

类似于给出的 DGV CellFormatting 答案,您可以响应来自DataTable 的事件,例如RowChanged。在那里,您可以执行任何操作并更新表。
...create table and columns
...populate table
// hook up event
dtSample.RowChanged += RowChanged;

然后在事件中,代码计算总体每单位平均值并显示在最后一行。在某些情况下,您可以使用DataTableCompute()方法。与Expression不同,它不会自动更新,并且如此显示的答案更新起来可能很笨拙。

对于DataTable中的类型化数据,响应事件进行计算相当容易:

private void RowChanged(object sender, DataRowChangeEventArgs e)
{
    // number of rows used 
    int Rows = dtSample.Rows.Count-1;
    if (e.Row == dtSample.Rows[Rows]) return;

    // display TotalSales / TotalUnits
    // get the units
    int TotUnits = dtSample
        .AsEnumerable()
        .Where(r => !r.IsNull("Quantity"))
        .Take(Rows)
        .Sum(n => n.Field<int>("Quantity"));
    
    // sum Sales, divide and display in DGV
    dtSample.Rows[Rows]["Price"] = dtSample
        .AsEnumerable()
        .Where(r => !r.IsNull("Sale"))
        .Take(Rows)
        .Sum(n => n.Field<decimal>("Sale")) / TotUnits;
}

enter image description here

“销售”列是通过一个“表达式”自动维护的,这意味着您不能手动更改该列。
底部的总平均价格也是“自动”更新的,不同之处在于我们必须编写一些代码来实现这一点。

3

有没有自定义的DataGridView可以做到这一点?

离题了,但如果您正在寻找自定义控件,请查看Free .NET Spreadsheet Control。此外,它还支持formula

如果编写计算代码是您的选择

如果编写计算代码是您的选择,您可以使用DataGridViewCellFormatting事件并在其中放置计算逻辑来计算基于其他单元格的值的单元格的值。还要处理CellEndEdit并调用InvalidateCellInvalidate以强制在引用单元格后更新单元格的值。

以下是一个示例:

void Form1_Load(object sender, EventArgs e)
{
    Random r = new Random();
    var dt = new DataTable();
    dt.Columns.Add("A", typeof(int));
    dt.Columns.Add("B", typeof(int));
    for (int i = 0; i < 10; i++)
        dt.Rows.Add(r.Next(100));
    grid.DataSource = dt;
    grid.CellFormatting += grid_CellFormatting;
    grid.CellEndEdit += grid_CellEndEdit;
}
void grid_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
    grid.Invalidate();
}
void grid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
    var grid = sender as DataGridView;
    var parameterColumnName = "A";       //Parameter column name
    var start = 0;                       //Start row index for formula
    var end = grid.RowCount - 1;         //End row index for formula
    var resultRowIndex = 0;              //Result row index
    var resultColumnName = "B";          //Result column name
    if (e.RowIndex == resultRowIndex &&
        grid.Columns[e.ColumnIndex].Name == resultColumnName)
    {
        var list = Enumerable.Range(start, end - start + 1)
              .Select(i => grid.Rows[i].Cells[parameterColumnName].Value)
              .Where(x => x != null && x != DBNull.Value)
              .Cast<int>();
        if (list.Any())
            e.Value = list.Max();
    }
}

注意

这个解决方案不仅限于DataTable,它适用于您在DataGridView中使用的任何DataSource,您可以在此解决方案中使用任何类型的数据源。


事件驱动方法足够快。我没有尝试过链接控件,但它似乎具有许多功能。 - Reza Aghaei
此解决方案不仅限于“DataTable”,无论您为“DataGridView”使用哪种“DataSource”,它都可以正常工作。 - Reza Aghaei
我检查了这些状态下的 DataGridView:绑定到 List<T>,绑定到 DataTable,没有 DataSource 并使用 RowCountColumnCount 创建列和行,没有 DataSource 并使用 Columns.AddRows.Add 创建列和行,在所有情况下都能正常工作。我怀疑你把 grid.CellFormatting += grid_CellFormatting; 放在一个不运行的位置。例如,你可能忘记将 Form1_Load 分配给 FormLoad 事件。 - Reza Aghaei
关于CellEndEdit,它仅适用于可编辑的网格,如果您的网格是只读的,则不需要它。 - Reza Aghaei
您可以在设计模式下使用 PropertyGrid 并为控件分配事件处理程序。同时,您也可以在代码中实现此功能。但请不要同时使用代码和设计模式,因为事件将会被附加两次。 - Reza Aghaei
显示剩余2条评论

2

不,你正在处理纯数据字符串,你需要在后台运行一个线程来执行计算并相应地更新UI。


0
你可以尝试以下几件事情:
处理单元格值更改事件,以便在计算中使用的单元格之一发生更改时检测并计算相应的值。类似这样(在新项目中尝试):
Public Class Form1
  Private WithEvents DGV As New DataGridView
  Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    DGV.SetBounds(20, 50, 400, 200)
    Controls.Add(DGV)
    DGV.Columns.Add("Qty", "Qty")
    DGV.Columns.Add("Price", "Price")
    DGV.Columns.Add("Total", "Total")
    DGV.Columns("Total").ReadOnly = True
    DGV.RowCount = 5
  End Sub

  Private Sub DGV_CellValueChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles DGV.CellValueChanged
    If e.ColumnIndex = DGV.Columns("Qty").Index Or e.ColumnIndex = DGV.Columns("Price").Index Then
      DGV("Total", e.RowIndex).Value = CInt(DGV("Qty", e.RowIndex).Value) * CDbl(DGV("Price", e.RowIndex).Value)
    End If
  End Sub
End Class

或者不直接将文件读入datagridview,而是将其读入datatable并将网格绑定到datatable。然后可以在datatable中添加一个表达式列来进行计算。类似这样的东西:
Public Class Form1
  Private WithEvents DGV As New DataGridView
  Private DT As New DataTable
  Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    DGV.SetBounds(20, 50, 400, 200)
    Controls.Add(DGV)
    DT.Columns.Add("Qty", GetType(Int32))
    DT.Columns.Add("Price", GetType(Double))
    DT.Columns.Add("Total", GetType(Double))
    DT.Columns("Total").Expression = "Qty * Price"
    DGV.DataSource = DT
  End Sub
End Class

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接