TStringGrid的性能差

6

我有一个包含10列的TStringGrid。向其中添加500行需要约2秒钟的时间。这种性能表现正常吗?

对我来说,这似乎有点慢。

我从数据库查询中获取数据。如果我循环遍历查询结果但不将结果写入StringGrid,则该过程大约需要100毫秒,因此不是数据库拖慢了速度。

一旦添加了行,StringGrid的性能就很好。

这是我正在使用的代码:

Grid.RowCount := Query.RecordCount;
J := 0;

while not Query.EOF do
begin
    Grid.Cells[0,J]:=Query.FieldByName('Value1').AsString;
    Grid.Cells[1,J]:=Query.FieldByName('Value2').AsString;
    Grid.Cells[2,J]:=Query.FieldByName('Value3').AsString;
    // etc for other columns.
    Inc(J);
    Query.Next();
end;

实际代码要复杂一些(表格列与查询列并不完全对应),但这是基本思路。


没有代码,这个问题是无法回答的。"不,这不正常"是可以接受的,"是的,它是"也可以,"可能 - 这取决于"也可以。你是自定义绘制网格,还是使用默认绘制?发布你用来填充网格的代码,也许有人可以帮忙。 - Ken White
TListView可能是更好的控件选择。 - David Heffernan
我已经添加了一些示例代码。网格只是一个标准的TStringGrid;没有自定义绘图。 - awmross
FieldByName 反复调用会很慢且毫无意义。 - Premature Optimization
你为什么不使用一个数据库感知的网格控件呢? - Warren P
@WarrenP 不确定。我没有写这段代码。 - awmross
11个回答

7

当处理大量记录时,我发现使用适当的TField变量非常重要。 FieldByName每次迭代字段集合,因此不是性能最好的选项。 在循环之前定义每个字段,如下所示:

var
  f1, f2: TStringField;
  f3: TIntegerField;

begin
  // MyStringGrid.BeginUpdate; // Can't do this
  // Could try something like this instead:
  // MyStringGrid.Perform(WM_SETREDRAW, 0, 0);
  try
    while ... do
    begin
      rowvalues[0] := f1.AsString;
      rowvalues[1] := f2.AsString;
      rowvalues[2] := Format('%4.2d', f3.AsInteger);
      // etc 
    end;
  finally
    // MyStringGrid.EndUpdate; // Can't - see above
    // MyStringGrid.Perform(WM_SETREDRAW, 1, 0);
    // MyStringGrid.Invalidate;
  end;
end;

除此之外,还需要使用BeginUpdate/EndUpdate,并在必要时调用Query.DisableControls。


我在我的问题中明确指出这不是一个数据库问题。访问数据库所需的时间是可以忽略不计的(约100毫秒)。 - awmross
确实如此。然而,实现良好的数据访问最佳实践方法对应用程序的性能没有坏处。此外,“仅循环查询”并不是衡量是否存在数据库问题的标准——如果未访问字段/值,则无法获得真实的性能结果。一如既往,通过应用不止一个“修复”措施,通常可以实现性能改进。 - shunty
我的错。真遗憾我在那些年写下那段代码时没有带上编译器。也许你可以提供一个替代方案,例如像这里的WM_SETREDRAW。 - shunty

6
解决方法是一次性使用“Rows”属性将一行中的所有值加起来。
现在我的代码看起来像这样:
Grid.RowCount := Query.RecordCount;
rowValues:=TStringList.Create;
J := 0;

while not Query.EOF do
begin
    rowValues[0]:=Query.FieldByName('Value1').AsString;
    rowValues[1]:=Query.FieldByName('Value2').AsString;
    rowValues[2]:=Query.FieldByName('Value3').AsString;
    // etc for other columns.
    Grid.Rows[J]:=rowValues;
    Inc(J);
    Query.Next();
end;

rowValues.Free; // for the OCD among us

这使时间从2秒降至约50毫秒。

4
在这个操作期间,你还应该锁定字符串网格。Grid.Cols[0].BeginUpdate, 然后尝试(设置 RowCount 并填充行),最后执行 Grid.Cols[0].EndUpdate。这有点奇怪,但这个技巧可以锁定整个网格,而不仅仅是列。 - LU RD
2
说实话,我不理解速度差异。这意味着Grid.Cells访问非常缓慢,我对此表示怀疑...有人能解释一下吗? - jpfollenius
据我所知,这只是更新次数的问题。使用单元格需要对网格进行10倍的“更新”;每个单元格都需要一次更新。而使用Grid.Rows每行只需要一次“更新”。因此,如果您在网格中有10列,则更新次数会增加10倍。 - awmross
@LU RD 我尝试在我的原始代码中调用BeginUpdate和EndUpdate,但并没有明显的改善。不确定? - awmross
1
最好发布能够运行的代码,否则使用省略号(...)表示省略部分。 - Warren P
显示剩余2条评论

