带公式支持的 DataGridView





5.00/5 (3投票s)
允许您向单元格添加公式的自定义 DataGridView
引言
本文介绍的项目是一个自定义的 DataGridView
实现,允许您向其单元格添加复杂公式。
公式引擎的实现基于 BNFUP 通用编译器,具体解释请参见本文。
FormulaDataGridView
项目包含网格本身的实现,而 GridFormulas
项目用于公式引擎的实现。BNFUP
项目对于提供解析和编译服务是必需的。
因此,如果您计划在项目中使用此网格,则必须引用三个不同的类库:BNFUP.dll、FormulaDataGridView.dll 和 GridFormulas.dll。
公式以纯文本字符串形式提供,是可以使用 -、+、*、/ 和 ^(幂运算)运算符的算术表达式。
您可以使用这些运算符的元素类型如下:
- 数字,可以包含小数。
- 变量。有两个变量,
col
包含包含公式的单元格的列索引,row
包含行索引。 - 单元格引用。您可以通过在括号 ([ 和 ]) 中引用网格的单元格来获取其值。要指示列和行,您可以使用任何表达式,但值将被四舍五入为整数。用冒号字符 (:) 分隔两个表达式。第一个位置用于列索引,第二个位置用于行,例如:
[col:row-1]
指的是包含公式的单元格上方单元格。 - 数学函数。它们只有一个参数,可以是任何算术表达式。允许的函数有
abs
(绝对值)、ceil
(大于或等于参数的最小整数)、floor
(小于或等于参数的最大整数)、ln
(自然对数)、log
(以 10 为底的对数)、sin
(正弦)、cos
(余弦)、tan
(正切)、exp
(e 的参数次幂)和sqrt
(平方根)。例如,使用sqrt([0:0])
,您可以获得网格中第一个单元格的平方根。 - 聚合函数。它们有两个逗号分隔的参数,必须是单元格引用。第一个参数指示要处理的第一行,第二个参数指示最后一个单元格。由这两个单元格定义的矩形中的所有单元格都将由该函数处理。可用的函数有
max
和min
(最大值和最小值)、sum
和prod
(值的总和和乘积)、avg
(算术平均值)、var
和svar
(方差和样本方差)以及sd
和ssd
(标准差和样本标准差)。例如,sum([0:row],[col-1:row])
计算公式左侧所有单元格的总和。
DataGridView
非常易于使用,您几乎不需要编写几行代码,但为了更好地理解,我已将 TestForm
项目添加到解决方案中,该项目展示了如何使用所有网格功能。您可以在我的博客的这篇文章中阅读有关此项目的更多信息,西班牙语版请点击此处。
Using the Code
实际上,FormulaDataGridView
控件只不过是一个普通的 DataGridView
。没有添加自定义单元格类型或其他复杂的东西。它可以像往常一样使用。公式由网格内部管理,使用一个将它们与单元格坐标链接起来的 Dictionary
,我只使用了几个新的属性和方法来扩展其功能。以下是它们的列表:
public string LanguageFile { get; set; }
public TextBox FormulaEditor { get; set; }
public FormulaBase GetFormula(int col, int row);
public void SaveCSV(string filename);
public void ReadCSV(string filename);
public void UpdateFormula();
public void UpdateFormula(string formula, int col, int row);
public void BindFormulas();
public void Initialize();
通过 FormulaEditor
属性,您可以提供一个 TextBox
控件,允许用户编辑网格中的公式。当公式更改后,您必须使用不带参数的 UpdateFormula
方法来编译它。如果文本为空字符串,则公式将从网格中所有选定的单元格中删除;否则,每个选定的单元格都将获得一个编译公式的实例。它们都是不同的,因此,如果您更改其中任何一个,其余的将保持不变。
您还可以使用带参数的 UpdateFormula
方法版本以编程方式更改单元格的公式。您必须提供一个包含公式的 string
以及单元格的列和行索引。
如果您将 DataGrid
绑定到某种数据源,您也可以通过将公式作为以 =
字符开头的文本字符串来提供。在这种情况下,您必须调用 BindFormulas
方法来指示网格编译它们。例如,考虑一个如下的 SQL 命令:
SELECT Q1, Q2, Q3, Q4, '=sum([0:row],[col-1:row])' as Total FROM TABLE;
如果您使用这样的命令填充网格,将创建五列,最后一列将是一个计算列,包含每行前四列的总和。
使用 Initialize
方法,您可以清除网格中的所有公式。
SaveCSV
和 ReadCSV
方法可用于将网格数据保存或从 csv 文件加载。该文件的第一行将包含列标题,字段必须用分号字符 (;) 分隔,该字符在公式中任何地方都未使用。如果单元格具有关联的公式,则公式将以文本形式保存到文件中,在相应单元格位置以 =
字符开头。
GetFormula
方法返回与给定单元格关联的公式,如果单元格没有公式,则返回 null
。公式具有在 GridFormulas
项目中定义的通用 FormulaBase
类型。从该类中,您只需要知道 CellReferences
属性,它以 Point 结构的形式枚举公式中引用的所有单元格(如果您希望在选择包含公式的单元格时向用户提供反馈),以及 Value
属性,它返回公式的结果,和 AsString
属性,它以文本字符串形式提供公式。
该项目的真正复杂性不在于 DataGridView
本身,而在于 GridFormulas
项目。由于您拥有源代码,因此可以通过添加更多函数来扩展公式中使用的语言。为此,您必须处理 BNF 规则和 BNFUPEditor
工具,该工具允许您定义用于编译它们的语言。请参阅上面引用的文章,了解如何使用此工具。这是项目中当前实现的语言的 BNF 格式定义:
<number>::=<digit>[<rnumber>]
|<decimalsep><rdecimal>;
<rnumber>::=<digit>[<rnumber>]
|<decimalsep><rdecimal>;
<rdecimal>::=<digit>[<rdecimal>];
<digit>::={0-9};
<decimalsep>::={,\.};
<variable>::='col','row';
<cell>::='['<expr>'';''<expr>']';
<function>::='sum','avg','max','min','prod','var','sd','svar','ssd' '('<cell>','<cell>')'
|'sqrt','ln','exp','sin','cos','tan','abs','ceil','floor','log' '('<expr>')';
<<expr>>::=<expr2>['+','-'<expr>];
<expr2>::=<expr1>['*','/'<expr2>];
<expr1>::=<expr0>['^'<expr1>];
<expr0>::=['-']<element>;
<element>::=<pexpr>
|<number>
|<variable>
|<cell>
|<function>;
<pexpr>::='('<expr>')';
在 GridFormulas
项目中实现函数的类是 Function
。如果您计划向语言添加更多函数,请使用 BNFUPEditor
规则编辑器修改 functions.bnf 文件(在 TestForm
项目目录中)中的规则,并将新的函数名称和实现添加到 Function
类。以下是您必须为此考虑的方法:
public override bool AddItem(ICompilableObject item);
public override double Value { get; set; }
private double PerformFunction(int c1, int r1, int c2, int r2);
private double PerformFunction();
当对象构建时,会调用 AddItem
方法。在这里,您必须验证函数名称。在 Value
属性中,您必须返回函数结果的值。PerformFunction
方法是您必须实现函数本身的地方。有两个版本。不带参数的版本用于数学函数,它们从 _exp
变量获取参数。带参数的版本用于聚合函数,参数是处理单元格的列和行的起始和结束索引。
修改了语言语法和 GridFormulas.dll 库后,您有两种选择。一种是使用 FormulaDataGridView
控件的 LanguageFile
属性来传递此文件的路径,以便网格使用扩展语言。另一种是替换 FromulaDataGridView
项目资源文件中的 .bnf 语言文件,并让网格在创建时使用它来构建规则表。
就这样!我认为这个控件非常易于使用,希望它能对某些人有所帮助,以额外的功能改进他们的应用程序。
感谢阅读!!!
历史
- 2017年1月14日:初始版本