免费 .NET 电子表格控件






4.92/5 (171投票s)
使用 C# 编写的 .NET 电子表格控件。支持单元格合并、边框样式、图案背景色、数据格式、冻结、公式、宏和脚本执行。
目录 |
引言本文介绍的是一个使用 C# 编写的 .NET 电子表格控件。支持单元格合并、边框样式、图案背景色、数据格式、冻结、公式和脚本执行。 ![]() ![]() |
有什么新变化!
新功能:分组和轮廓

新功能:单元格主体

0.8.5 版本中的其他更改
- 减少了 DLL 引用。现在不再需要 unvell.common.dll 和 unvell.UIControls.dll。这些 DLL 中的所有源代码已合并到 ReoGrid 中,并已开源。
- 对于 DLL 合并,命名空间从 'Unvell' 更改为 'unvell'。
- 现在可以使用许多单元格事件,例如 CellMouseDown 和 FocusPosChanged。
- 性能改进和若干错误修复。
快照




使用指南
安装
通过窗体设计器添加控件
- 右键单击工具箱面板,选择“选择项…”
- 选择“.NET Framework 组件”选项卡
- 单击“浏览…”按钮
- 在打开文件对话框中选择“Unvell.ReoGrid.dll”
- 单击“确定”关闭对话框
- 选择工具箱中出现的控件并将其拖到设计器中,添加的控件如下所示:


通过编程添加控件
将以下必需文件添加到不同分发库的引用列表中:
- 最小版本 - 仅支持 Grid 控件和核心功能
- 编辑器版 - Grid 控件、核心功能和 GUI 编辑器支持
- 完整版 - Grid 控件、核心功能、GUI 编辑器和脚本执行
库引用表:
DLL 名称 | 描述 | 最小版本 | 编辑器版 | 完整版 |
---|---|---|---|---|
unvell.ReoGrid.dll | ReoGrid 控件、核心功能 | 是 | 是 | 是 |
unvell.ReoGridEditor.dll | ReoGrid 编辑器的控件和窗体 | 是 | 是 | |
unvell.ReoScript.dll | .NET 脚本语言引擎 | 是 | ||
unvell.ReoScriptEditor.dll | 脚本编辑器的控件和窗体 | 是 | ||
Antlr3.Runtime.dll | [第三方] ANTLR 语法工具运行时 [^] | 是 | ||
FastColoredTextBox.dll | [第三方] 功能强大且快速的带语法高亮文本编辑器 [^] | 是 |
创建实例
将以下两个 DLL 添加到项目的引用列表中:
unvell.ReoGrid.dll // 此控件
导入命名空间:
using Unvell.ReoGrid;
为控件创建实例:
// 创建实例 var grid = new Unvell.ReoGrid.ReoGridControl() { // 停靠以填充父控件 Dock = DockStyle.Fill, }; this.Controls.Add(grid);
API 用法
调用控件的三种方式:
- 调用控件方法 - 直接调用控件方法,如 SetCellData。
- 使用操作 - 通过执行操作来执行,这提供了撤销操作的能力。
- 运行脚本 - 类似 VBA 的强大宏和脚本执行功能,也可以更改控件的行为。
操作机制提供了对许多操作执行撤销/重做/重复操作的能力。用户发出的操作最好通过执行操作来完成。
调用控件方法
grid.SetCellData(new ReoGridPos(2,1), "hello world");
使用操作
1. 导入此命名空间:
using Unvell.ReoGrid.Actions;
2. 调用 grid 控件的 'DoAction' 方法,并将操作作为参数传递
grid.DoAction(new RGSetCellDataAction(new ReoGridPos(2, 1), "hello world"));
撤销或重做上次操作:
grid.Undo(); grid.Redo();
重复上次操作以应用于其他范围:
grid.RepeatLastAction(new ReoGridRange(2, 3, 5, 5));
运行脚本
请确保用于支持脚本执行的模块都已引用到您的项目中。请参阅上面的库引用表。
导入命名空间:
using Unvell.ReoScript;
运行脚本:
string script = "grid.getCell(2,1).data = 'hello world';"; grid.RunScript(script);
单元格
更改行和列
grid.SetRows(6); grid.SetCols(5);