3

第一个优化是通过使用本地TQuery来替换非常慢的Query.FieldByName('Value1')调用。

var
  F1, F2, F3: TField;

Grid.RowCount := Query.RecordCount;
J := 0;
F1 := Query.FieldByName('Value1');
F2 := Query.FieldByName('Value2');
F3 := Query.FieldByName('Value3');
while not Query.EOF do
begin
    Grid.Cells[0,J]:=F1.AsString;
    Grid.Cells[1,J]:=F2.AsString;
    Grid.Cells[2,J]:=F3.AsString;
    // etc for other columns.
    Inc(J);
    Query.Next();
end;

如果这还不够,可以使用虚拟模式中的网格,即检索所有内容并存储在TStringList或任何内存结构中,然后使用OnGetText或OnDrawCell方法。

2
@ Arnaud,你确定F1等是TQuery类型吗?我认为它们应该是TField类型。 - iamjoosy
1
@iamjoosy 当然,你是对的,它们是TField类型。已修复。感谢您的报告。如果SO有内联解析器和Delphi编译器来检查我们的代码就好了。 ;) - Arnaud Bouchez

3
在循环中使用FieldByName非常缓慢,因为它每次都要重新计算。您应该在循环外计算它,然后在循环内仅使用结果。

3

TStringGrid对于少量记录的情况下可以正常工作,但是不要尝试超过10,000条记录。

我们在加载/排序/分组大型表格集时,使用基于Delphi TStringGrid的TMS的TAdvStringGrid遇到了严重的性能问题,而且当在网格顶部插入一行时(展开网格组节点)也会出现此类问题。另外,内存使用率很高。 是的,我已经使用了beginupdate/endupdate,还有其他技巧。但是在深入研究TStringGrid结构后,我得出结论,它永远无法快速处理许多记录。

作为一般提示(适用于大型网格):使用OnGetText(和OnSetText)事件。该事件用于按需填充网格(仅显示的单元格)。将数据存储在自己的数据对象中。这使我们的网格非常快速(1,000,000条记录不再是问题,可以在几秒钟内加载!)


2

尝试使用AQTime或类似工具(分析器)进行测试。


没有代码很难确定,但我认为性能差是由于FieldByName而不是StringGrid引起的。

FieldByName进行线性搜索:

  for I := 0 to FList.Count - 1 do
  begin
    Result := FList.Items[I];
  ...

如果您的数据集有很多列(字段),性能仍将较低。
问候。

FieldByName确实很有效。当在一个大循环中引用了数十个字段时,我们通过这种方式解决了几个性能问题。 - Chris Thornton

2
我本来想说“为什么不使用beginupdate/endupdate呢?”,但现在我发现常规的字符串网格不支持它。
在搜索时,我找到了一种模拟beginupdate/endupdate的方法:http://www.experts-exchange.com/Programming/Languages/Pascal/Delphi/Q_21832072.html
请参考ZhaawZ的答案,他使用一对WM_SETREDRAW消息来禁用/启用重绘。如果这样做可以起作用,请与“消除使用FieldbyName技巧”结合使用,应该不需要花费多少时间就能完成绘制。

2

我认为它变慢是因为每次添加一行都要重绘。既然你正在从查询中获取值,我认为最好使用TDBGrid。

祝好。


2
虽然使用 TDBGrid 是一种选择,但是应该可以将 500 行添加到字符串网格中而不会出现性能问题,所以我认为这并没有什么帮助。 - jpfollenius
+1 @Cesar - 最终我也是这样做了,但这仍然是一个有普遍兴趣的有趣问题。 - Mawg says reinstate Monica

2
如果您知道要添加多少行,请将当前行数存储在临时变量中,将网格的rowcount设置为当前行数加上要添加的行数,然后将新值分配给行(使用您存储的以前的行数)而不是添加它们。这将减少大量的后台处理。

我认为代码已经实现了这个功能(请参见上面添加的代码示例) - awmross
如果您在原始问题中提供了代码,我本来就会知道那个的。感谢您的澄清。 - Argalatyr

1
在循环之前设置 Grid.RowCount = 2,然后当循环完成时将 rowcount 设置为正确的值。这样可以避免对 OnPaint 事件的大量调用。

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