上周,我观察到了一些我没想到的事情,下面将进行描述。我很好奇为什么会这样。它是TDataSet类内部的东西,TDBGrid的工件还是其他东西?
打开的ClientDataSet中的字段顺序已更改。具体来说,我在使用FieldDefs定义了结构之后,通过调用CreateDatatSet在代码中创建了ClientDataSet。此ClientDataSet结构的第一个字段是一个名为StartOfWeek的Date字段。只是片刻之后,由于StartOfWeek字段不再是ClientDataSet中的第一个字段,因此我还编写了假定StartOfWeek字段位于第零个位置ClientDataSet.Fields [0]的代码失败了。
经过一番调查,我了解到ClientDataSet中的每个单个字段在给定时刻可能会出现在不同于创建ClientDataSet时原始结构的某个位置。我不知道会发生这种情况,在Google上进行的搜索也没有提及这种影响。
发生的事情不是魔术。这些字段不会自行更改位置,也不会基于我在代码中所做的任何更改。导致字段从物理上看起来改变ClientDataSet中位置的原因是用户更改了ClientDataSet附加到的DbGrid中的列的顺序(当然是通过DataSource组件)。我在Delphi 7,Delphi 2007和Delphi 2010中复制了这种效果。
我创建了一个非常简单的Delphi应用程序来演示这种效果。它由具有一个DBGrid,一个数据源,两个ClientDataSet和两个Button的单个表单组成。这种形式的OnCreate事件处理程序如下所示
procedure TForm1.FormCreate(Sender: TObject);
begin
with ClientDataSet1.FieldDefs do
begin
Clear;
Add('StartOfWeek', ftDate);
Add('Label', ftString, 30);
Add('Count', ftInteger);
Add('Active', ftBoolean);
end;
ClientDataSet1.CreateDataSet;
end;
标记为“显示ClientDataSet结构”的Button1包含以下OnClick事件处理程序。
procedure TForm1.Button1Click(Sender: TObject);
var
sl: TStringList;
i: Integer;
begin
sl := TStringList.Create;
try
sl.Add('The Structure of ' + ClientDataSet1.Name);
sl.Add('- - - - - - - - - - - - - - - - - ');
for i := 0 to ClientDataSet1.FieldCount - 1 do
sl.Add(ClientDataSet1.Fields[i].FieldName);
ShowMessage(sl.Text);
finally
sl.Free;
end;
end;
为了演示运动场效应,请运行此应用程序,然后单击标记为Show ClientDataSet Structure的按钮。您应该看到如下所示的内容:
The Structure of ClientDataSet1
- - - - - - - - - - - - - - - - -
StartOfWeek
Label
Count
Active
接下来,拖动DBGrid的列以重新排列字段的显示顺序。再次单击“显示ClientDataSet结构”按钮。这次您将看到类似于此处显示的内容:
The Structure of ClientDataSet1
- - - - - - - - - - - - - - - - -
Label
StartOfWeek
Active
Count
此示例的显着之处在于,正在移动DBGrid的列,但是对ClientDataSet中的字段的位置有明显的影响,使得ClientDataSet.Field [0]中的字段位于一个不一定要稍后。而且,不幸的是,这显然不是ClientDataSet问题。我对基于BDE的TTable和基于ADO的AdoTable执行了相同的测试,并获得了相同的效果。
如果您不需要引用显示在DBGrid中的ClientDataSet中的字段,则不必担心这种影响。对于其余的人,我可以想到几种解决方案。
避免此问题的最简单(虽然不是必需)的最佳方法是防止用户对DBGrid中的字段重新排序。这可以通过从DBGrid的Options属性中删除dgResizeColumn标志来完成。虽然这种方法有效,但从用户的角度来看,它消除了潜在的有价值的显示选项。此外,删除此标志不仅限制了列的重新排序,而且还阻止了列大小的调整。 (要了解如何在不删除列大小调整选项的情况下限制列的重新排序,请参阅http://delphi.about.com/od/adptips2005/a/bltip0105_2.htm。)
第二种解决方法是避免根据其字面位置引用数据集的字段(因为这是问题的本质)。换句话说,如果需要引用Count字段,请不要使用DataSet.Fields [2]。只要您知道该字段的名称,就可以使用DataSet.FieldByName('Count')之类的东西。
但是,使用FieldByName有一个相当大的缺点。具体来说,此方法通过遍历DataSet的Fields属性并基于字段名称查找匹配项来标识该字段。由于每次调用FieldByName都会执行此操作,因此在需要多次引用该字段的情况下(例如在导航大型DataSet的循环中),应避免使用此方法。
如果确实需要重复(且多次)引用该字段,请考虑使用类似以下代码片段的内容:
var
CountField: TIntegerField;
Sum: Integer;
begin
Sum := 0;
CountField := TIntegerField(ClientDataSet1.FieldByName('Count'));
ClientDataSet1.DisableControls; //assuming we're attached to a DBGrid
try
ClientDataSet1.First;
while not ClientDataSet1.EOF do
begin
Sum := Sum + CountField.AsInteger;
ClientDataSet1.Next;
end;
finally
ClientDataSet1.EnableControls;
end;
还有第三种解决方案,但是仅当您的数据集是ClientDataSet时才可用,就像我原始示例中的解决方案一样。在这种情况下,您可以创建原始ClientDataSet的副本,并且它将具有原始结构。结果,无论用户对显示ClientDataSets数据的DBGrid进行了什么操作,在零位位置创建的任何字段都仍将位于该位置。
下面的代码对此进行了演示,该代码与标记为“显示克隆的ClientDataSet结构”的按钮的OnClick事件处理程序相关联。
procedure TForm1.Button2Click(Sender: TObject);
var
sl: TStringList;
i: Integer;
CloneClientDataSet: TClientDataSet;
begin
CloneClientDataSet := TClientDataSet.Create(nil);
try
CloneClientDataSet.CloneCursor(ClientDataSet1, True);
sl := TStringList.Create;
try
sl.Add('The Structure of ' + CloneClientDataSet.Name);
sl.Add('- - - - - - - - - - - - - - - - - ');
for i := 0 to CloneClientDataSet.FieldCount - 1 do
sl.Add(CloneClientDataSet.Fields[i].FieldName);
ShowMessage(sl.Text);
finally
sl.Free;
end;
finally
CloneClientDataSet.Free;
end;
end;
如果运行此项目并单击标记为“显示克隆的ClientDataSet结构”的按钮,则始终将获得ClientDataSet的真实结构,如下所示
The Structure of ClientDataSet1
- - - - - - - - - - - - - - - - -
StartOfWeek
Label
Count
Active
附录:
重要的是要注意,基础数据的实际结构不受影响。具体来说,如果在更改DBGrid中的列顺序之后,调用ClientDataSet的SaveToFile方法,则保存的结构是原始(真正的内部)结构。另外,如果将一个ClientDataSet的Data属性复制到另一个ClientDataSet,则目标ClientDataSet也将显示真实结构(类似于克隆源ClientDataSet时观察到的效果)。
同样,对绑定到其他经过测试的数据集(包括TTable和AdoTable)的DBGrid列顺序的更改实际上不会影响基础表的结构。例如,一个TTable可以显示Delphi附带的customer.db示例Paradox表中的数据,但实际上并不会改变该表的结构(您也不希望这样做)。
从这些观察结果可以得出的结论是,DataSet本身的内部结构保持完整。结果,我必须假设某个地方存在DataSet结构的辅助表示。而且,它必须与数据集关联(这似乎是过大的做法,因为并非所有对数据集的使用都需要此关联),又必须与DBGrid关联(由于DBGrid使用了此功能,因此更有意义,但并非如此) TField重新排序似乎与DataSet本身保持一致的观察结果支持了这一点,或者是其他原因。
另一个选择是,效果与TGridDataLink相关联,TGridDataLink是使多行感知控件(如DBGrids)具有数据感知能力的类。但是,我也倾向于拒绝这种解释,因为此类与网格关联,而不是与DataSet关联,这再次是因为效果似乎与DataSet类本身有关。
这使我回到了最初的问题。这是TDataSet类内部的东西,TDBGrid的工件还是其他东西?
也请允许我在这里强调一些我添加到以下评论之一的内容。最重要的是,我的文章旨在使开发人员意识到,当他们使用可以更改列顺序的DBGrid时,其TField的顺序也可能会更改。该工件可能会引入间歇性的严重错误,这些错误很难识别和修复。而且,不,我不认为这是Delphi的错误。我怀疑一切都按设计的方式进行。只是我们许多人不知道这种行为正在发生。现在我们知道了。
最佳答案
显然,该行为是设计使然。实际上,它与dbgrid无关。这仅仅是设置字段索引的列的副作用。举例来说,
ClientDataSet1.Fields [0] .Index:= 1;
会导致“显示ClientDataSet结构”按钮的输出相应地更改(是否存在网格)。 TField.Index的文档指出了;
“通过更改索引的值来更改字段在数据集中的位置顺序。更改索引值会影响字段在数据网格中的显示顺序,但不会影响字段在物理数据库表中的位置。”
一个人应该得出结论,相反的说法也应该成立,并且更改网格中字段的顺序应该导致字段索引被更改。
导致此问题的代码在TColumn.SetIndex中。 TCustomDBGrid.ColumnMoved为移动的列设置新索引,而TColumn.SetIndex为该列的字段设置新索引。
procedure TColumn.SetIndex(Value: Integer);
[...]
if (Col <> nil) then
begin
Fld := Col.Field;
if Assigned(Fld) then
Field.Index := Fld.Index;
end;
[...]