初始值 | 最小值 | 最大值 | |
---|---|---|---|
行数 | 200 | 1 | 1,048,576 |
列数 | 100 | 1 | 32,768 |
行高 | 20 | 0 | 无限制 |
列宽 | 70 | 0 | 无限制 |
获取行数和列数:
int rows = grid.RowCount; int cols = grid.ColCount;
更改高度和宽度
将高度和宽度均设置为 10 像素
grid.SetRowsHeight(0, grid.RowCount, 10); grid.SetColsWidth(0, grid.ColCount, 10);

获取高度和宽度
int height = GetRowHeight(int row); int width = GetColumnWidth(int col);
设置单元格数据
grid[5, 2] = "hello world" grid["A2"] = 100; grid["C3"] = new object[,] { {1,2,3}, {4,5,6} }; // 设置范围 grid[new ReoGridPos("B10")] = new MyClass(); // 需要 ToString()

自动数据格式
默认情况下,所有单元格的数据格式为常规。一旦单元格数据发生更改,ReoGrid 会尝试测试数据的格式并选择默认的单元格格式化程序。如果检测到数字或百分比等数据类型,文本水平对齐将自动设置为右对齐。
要禁用自动格式检查,请将控件的 Edit_AutoFormatCell 设置为 false。
grid.SetSettings(ReoGridSettings.Edit_AutoFormatCell, false);
合并和取消合并
合并范围从 (2,1) 开始,行数为 3,列数为 4:
grid.MergeRange(new ReoGridRange(2, 1, 3, 4));

取消合并范围:
grid.UnmergeRange(new ReoGridRange(0, 0, 10, 10));
样式
即使只有一个单元格需要样式,也要始终为范围设置样式。ReoGrid 使用一个名为PlainStyleFlag的枚举值来确定要处理哪些样式。
背景色
将背景色设置为范围 (2,1,3,4):
grid.SetRangeStyle(new ReoGridRange(2, 1, 3, 4), new Unvell.ReoGrid.ReoGridRangeStyle { Flag = PlainStyleFlag.FillColor, BackColor = Color.SkyBlue, });

背景图案颜色
将背景图案颜色设置为整个网格:
grid.SetRangeStyle(ReoGridRange.EntireRange, new Unvell.ReoGrid.ReoGridRangeStyle { Flag = PlainStyleFlag.FillPattern | PlainStyleFlag.FillColor, BackColor = Color.LightYellow, FillPatternColor = Color.SkyBlue, FillPatternStyle = System.Drawing.Drawing2D.HatchStyle.DiagonalBrick });

文本颜色
grid.SetRangeStyle(new ReoGridRange(1, 1, 1, 1), new ReoGridRangeStyle() { Flag = PlainStyleFlag.TextColor, TextColor = Color.Red, });

文本对齐

将单元格的文本水平对齐设置为居中:
grid.DoAction(new RGSetRangeStyleAction(new ReoGridRange(0, 0, 3, 3), new ReoGridRangeStyle { Flag = PlainStyleFlag.HorizontalAlign, HAlign = ReoGridHorAlign.Center, }));

文本换行
将文本换行模式设置为“WordWrap”:(默认是无换行)
grid[1, 1] = "How many beers can you drink?"; grid.SetRangeStyle(new ReoGridRange(1, 1, 1, 1), new ReoGridRangeStyle() { Flag = PlainStyleFlag.TextWrap, TextWrapMode = TextWrapMode.WordBreak, });

字体
更改字体名称和大小:
grid.SetRangeStyle(new ReoGridRange(1, 1, 1, 1), new ReoGridRangeStyle() { Flag = PlainStyleFlag.FontSize | PlainStyleFlag.FontName, FontName = "Arial", FontSize = 20, });

移除样式
始终从范围中移除样式,即使只需要一个单元格。通过指定PlainStyleFlag来设置要移除的样式。
从指定范围移除背景色:
grid.RemoveRangeStyle(new ReoGridRange(2, 2, 3, 3), PlainStyleFlag.FillAll);
边框
在指定范围周围设置粗体轮廓边框:
grid.SetRangeBorder(new ReoGridRange(2, 1, 5, 3), ReoGridBorderPos.Outline, new ReoGridBorderStyle { Color = Color.Magenta, Style = BorderLineStyle.BoldSolid, });

通过传递不同的ReoGridBorderPos标志来更改边框的设置位置:
grid.DoAction(new RGSetRangeBorderAction(new ReoGridRange(2, 1, 5, 3), ReoGridBorderPos.All, new ReoGridBorderStyle { Color = Color.Red, Style = BorderLineStyle.Dashed })); // 通过操作设置

