将公式单元格应用于 DataGridview

Apply Formula Cell to a DataGridview

我想在 DataGridView 中添加公式单元格。是否有任何自定义 DataGridView 可以执行此操作?

示例:

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

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

是否有任何自定义 DataGridView 可以执行此操作?

题外话,但如果您正在寻找自定义控件,请查看 Free .NET Spreadsheet Control. Also it supports formula

如果编写用于计算的代码是您的一种选择

如果您可以选择编写用于计算的代码,要根据其他一些单元格的值计算一个单元格的值,您可以使用 CellFormatting 事件 DataGridView 并将计算逻辑放在那里。还要处理 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,无论您将 DataSource 用于 DataGridView,它都可以工作,并且您可以在此解决方案中使用任何类型的数据源。

在很多很多情况下,数据实际上并不在 DataGridView 中,而是在其他地方,例如 DataTable 或某种集合(例如 List<T>)。该控件只是向用户呈现数据视图。

有多种方法可以按照您的意愿进行操作。对于这两者,数据实际上驻留在 DataTable.

表达式

一个 DataColumn 可以分配一个 Expression。请参阅 link 以了解支持的表达式类型、关键字、运算符和函数。以下将为某些行创建一个基于表达式的多个 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 在行级别工作。对于诸如 TOTALS 行之类的东西,没有 all-rows/table-wise 对应部分 - 这是因为数据通常来自 DataSource。添加计算行可能会意外地将新数据添加到该源(如数据库)。但是在代码中并不难做到:

事件驱动计算

类似于 DGV CellFormatting 给出的答案,您可以响应来自 DataTable 的事件,例如 RowChanged。在那里你可以执行任何操作并更新 table.

...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;
}

“销售额”列通过 Expression 自动维护,这意味着您无法对该列进行任何手动操作。

底部的整体平均价格也是“自动”更新的,不同的是我们不得不写一些代码来做到这一点。

您可以尝试几件事:

处理单元格值更改事件以检测计算中使用的单元格之一何时更改并计算适当的值。像这样(在新项目中尝试)

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

或者不是直接将文件读取到数据网格视图,而是将其读取到数据表中并将网格绑定到数据表。然后,您可以在数据表中添加一个表达式列来进行计算。像这样:

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