一个打开的ClientDataSet中字段的顺序发生了改变。具体来说,我通过在FieldDefs中定义结构后调用CreateDatatSet在代码中创建了一个ClientDataSet。这个ClientDataSet结构中的第一个字段是一个名为StartOfWeek的日期字段。只有片刻之后,我编写的代码假定StartOfWeek字段在零位置(ClientDataSet.Fields[0]),但失败了,因为StartOfWeek字段不再是ClientDataSet中的第一个字段。
经过一些调查,我了解到,可能每个ClientDataSet中的字段在某个时刻都可能出现在与创建ClientDataSet时的原始结构不同的位置。我不知道这种情况可能发生,Google搜索也没有提到这种效果。
发生的事情并不神奇。字段不会自己改变位置,它们也不会根据我的代码进行任何更改。导致字段在ClientDataSet中物理上看起来改变位置的原因是用户更改了与ClientDataSet相关联的DbGrid中列的顺序(当然是通过DataSource组件)。我在Delphi 7、Delphi 2007和Delphi 2010中复制了这种效果。
我创建了一个非常简单的Delphi应用程序来演示这种效果。它由一个带有一个DBGrid、一个DataSource、两个ClientDataSets和两个按钮的单个表单组成。这个表单的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;
Button1标签为“显示ClientDataSet结构”,其包含以下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;
为了展示移动字段效果,请运行此应用程序并单击标记为“显示ClientDataSet结构”的按钮。您应该会看到类似于此处所示的内容:
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的TTables和基于ADO的AdoTables进行了相同的测试,并获得了相同的效果。
如果您从未需要引用在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;
有第三种解决方案,但只适用于DataSet是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的列顺序更改实际上也不会影响底层表的结构。例如,从Delphi中附带的customer.db示例Paradox表中显示数据的TTable实际上不会更改该表的结构(你也不应该期望它会更改)。
我们可以得出的结论是,数据集本身的内部结构保持完整。因此,我必须假设有一个第二个表示数据集结构的副本。并且,它必须要么与数据集关联(这似乎过于复杂,因为并非所有使用数据集的情况都需要这种结构),要么与DBGrid关联(这更合理,因为DBGrid正在使用此功能,但不支持观察到TField重新排序似乎仍然与数据集本身相关),或者是其他东西。
另一个选择是,该效果与TGridDataLink相关,它是为多行感知控件(如DBGrids)提供数据感知能力的类。但是,我倾向于拒绝这种解释,因为此类与网格而不是数据集相关联,同样因为该效果似乎仍然与数据集类本身相关。
这让我回到最初的问题。这个效果是TDataSet类内部的东西,还是TDBGrid的产物,或者是其他什么东西?
请允许我在这里强调一些事情,我已经在下面的评论中添加了。最重要的是,我的帖子旨在让开发人员意识到,当他们使用可以更改列顺序的DBGrid时,他们的TFields顺序也可能会改变。这种现象可能会引入间歇性和严重的错误,这些错误可能很难识别和修复。而且,不,我不认为这是Delphi的错误。我怀疑一切都按照设计工作。只是我们中的许多人不知道这种行为正在发生。现在我们知道了。