打开的ClientDataSet中的字段顺序发生变化.具体来说,我通过使用FieldDefs定义其结构后调用CreateDatatSet在代码中创建了一个ClientDataSet.此ClientDataSet结构中的第一个字段是名为StartOfWeek的Date字段.只有片刻之后,我还写过的代码,其中假设StartOfWeek字段处于零位置,ClientDataSet.Fields [0]失败,因为StartOfWeek字段不再是ClientDataSet中的第一个字段.
经过一番调查,我了解到,ClientDataSet中的每一个字段都可能在给定的时刻出现在创建ClientDataSet时与原始结构不同的位置.我不知道这可能会发生,谷歌搜索也没有提到这个效果.
发生了什么不是魔术这些字段本身并没有改变位置,也没有根据我在代码中所做的任何改变.导致这些字段在ClientDataSet中实际出现更改位置的原因是,用户已经更改了ClientDataSet所附加的DbGrid中的列(当然是通过DataSource组件)的顺序.我在Delphi 7,Delphi 2007和Delphi 2010中复制了这个效果.
我创建了一个非常简单的Delphi应用程序,演示了这种效果.它由单个表单与一个DBGrid,DataSource,两个ClientDataSet和两个按钮组成.此表单的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;
标记为Show ClientDataSet Structure的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的列以重新排列字段的显示顺序.再次单击显示客户端数据集结构按钮.这次你会看到类似的东西:
The Structure of ClientDataSet1 - - - - - - - - - - - - - - - - - Label StartOfWeek Active Count
这个例子令人惊奇的是DBGrid的列正被移动,但是对ClientDataSet中的Fields的位置有明显的影响,使得ClientDataSet.Field [0]中的字段位于一个点不一定在那个瞬间.而且,不幸的是,这并不是一个ClientDataSet问题.我使用基于BDE的TTables和基于ADO的AdoTables进行了同样的测试,并得到了相同的效果.
如果您不需要引用显示在DBGrid中的ClientDataSet中的字段,则不必担心此效果.对于其余的人,我可以想到几个解决方案.
最简单的,虽然不是必需的,但避免此问题的最佳方法是防止用户重新排序DBGrid中的字段.这可以通过从DBGrid的Options属性中删除dgResizeColumn标志来完成.虽然这种方法是有效的,但从用户的角度来看,它消除了潜在的有价值的显示选项.此外,删除此标志不仅限制列重新排序,还可以防止列大小调整. (要了解如何限制列重新排序而不删除列调整大小选项,请参阅http://delphi.about.com/od/adptips2005/a/bltip0105_2.htm.)
第二个解决方法是避免基于它们的字面位置引用DataSet的字段(因为这是问题的本质).按顺序,如果您需要参考“计数”字段,请不要使用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做了什么,无论在零位置创建哪个字段,仍将处于该位置.
这在以下代码中演示,该代码与标记为显示克隆的客户端数据结构的按钮的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方法,则保存的结构是原始(true internal)结构.此外,如果将一个ClientDataSet的Data属性复制到另一个ClientDataSet,则目标ClientDataSet还将显示真实的结构(与克隆源ClientDataSet时观察到的结果相似).
类似地,绑定到其他测试数据集(包括TTable和AdoTable)的DBGrids的列顺序的更改实际上不会影响底层表的结构.例如,显示来自client.db示例Paradox表中的数据的TTable不会实际更改该表的结构(也不期望它).
我们可以从这些观察结果得出结论,DataSet本身的内部结构保持不变.因此,我必须假设DataSet的结构存在二次表示.而且,它必须与DataSet相关联(这似乎是过度的,因为DataSet的所有使用都不需要这个),与DBGrid相关联(这使得DBGrid使用此功能更有意义,但不是支持的观察,TField重新排序似乎与DataSet本身一直存在),或者是别的.
另一个替代方案是,效果与TGridDataLink相关联,TGridDataLink是提供多重感知控件(如DBGrids)的数据感知的类.然而,我也倾向于拒绝这个解释,因为这个类与网格相关联,而不是DataSet,因为效果似乎与DataSet类本身一样存在.
这让我回到原来的问题. TDataSet类是TDBGrid的工件还是其他东西?
允许我也在这里强调,我添加了以下评论之一.更重要的是,我的帖子旨在让开发人员意识到,当他们使用DBGrids,其列命令可以被更改时,它们的TField的顺序也可能正在改变.这个工件可以引入间歇性和严重的错误,这可能非常难以识别和修复.而且,不,我不认为这是Delphi的错误.我怀疑一切工作都是为了工作而设计的.只是我们中许多人不知道这种行为正在发生.现在我们知道
解决方法
ClientDataSet1.Fields [0] .Index:= 1;
将导致“Show ClientDataSet Structure”按钮的输出相应地改变,或者是否有一个网格. 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; [...]