2009-12-31 65 views
12

我上周观察了一些我没有预料到的情况,下面将对此进行描述。我很好奇为什么会发生这种情况。 TDataSet类是TDBGrid的一个工件,还是其他东西?移动DBGrid中的列似乎移动了附加的数据集字段

打开的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的单一窗体,一个数据源,两个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; 

要演示运动场效应,请运行此应用程序并单击标记为Show ClientDataSet Structure的按钮。您应该看到如下所示的内容:

The Structure of ClientDataSet1 
- - - - - - - - - - - - - - - - - 
StartOfWeek 
Label 
Count 
Active 

接下来,拖动DBGrid的列以重新排列字段的显示顺序。再次单击Show ClientDataSet Structure按钮。这时候你会看到类似的东西如下所示:

The Structure of ClientDataSet1 
- - - - - - - - - - - - - - - - - 
Label 
StartOfWeek 
Active 
Count 

什么是显着的这个例子是一个DBGrid列被移动,但在字段中的位置的明显效果ClientDataSet,使得位于一个点上的ClientDataSet.Field [0]位置的字段不一定会在那个时刻出现。不幸的是,这并不是一个明显的ClientDataSet问题。我使用基于BDE的TTables和基于ADO的AdoTables执行了相同的测试,并获得了相同的效果。

如果您永远不需要引用您的ClientDataSet中显示在DBGrid中的字段,那么您不必担心这种影响。对于其他人,我可以想到几种解决方案。

最简单但并非必要的避免此问题的最佳方法是防止用户重新排序DBGrid中的字段。这可以通过从DBGrid的Options属性中删除dgResizeColumn标志来完成。尽管这种方法很有效,但从用户的角度来看,它消除了潜在的宝贵显示选项。此外,删除此标志不仅会限制列重新排序,还会阻止列大小调整。 (要了解如何限制列重新排序而不删除列大小调整选项,请参阅http://delphi.about.com/od/adptips2005/a/bltip0105_2.htm。)

第二种解决方法是避免基于字面位置引用DataSet的字段(因为这是问题的实质)。换句话说,如果您需要引用Count字段,请不要使用DataSet.Fields [2]。只要您知道该字段的名称,就可以使用类似DataSet.FieldByName('Count')的内容。

但是,使用FieldByName有一个相当大的缺点。具体而言,此方法通过迭代DataSet的Fields属性来标识字段,并根据字段名称查找匹配项。由于每次调用FieldByName时都会执行此操作,因此在需要多次引用该字段的情况下(如在导航大型数据集的循环中)应该避免使用此方法。

如果需要引用多次现场(和大量的时间),可以考虑使用类似下面的代码片段:

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被克隆时观察到的效果类似)。

同样,绑定到其他测试数据集(包括TTable和AdoTable)的DBGrid的列顺序更改实际上并不影响基础表的结构。例如,一个显示Delphi附带的customer.db示例Paradox表的数据的TTable实际上并未改变该表的结构(您也不会期望它)。

从这些意见中我们可以得出结论,DataSet本身的内部结构保持不变。因此,我必须假定DataSet的结构在某处存在二次表示。而且,它必须与DataSet相关联(这似乎是过度的,因为并不是所有使用DataSet都需要这样的),与DBGrid相关联(由于DBGrid使用了此功能,所以更合理由TField重新排序似乎与DataSet本身一直存在的观察支持),或者是其他内容。

另一种替代方法是该效果与TGridDataLink相关联,该TGridDataLink是为多路感知控件(如DBGrids)提供其数据感知的类。但是,我倾向于拒绝这个解释,因为这个类与网格相关联,而不是DataSet,因为效果似乎与DataSet类本身一样持久。

这使我回到原来的问题。这是TDataSet类的内部效应,TDBGrid的人为因素还是其他?

允许我也强调一下这里我添加到下面的评论之一。更重要的是,我的帖子旨在让开发人员意识到,当他们使用的DBGrid的列顺序可以改变时,他们的TFields的顺序也可能发生变化。这个工件可能会引入间歇性和严重的错误,这些错误很难识别和修复。不,我不认为这是一个Delphi错误。我怀疑所有事情都是按照设计的方式工作的。只是我们很多人不知道这种行为正在发生。现在我们知道了。

+3

非常丰富,但是在这里有一个问题吗? – 2009-12-31 06:21:23

+0

感谢@Cary,我对此毫不知情,而且我经常使用DataSet.Field [x]构造。我想你应该在Embarcadero网站上报告它是一个错误。 – Wodzu 2009-12-31 09:33:26

+0

有一个问题出现在第二句话中:“TDataSet类是内部的东西,TDBGrid的人工产物还是其他东西?“我花了一段时间(一个小时左右)搜索TCustomGrid和TDataSet源代码,但没有看到发生了什么。 更重要的是,这就是为什么我的帖子太长了,我至少想Delphi开发人员意识到这种有趣的行为,对于任何使用DBGrid或其他类似的网格,在TFields命令中产生这些更改的人来说,它可能是间歇性的,并且很难发现错误的来源 – 2009-12-31 16:24:58

回答

3