要移除边框:
grid.RemoveRangeBorder(new ReoGridRange(2, 1, 5, 1), ReoGridBorderPos.All);
或将边框颜色设置为 Color.Empty 以移除边框
grid.SetRangeBorder(new ReoGridRange(2, 1, 5, 3), ReoGridBorderPos.All, new ReoGridBorderStyle { Color = Color.Empty };
GetRangeBorder方法可用于从范围获取边框信息:
ReoGridRangeBorderInfo GetRangeBorder(ReoGridRange range);
数据格式
导入命名空间以使用数据格式功能:using Unvell.ReoGrid.DataFormat;
使用SetRangeDataFormat方法指定单元格数据格式。
SetRangeDataFormat(ReoGridRange range, CellDataFormatFlag flag, object argument)
例如: grid.SetRangeDataFormat(ReoGridRange.EntireRange, CellDataFormatFlag.Number, new NumberDataFormatter.NumberFormatArgs() { // 小数位数 DecimalPlaces = 4, // 负数样式: (123) NegativeStyle = NumberDataFormatter.NumberNegativeStyle.RedBrackets, // 使用千位分隔符: 123,456 UseSeparator = true, }); // 或者使用操作设置 RGSetRangeDataFormatAction(ReoGridRange range, CellDataFormatFlag format, object dataFormatArgs);
测试: grid[1, 1] = 12345; grid[1, 2] = 12345.67890; grid[2, 1] = -1234; grid[2, 2] = -1234.56789;

内置数据格式化程序表:
类型 | CellDataFormatFlag | 参数 |
---|---|---|
数字 | CellDataFormatFlag.Number | NumberDataFormatter.NumberFormatArgs |
日期时间 | CellDataFormatFlag.DateTime | DateTimeDataFormatter.DateTimeFormatArgs |
百分比 | CellDataFormatFlag.Percent | PercentDataFormatter.PercentFormatArgs |
货币 | CellDataFormatFlag.Currency | CurrencyDataFormatter.CurrencyFormatArgs |
文本 | CellDataFormatFlag.Text | 无 |
单元格主体
ReoGrid 提供了一个接口,支持您创建自定义单元格,例如创建普通的 Windows 控件。

单元格主体
自定义单元格主体必须继承自 CellBody
或实现 ICellBody
。CellBody
类提供了以下虚方法,可以被重写以处理用户的操作
public interface ICellBody
{
void OnSetup(ReoGridControl ctrl, ReoGridCell cell);
Rectangle Bounds { get; set; }
void OnBoundsChanged(ReoGridCell cell);
bool AutoCaptureMouse();
bool OnMouseDown(RGCellMouseEventArgs e);
bool OnMouseMove(RGCellMouseEventArgs e);
bool OnMouseUp(RGCellMouseEventArgs e);
bool OnMouseEnter(RGCellMouseEventArgs e);
bool OnMouseLeave(RGCellMouseEventArgs e);
void OnMouseWheel(RGCellMouseEventArgs e);
bool OnKeyDown(KeyEventArgs e);
bool OnKeyUp(KeyEventArgs e);
void OnPaint(RGDrawingContext dc);
bool OnStartEdit(ReoGridCell cell);
object OnEndEdit(ReoGridCell cell, object data);
void OnGotFocus(ReoGridCell cell);
void OnLostFocus(ReoGridCell cell);
object OnSetData(object data);
object OnSetText(string text);
object GetData();
}
所有者绘图
通过重写 OnPaint
方法在单元格中绘制任何内容
private class MyCellBody : CellBody
{
public override void OnPaint(RGDrawingContext dc)
{
// draw an ellipse inside cell, 'Bounds' is the cell's boundary
dc.Graphics.DrawEllipse(Pens.Blue, base.Bounds);
}
}

处理鼠标和键盘事件
通过重写 OnKeyDown 和 OnMouseDown 等方法来处理键盘和鼠标事件。
public override bool OnMouseDown(RGCellMouseEventArgs e)
{
// translate cursor position to percent
int value = (int)Math.Round(x * 100f / (Bounds.Width));
// update cell's data, but do not set the data to cell directly, call methods of control instead
grid.SetCellData(e.CellPosition, value);
}

通过公式进行数据绑定
可以使用公式绑定两个单元格的数据,例如:
grid["C7"] = "=E7";

事件
示例:通过处理事件设置可编辑范围
仅允许在指定范围内进行文本编辑
var editableRange = new ReoGridRange(3,1,2,3);
grid.SetRangeBorder(editableRange, ReoGridBorderPos.Outline, ReoGridBorderStyle.SolidBlack);
grid[2, 1] = "Edit only be allowed in this range:";
grid.BeforeCellEdit += (s, e) => e.IsCancelled = !editableRange.Contains(e.Cell.GetPos());

选择范围
PickRange 方法用法:
PickRange(Func<...,bool> handler, Cursor)
其中
- handler 是一个匿名函数,当用户选择范围时会被调用。如果范围符合预期,则从该处理程序返回 true。返回 false 将继续选择范围,直到从该函数返回 true 为止。
- Cursor 是用户选择范围时显示的鼠标光标。选择完成后,光标将恢复为默认。
例如:
grid.PickRange((inst, range) => { MessageBox.Show("用户选择了范围: " + range.ToString()); return true; }, Cursors.Hand);

冻结
将冻结设置为从第 5 行开始:
grid.FreezeToCell(5, 0);

取消冻结:
Unfreeze
获取冻结位置:
ReoGridPos pos = grid.GetFreezePos();
缩放
grid.ZoomIn(); // +0.1f 比例因子 grid.ZoomOut(); // -0.1f 比例因子 grid.ZoomReset(); // 重置为 1f 比例因子

设置比例因子
grid.SetScale(2f, Point.Empty);

分组和轮廓
轮廓可以附加到行或列,例如

添加轮廓
grid.AddOutline(RowOrColumn.Row, 3, 5); // group rows from 3, number of rows is 5
grid.AddOutline(RowOrColumn.Column, 5, 2); // group columns
请注意以下可能导致的异常- OutlineIntersectedException - 当要添加的轮廓与已添加到工作表中的另一个轮廓相交时。
- OutlineAlreadyDefinedException - 当要添加的轮廓与已添加到工作表中的另一个轮廓相同时。
- OutlineTooMuchException - 当轮廓级别数达到最大可用级别 10 时。
以及用于折叠和展开轮廓的这些函数
grid.CollapseOutline(RowOrColumn.Row, 3, 5);
grid.ExpandOutline(RowOrColumn.Row, 3, 5);
以及删除或清除轮廓
grid.RemoveOutline(RowOrColumn.Row, 3, 5);
grid.ClearOutlines(RowOrColumn.Row);
自动行为:折叠、展开和删除
如果附加到(以…结尾)某行的已展开轮廓被隐藏,或该行的高度设置为零,则轮廓会自动折叠。如果分组某些行的折叠轮廓全部显示,或行高不为零,则轮廓会自动展开。此行为无法禁用。如果附加到行或列的轮廓全部被删除,则该轮廓会自动删除。
轮廓与冻结
轮廓可以与冻结一起工作,最大行数为 9,例如

设置
隐藏滚动条和页眉
grid.SetSettings(ReoGridSettings.View_ShowScrolls | ReoGridSettings.View_ShowHeaders, false);

隐藏网格线
grid.SetSettings(ReoGridSettings.View_ShowGridLine, false);

禁用通过鼠标调整行高和列宽
grid.SetSettings(ReoGridSettings.Edit_AllowAdjustRowHeight | ReoGridSettings.Edit_AllowAdjustColumnWidth, false);
公式
以 '=' 开头的单元格文本将自动作为公式处理。当引用的单元格数据发生更改时,带有公式的单元格将自动更新。

当公式更改时,将计算并显示公式的值。

以 (') 开头的数据将被忽略为公式。
脚本执行
ReoGrid 控件支持 ECMAScript 风格的脚本执行。但是,您可以使用最小化构建包,它不提供脚本功能。请查看上面的引用表。
通过脚本设置数据
变量 'grid' 是一个指向 grid 控件的全局对象。脚本示例:
// 获取 pos(0,0) 的单元格并设置单元格数据为 'hello world' grid.getCell(0,0).data = 'hello world';

ReoGrid 使用 ReoScript 来实现脚本执行。ReoScript 是一个 ECMAScript 风格的脚本语言引擎,用于 .NET 应用程序,更多详细信息请访问 reoscript.codeplex.com。
处理数据更改事件

通过返回 false 来取消操作
脚本中处理的事件返回 false 以通知控件取消当前操作。
grid.cellBeforeEdit = function(cell) {
if(cell.pos.row == 2 && cell.pos.col == 3) {
return false;
}
};
有关脚本执行的更多详细信息,请访问 此处。
自定义函数
在 C# 中添加自定义函数
grid.Srm["sqrt"] = new NativeFunctionObject("sqrt", (ctx, owner, args) => { if (args.Length < 1) return NaNValue.Value; else return Math.Sqrt(ScriptRunningMachine.GetDoubleValue(args[0], 0)); });


通过运行脚本添加自定义函数
将函数添加到script对象将使其在公式和脚本中都可用。



通过运行脚本在 C# 中添加函数
也可以通过运行脚本来创建 C# 中的函数:(例如,lambda 表达式)
grid.RunScript("script.myfunc = data => '[' + data + ']';");
XML 序列化
保存到文件或流:
grid.Save(@"d:\path\grid.rgf"); grid.Save(stream); // 保存到流
Grid 数据以 XML 格式保存,如下所示:

从文件或流加载:
grid.Load(@"d:\path\grid.rgf"); grid.Load(stream); // 从流加载
控件外观
// 使用主题颜色创建控件样式实例 ReoGridControlStyle rgcs = new ReoGridControlStyle(Color.White, Color.DarkOrange, false); // 将文本颜色设置为“白色” rgcs.SetColor(ReoGridControlColors.GridText, Color.White); // 应用外观样式 grid.SetControlStyle(rgcs);
控件外观将更改为:

重置控件
将控件重置为默认状态。
grid.Reset();
实现
工作表数据管理
分页
使用默认的二维数组来管理单元格数据会占用大量内存,即使没有初始化任何元素。一种方法是仅为已附加元素的数组分配内存。并通过将一个大数组分成许多小数组来减少内存使用,这就是分页。

在查找单元格之前,需要先找到其所在页。对于一维数组,计算其页索引和单元格索引:
int pageIndex = index / pageSize; int cellIndex = index % pageSize;
如果页的大小是 2 的幂,则使用位运算而不是除法会更有效。
树
通过组合一些页面,将它们链接到一个更大的页面作为父索引页,可以帮助减少内存使用。

所有单元格数据都将附加到索引页的最底部,例如:

任何未访问过的索引页和单元格都将为 null,它们不占用内存。

要访问一个单元格,根据树的深度,一个单元格的索引将被计算两次(树的深度为 2):

较小的页面占用内存较少,但如果它们太小,则需要创建更深的树才能获得更大的数组容量。更深的树将在访问单元格时花费更多时间。考虑在页面大小和树深度之间取得良好平衡是最重要的。目前 ReoGrid 使用以下页面大小值:
- 行数:128
- 列数:32
- 树深度:3
- 行数:128^3 = 2,097,152
- 列数:32^3 = 32,768
尽管单元格的行容量可以达到 2,097,152,但为了适应行标题的数据结构,需要将行数限制为 1,048,576。
代码实现
在 ReoGrid 中计算索引、获取和设置分页索引的二维数组元素,getter 和 setter 如下:
get { Node node = root; // MaxDepth 为 3 (当行页面大小为 128 时,RowBitLen 为 7) int r = (row >> (RowBitLen * d)) % RowSize; // ColBitLen 为 5 (当列页面大小为 32 时) int c = (col >> (ColBitLen * d)) % ColSize; node = node.nodes[r, c]; if (node == null) return default(T); }
set { Node node = root; for (int d = MaxDepth - 1; d > 0; d--) { // RowBitLen 为 7 (当行页面大小为 128 时) int r = (row >> (RowBitLen * d)) % RowSize; // ColBitLen 为 5 (当列页面大小为 32 时) int c = (col >> (ColBitLen * d)) % ColSize; Node child = node.nodes[r, c]; if (child == null) { if (value == null) { return; } else { child = node.nodes[r, c] = new Node(); if (d > 1) { child.nodes = new Node[RowSize, ColSize]; } } } node = child; } if (node.data == null) { if (value == null) { return; } else { node.data = new T[RowSize, ColSize]; } } node.data[row % RowSize, col % ColSize] = value; }
待改进之处
存在一个问题,也是一个性能问题,ReoGrid 不知道数据的边界。考虑到当用户选择整行或整列,并且 ReoGrid 被要求计算总和值时。唯一要做的事情就是逐个获取单元格的数据,并计算它们的总和,如果工作表很大,这将花费很长时间。
解决方案 1:为每个页面添加有效数据边界?
添加数据边界以识别有效数据的开始和结束位置,似乎可以提高迭代速度。代码如下:
double sum = 0; foreach(var page in sheetData) { for(int r = page.minRow; r <= page.maxRow; r++) { for(int c = page.minCol; c <= page.maxCol; c++) { sum += page[r, c].data; } } }
解决方案 2:有效行和有效列标识
向行标题和列标题添加标志,以标识该行或列是否包含任何数据。代码如下:
double sum = 0; for (int r = 0; r < sheet.RowCount; r++) { if(sheet.IsEmptyRow(r)) continue; for (int c = 0; c < sheet.ColCount; c++) { if(sheet.IsEmptyCol(c)) continue; sum += sheet[r, c].data; } }
其他解决方案?
在以下几点上需要改进有效的内存管理方法,如果您有任何答案,请告诉我。
- 更少的内存使用
- 更快的访问速度
- 有效的数据聚合以计算总和、平均值、计数等。
性能测试
2014 年 1 月 13 日星期一 (ReoGrid 版本 0.8.5)测试项 | 总周期 | 耗时 (ms) | 单个周期 (μs) | 内存使用 (MB) |
顺序读写 (1000x1000) | 2,000,000 | 130 | 0.065 | 9.6 |
顺序读写 (1,000,000x100) | 20,000,000 | 1297 | 0.06485 | 46 |
间隔读写 (10,000x10,000, 间隔: 100) | 20,000 | 81 | 4.05 | 35 |
随机读写 | 20,000 | 263 | 13.15 | 73 |
遍历空范围 | 200,000,000 | 150 | 0.00075 | - |
填充 1,000,000 行 | 1,000,000 | 110 | 0.11 | 145 |
求和 1,000,000 行 (迭代器) | 1,000,000 | 113 | 0.113 | - |
求和 1,000,000 行 (For 循环) | 1,000,000 | 50 | 0.05 | - |
测试用例可在 ReoGrid 项目的 TestCases 解决方案中找到。
单元格数据更新
当用户编辑单元格,或单元格需要用新数据更新时,ReoGrid 在单元格数据更新期间执行以下操作。

迭代 - 单元格搜索
由于索引页,从未访问过的单元格将为 null,因此可以跳过这些页面和单元格,跳过过程将有助于提高迭代速度。目前 ReoGrid 在以下情况下执行跳过过程:

- 空单元格 - 没有数据和自定义样式的单元格,在迭代期间会被跳过
- 空索引页 - 没有附加任何单元格的索引页,在迭代期间会被跳过
- 无效单元格 - 被另一个单元格合并的单元格,它将变得无效并在迭代期间被跳过
for (int r = startRow; r < r2; r++) { for (int c = startCol; c < c2; ) { if (IsPageNull(r, c)) { // 跳过当前页面 (+= 保持不变) c += (ColSize - (c % ColSize)); } else { T obj = this[r, c]; int cspan = 1; if (!ignoreNull || obj != null) { cspan = iterator(r, c, obj); // 迭代器回调 if (cspan <= 0) return; } // 当 cspan > 1 时跳过合并的单元格 // cspan 由高级迭代器返回 c += cspan; } } }
合并实现
设计合并实现时考虑的要点
- 更快速地查找合并范围的边界(当用户单击单元格内部时)- 当控件接收到鼠标事件(鼠标按下或鼠标悬停)时,应选择整个合并范围,而不是单个单元格。需要检查被单击的单元格是否是合并范围的一部分,如果是,如何快速找到该范围。
- 应跳过合并范围内的网格线绘制 - 默认情况下,电子表格会绘制水平和垂直网格线来填充显示给用户的视口。但是,应跳过合并范围内的网格线,即使合并范围显示时没有填充任何背景色。

两个单元格被定义为特殊单元格,一个称为合并起始单元格,另一个称为合并结束单元格。它们分别定义在合并范围的左上角和右下角。该范围内的所有单元格都将被立即初始化,并且它们将保留两个指向合并起始单元格和合并结束单元格的指针引用。

当用户单击包含在范围内的任何单元格时,现在可以通过两个指针(合并起始单元格和合并结束单元格)快速找到该范围。另一件事是需要跳过在合并范围内的网格线绘制,这也可以通过这两个引用来计算应跳过多少个单元格。
int skips = merge-end-cell.col - merge-start-cell.col;

需要改进之处
初始化内部单元格以保留引用会占用大量内存,合并时创建单元格实例也需要很长时间。是否有不必要初始化内部单元格的解决方案?
边框渲染
加速绘制边框的思路
一个不太好的设计是逐个单元格绘制边框,这当然是最容易实现的。为了加速边框绘制,一个直接的想法是找到水平或垂直方向上相同的边框,然后绘制一次。

查找相同边框的时机
在绘制期间查找相同边框是可以接受的解决方案,但仍可优化。电子表格渲染的时间成本很高,需要尽可能减少在渲染时执行的操作。考虑一些可以预先计算并保存在内存中,在绘制期间直接获取和使用会更有效(用空间换时间)。
解决方案
ReoGrid 使用一个名为span的变量来标识水平和垂直方向上的相同边框。当设置新边框或删除任何边框时,此变量将被管理和更新。ReoGrid 在边框更改时维护相同边框的信息和关系,并始终只绘制一次相同边框。

连续递减跨度
使用span可以大大提高绘图速度。但有一个棘手的问题是滚动。始终只绘制可见区域内的内容以提高渲染速度是一个好方法。但是,当保持span信息的边框滚动出可见区域时,后续的边框也会消失。

为了解决这个问题,一种想法是将span附加到每个边框。然后不用关心边框从哪里开始,每个边框都指示后续边框如何使用span进行绘制。

待改进之处
虽然使用span来加速渲染很有效,但span的维护变得非常复杂,尤其是在插入和删除单元格时。当行和列变得不可见时,span信息不会被排除,它仍然包含在计算和渲染中。
我还在考虑只在内存中保留一个边框,它有一个属性span = 5,但是当它滚动出可见区域时,如何检测和绘制它。当列插入时也有同样的问题。
单元格样式管理
通常用户会将样式应用于整行或整列。在样式管理实现时需要考虑这一点。

将一些样式应用于页眉,而不是单元格
ReoGrid 将样式信息保存在每个单元格中,除了某些特殊情况。它们是:
- 将样式应用于整个电子表格(无论有多少单元格存在)
- 将样式应用于整行或整列(无论有多少单元格存在)
当用户选择整个电子表格并为其设置样式时,ReoGrid 将样式保存到名为RootStyle的变量中,该变量是工作表的成员变量。并且每个行标题、列标题都有相同的结构来存储行样式和列样式。设置三个特殊位置样式的示例:

待改进之处
虽然考虑将样式放在三个特殊位置可以帮助减少内存使用。但我认为还有更好的解决方案。例如,始终将样式保存到范围,而不是单元格,即使只有一个单元格使用它。对于此实现,SetCellStyle当前定义为非公共方法。我建议只使用SetRangeStyle。

视口控制器
ReoGrid 中引入了一个类似 MVC 的设计来实现表示层的渲染。根据 Excel,至少需要向最终用户呈现两种不同的布局。

一个重要的事情是,不同的布局具有不同的行为,例如滚动行为和页眉编辑。
普通布局

页面布局

ReoGrid 中的实现
首先要做的就是将控件的整个显示区域分成几个小视图,并且所有视图都只渲染自己的内容。

由于行标题和列标题具有不同的滚动方向,因此按滚动方向分割视图似乎更好。

如何滚动
GDI+ 图形对象提供了 TranslateTransform 方法,可用于更改绘图上下文的原点,这对于实现滚动很有用。无需更改电子表格中每个单元格的位置,只需使用此方法通过指定偏移量来更改渲染上下文的起始位置。
接口定义
一些视图不一定需要滚动,减少执行 TranslateTransform 的次数有助于节省渲染时间,ReoGrid 中设计了一套不同的接口,无滚动视图:
interface View { // 在源代码中称为 'IPart' void Draw(); // 绘制不带滚动的内容 Rectangle Bounds { get; set; } // 视图边界 }
interface ScrollView : View { // 在源代码中称为 'IViewPart' void Draw(); // 绘制不带滚动的内容,并通过 ViewStart 执行 TranslateTranform void DrawView(); // 绘制带滚动的内容 void Scroll(int x, int y); // 执行滚动,将偏移量应用于 ViewStart Point ViewStart { get; set; } // 水平和垂直方向的偏移量 }
可见区域
始终渲染整个电子表格的可见区域可以提高渲染性能。滚动后,每个视图都维护可见区域,只绘制包含或与可见区域相交的内容。

冻结实现
一旦实现了像行标题视图那样用于渲染行标题的视图,只需克隆该视图,禁用其滚动功能或更改其滚动方向,即可更轻松地实现冻结。

Excel 和 ReoGrid 之间不同的滚动效果
ReoGrid 使用基于像素的滚动计算,但 Excel 似乎使用基于单元格的滚动计算。我认为每种方法都有不同的优点和缺点。

- Excel
- 滚动起始位置:单元格的左上角
- 按单元格数量滚动
- 优点 - 由于按单元格数量滚动,一旦滚动条的值改变,就可以非常快速地找到要显示的单元格位置。当冻结、折叠轮廓或隐藏任何行或列时,即使在拆分的窗口渲染中,它仍然可以快速工作。
- 缺点 - 无法强制滚动从行中间开始。当行很大时,滚动从行的顶部或底部开始,这在用户体验方面稍显不佳。
- ReoGrid
- 滚动起始位置:任意像素
- 按屏幕像素滚动
- 优点 - 改善用户体验,使滚动更流畅
- 缺点 - 由于按屏幕像素滚动,当滚动条的值改变时,需要决定电子表格从哪一行哪一列开始渲染,还需要将像素转换为单元格索引,此转换会在滚动期间花费一些时间。我曾考虑并尝试通过多种方法解决此问题,其中一种是进行二分搜索。
通过二分搜索将像素位置转换为单元格索引
行和列的所有标题都有像素坐标 (x,y),宽度或高度与它们一起保存在内存中。由于标题始终按索引和像素排序,因此可以使用二分搜索将像素转换为单元格索引,我发现这非常快。

层管理
划分并创建几个表示层,确保它们只做不同的事情,这将使一切变得更容易。通过分析 Excel 的 UI,我认为可能需要实现 4 个层:

多层管理如下:

事件分发
与其他 UI 框架、应用程序一样,当有多个层时,它们在控件的同一表示区域内执行不同的操作,它们都可以接收鼠标和键盘的事件。如何决定哪个层应该获得鼠标事件,或者键盘事件应该分发到哪个层,ReoGrid 将设计一种机制和一套接口,主类称为LayerManagement,每个层的接口称为ILayerController。其中一个LayerController是IViewportController。事件将被路由到每个层,请求它们按如下方式处理事件。

致谢
感谢许多人为这个软件和文章提供的帮助,非常抱歉未能在此一一列出。(按日期排序)
- 感谢 TL Wallace 关于单元格类型的良好建议。
- 感谢 i00 提供拼写检查支持。
- 感谢 Bill Woodruff 帮助解决平台兼容性问题,并为改进本文提出建议。
- 感谢 fred 帮助翻译成法语
- 感谢 Champion Chen 为此控件提出的许多想法
- 感谢 niujingui 提出分组和轮廓功能(将在 v0.8.5 中提供)
- 感谢 ves.p 提出和开发此控件的某些新功能
- 感谢 V.V.Shinkevich 和 Teruteru314 帮助解决非英语文化环境下的加载错误
- 感谢 José Santos 对此控件的许多建议
- ANTLR 3 [^]
- FastColoredTextBox [^]
历史
日期 | 控件版本 | 更新内容 |
2014 年 1 月 24 日 | ReoGrid 0.8.5 | 新增:轮廓 & 单元格主体 性能改进 命名空间从 'Unvell' 改为 'unvell' |
2014 年 1 月 15 日 | ReoGrid 0.8.4 | 稳定性改进 文章:添加实现描述 |
2013 年 12 月 16 日 | ReoGrid 0.8.2 | 添加文本换行功能 添加文本对齐设置页面 添加剪贴板事件支持 添加更多脚本事件支持 添加更多演示 |
2013 年 12 月 9 日 | ReoGrid 0.8.1 | 创建 |
关于
ReoGrid 目前仍在开发中,获取最新下载、报告错误,并关注我们:
请随时提出您的建议、问题和反馈。
技术支持和定制开发服务可在 unvell.com 获取。
本代码和信息按“原样”提供,不附带任何形式的保证,无论是明示的还是暗示的,包括但不限于对适销性和/或特定用途适用性的暗示保证。