12

上周我观察到了一些我没有预料到的事情,将在下面描述。我很好奇为什么会发生这种情况。它是 TDataSet 类内部的东西、TDBGrid 的工件还是其他东西?

打开的 ClientDataSet 中的字段顺序发生了变化。具体来说,我在使用 FieldDefs 定义其结构后,通过调用 CreateDatatSet 在代码中创建了一个 ClientDataSet。此 ClientDataSet 结构中的第一个字段是名为 StartOfWeek 的日期字段。不久之后,我也编写的代码(假设 StartOfWeek 字段位于第零位 ClientDataSet.Fields[0])失败了,因为 StartOfWeek 字段不再是 ClientDataSet 中的第一个字段。

经过一番调查,我了解到 ClientDataSet 中的每个字段都可能在特定时刻出现在与创建 ClientDataSet 时的原始结构不同的某个位置。我不知道这可能会发生,在谷歌上搜索也没有提到这种效果。

发生的事情不是魔术。这些字段本身并没有改变位置,也没有根据我在代码中所做的任何事情而改变。导致字段在 ClientDataSet 中物理出现位置变化的原因是用户更改了 ClientDataSet 附加到的 DbGrid 中列的顺序(当然是通过 DataSource 组件)。我在 Delphi 7、Delphi 2007 和 Delphi 2010 中复制了这种效果。

我创建了一个非常简单的 Delphi 应用程序来演示这种效果。它由一个带有一个 DBGrid、一个 DataSource、两个 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;

Button1(标记为 Show ClientDataSet Structure)包含以下 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 中 Fields 的位置有明显影响,因此 ClientDataSet.Field[0] 中的字段位置为 1点不一定在片刻之后。而且,不幸的是,这显然不是 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 做了什么,在第零位置创建的任何字段仍将位于该位置。

这在以下代码中进行了演示,该代码与标记为 Show Cloned ClientDataSet Structure 的按钮的 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 时,他们的 TField 的顺序也可能会发生变化。该工件可能会引入难以识别和修复的间歇性和严重错误。而且,不,我不认为这是一个 Delphi 错误。我怀疑一切都按照设计的方式工作。只是我们中的许多人都不知道这种行为正在发生。现在我们知道了。

4

3 回答 3

3

显然,这种行为是设计使然。实际上它与 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;
[...]
于 2010-01-05T02:19:20.380 回答
1

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

以下是它的引用方式:

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

这些字段不受您之前描述的行为的影响。所以我们仍然可以通过索引来引用字段。

测试一下,告诉我结果如何。它对我有用。

编辑:

当然,它只适用于 ADO 组件,不适用于 TClientDataSet...

编辑2:

Cary 我不知道这是否是您问题的答案,但是我一直在 embarcadero 论坛上推动人们,Wayne Niddery 就所有菲尔兹运动给了我非常详细的答案。

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

在此处阅读完整主题: https ://forums.embarcadero.com/post!reply.jspa?messageID=197287

于 2009-12-31T09:52:16.330 回答
1

Wodzu 发布了针对特定于 ADO DataSet 的重新排序字段问题的解决方案,但他引导我找到了一个类似的解决方案,并且可用于所有 DataSet(是否在所有DataSet 中正确实现是另一个问题)。请注意,这个答案和 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 才能使用对 Fields 的引用进行限定。此外,此方法的参数必须在 1 到 DataSet.FieldCount 范围内。因此,要引用 DataSet 中的第一个字段,请使用以下代码:

ClientDataSet1.Fields.FieldByNumber(1)

与 Fields 数组一样,FieldByNumber 返回一个 TField 引用。因此,如果要引用特定于特定 TField 类的方法,则必须将返回值强制转换为适当的类。例如,要将 TBlobField 的内容保存到文件中,您可能必须执行类似以下代码的操作:

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

请注意,我并不是建议您应该使用整数文字来引用 DataSet 中的 TFields。就个人而言,使用通过一次性调用 FieldByName 来初始化的 TField 变量更具可读性,并且不受表结构物理顺序变化的影响(尽管不能不受字段名称变化的影响!)。

但是,如果您有与 DBGrids 关联的 DataSets 其 Columns 可以重新排序,并且您使用整数文字作为 Fields 数组的索引器引用这些 DataSets 的字段,您可能需要考虑将您的代码转换为使用 DataSet.Fields.FieldByName 方法.

于 2009-12-31T17:49:17.450 回答