显然,行为是通过设计。实际上它与dbgrid无关。这只是设置字段索引的列的副作用。例如,这个声明,

ClientDataSet1.Fields [0] .Index:= 1;

将导致“显示客户端数据结构”按钮的输出相应地改变,无论是否有网格。 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; 
[...] 
1

Cary我想我已经找到了解决这个问题的方法。我们需要使用Recordset COM对象的内部Fields属性,而不是使用VCL wrapper Fields。

这里是应该被引用:

qry.Recordset.Fields.Item[0].Value 

这些领域不会受到你刚才描述的行为。所以我们仍然可以通过它们的索引来引用这些字段。

测试一下,告诉我结果是什么。它为我工作。

编辑:

当然它会工作只为ADO组件,而不是将TClientDataSet ...

EDIT2:

卡里,我不知道这是不是答案你的问题,不过我一直在embarcadero论坛上推荐人,Wayne Niddery给了我关于这场Fields运动的详细解答。

长话短说:如果您明确定义TDBGrid中的列,则字段索引不会移动!现在有更多的意义,不是吗?

阅读完整的线程在这里: https://forums.embarcadero.com/post!reply.jspa?messageID=197287

+0

您的解决方案是一个很好的解决方案,因为您仍然可以通过其在底层数据集结构中已知的物理位置可靠地引用特定的TField。正如你所指出的,这个限制只适用于ADO DataSet。 但是,您在第二次搜索TFields类的相应成员时发送了我。该类有一个FieldByNumber方法,该方法相对于关联网格中的列的顺序是不变的。 FieldByNumber就像你的Item属性。 我已经添加了一个解答FieldByNumber更详细的问题的答案,比这里提供的更详细。 – 2009-12-31 17:17:54

+0

在行之间读取,我们可以得出结论,DBGrid是此效果的来源,它以某种方式与DataSet的TFields进行交互。我将根据这个结论接受你的答案。但我认为我们仍然可以深入挖掘。我想知道更多关于负责的确切机制,因为它可能是其他已知副作用的来源。谢谢,Dimitrij – 2010-01-01 00:32:02

1

Wodzu发布了解决方案,这是特定于ADO DataSet中的排序场问题,但他带领我到一个解决方案是相似的,并适用于所有数据集(无论在正确执行 DataSets是另一个问题)。请注意,这个答案和Wodzu的答案都不是对原始问题的回答。相反,它是解决问题的一个解决方案,而问题涉及这个工件的起源。

Wodzu的解决方案引导我使用的解决方案是FieldByNumber,它是Fields属性的一种方法。有两个有趣的方面使用FieldByNumber。首先,您必须使用DataSet的Fields属性限定其引用。其次,与采用基于零的索引器的Fields数组不同,FieldByNumber是一种方法,它采用基于一个参数来指示要引用的TField的位置。

以下是我在原始问题中发布的Button1事件处理程序的更新版本。该版本使用FieldByNumber。

procedure TForm1.Button1Click(Sender: TObject); 
var 
    sl: TStringList; 
    i: Integer; 
begin 
    sl := TStringList.Create; 
    try 
    sl.Add('The Structure of ' + ClientDataSet1.Name + 
     ' using FieldByNumber'); 
    sl.Add('- - - - - - - - - - - - - - - - - '); 
    for i := 0 to ClientDataSet1.FieldCount - 1 do 
     sl.Add(ClientDataSet1.Fields.FieldByNumber(i + 1).FieldName); 
    ShowMessage(sl.Text); 
    finally 
    sl.Free; 
    end; 
end; 

对于实例项目,代码产生以下输出,而不管相关联的DBGrid列的取向:

The Structure of ClientDataSet1 using FieldByNumber 
- - - - - - - - - - - - - - - - - 
StartOfWeek 
Label 
Count 
Active 

要重复,请注意,参考到底层TField所需FieldByNumber通过对字段的引用进行限定。此外,此方法的参数必须位于1到DataSet.FieldCount范围内。其结果是,指在第一字段中的数据集,则使用下面的代码:

ClientDataSet1.Fields.FieldByNumber(1) 

字段阵列一样,FieldByNumber返回TField参考。因此,如果要引用特定于特定TField类的方法,则必须将返回的值转换为适当的类。例如,为了节省TBlobField到一个文件中的内容,您可能需要做类似下面的代码:

TBlobField(MyDataSet.Fields.FieldByNumber(6)).SaveToFile('c:\mypic.jpg'); 

注意,我并不是说你应该使用整型常量数据集引用中tfields。就个人而言,通过一次性调用FieldByName来初始化的TField变量的使用更具可读性,并且不受表格结构物理顺序变化的影响(尽管不会影响字段名称的变化!)。但是,如果DataSet与其列可以重新排序的DBGrid关联,并且您使用整数文字作为Fields数组的索引器引用这些DataSet的字段,则可能需要考虑将您的代码转换为使用DataSet。 Fields.FieldByName方法。

+2

这在Stack Overflow上赢得了“最长的Delphi问题”。 :-) – 2009-12-31 18:18:18

+0

@Cary,请看看我的最新答案。对于这个“问题”还有另一种解决方案。 – Wodzu 2009-12-31 21:06:11