在 DBGrid 中移动列似乎会移动附加的数据集字段
上周我观察到一些出乎我意料的事情,并将在下面描述。我很好奇为什么会发生这种情况。它是 TDataSet 类的内部内容、TDBGrid 的工件还是其他内容?
打开的 ClientDataSet 中字段的顺序发生了变化。具体来说,我在使用 FieldDefs 定义其结构后,通过调用 CreateDatatSet 在代码中创建了一个 ClientDataSet。此 ClientDataSet 结构中的第一个字段是名为 StartOfWeek 的日期字段。不久之后,我编写的代码(假设 StartOfWeek 字段位于第 0 个位置 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;
要演示移动场效应,请运行此应用程序并单击标有“显示 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 的 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;
还有第三种解决方案,但仅当您的 DataSet 是 ClientDataSet 时才可用,如我原来的例子。在这些情况下,您可以创建原始 ClientDataSet 的克隆,它将具有原始结构。因此,无论用户对显示 ClientDataSets 数据的 DBGrid 执行了什么操作,在第 0 个位置创建的字段仍将位于该位置。
下面的代码对此进行了演示,该代码与标记为“显示克隆的 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 实际上不会更改该表的结构(您也不希望它更改)。
从这些观察中我们可以得出的结论是,数据集本身的内部结构保持不变。因此,我必须假设在某处存在数据集结构的辅助表示。并且,它必须与 DataSet 关联(这似乎有点过分了,因为并非所有 DataSet 的使用都需要这个),或者与 DBGrid 关联(这更有意义,因为 DBGrid 正在使用此功能,但它不是观察结果支持了 TField 重新排序似乎与数据集本身一致),或者是其他东西。
另一种选择是,该效果与 TGridDataLink 相关联,该类为多行感知控件(如 DBGrid)提供数据感知能力。然而,我也倾向于拒绝这种解释,因为此类与网格相关联,而不是与 DataSet 相关联,同样,因为该效果似乎与 DataSet 类本身相关。
这让我回到了最初的问题。这种效果是 TDataSet 类的内部效果、TDBGrid 的产物还是其他效果?
请允许我在此强调我在以下评论之一中添加的内容。最重要的是,我的文章旨在让开发人员意识到,当他们使用可以更改列顺序的 DBGrid 时,其 TField 的顺序也可能会发生变化。此工件可能会引入间歇性的严重错误,这些错误很难识别和修复。而且,不,我不认为这是 Delphi 的错误。我怀疑一切都按照设计的方式进行。只是我们很多人都没有意识到这种行为正在发生。现在我们知道了。
I observed something last week that I did not expect, and will describe below. I am curious as to why this happens. Is it something internal to the TDataSet class, an artifact of the TDBGrid, or something else?
The order of the fields in an open ClientDataSet changed. Specifically, I created a ClientDataSet in code by calling CreateDatatSet after defining its structure using FieldDefs. The first field in this ClientDataSet's structure was a Date field named StartOfWeek. Only moments later, code that I had also written, which assumed that the StartOfWeek field was in the zeroeth position, ClientDataSet.Fields[0], failed, since the StartOfWeek field was no longer the first field in the ClientDataSet.
After some investigation, I learned that it was possible that every single field in the ClientDataSet might, at a given moment, appear in some position different from the original structure at the time that the ClientDataSet was created. I was unaware that this could happen, and a search on Google didn't turn up any mention of this effect either.
What happened wasn't magic. The fields didn't change position by themselves, nor did they change based on anything I did in my code. What caused the fields to physically appear to change position in the ClientDataSet was that the user had changed the order of the Columns in a DbGrid to which the ClientDataSet was attached (through a DataSource component, of course). I replicated this effect in Delphi 7, Delphi 2007, and Delphi 2010.
I created a very simple Delphi application that demonstrates this effect. It consists of a single form with one DBGrid, a DataSource, two ClientDataSets, and two Buttons. The OnCreate event handler of this form looks like the following
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, which is labeled Show ClientDataSet Structure, contains the following OnClick event handler.
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;
To demonstrate the moving field effect, run this application and click the button labeled Show ClientDataSet Structure. You should see something like that shown here:
The Structure of ClientDataSet1
- - - - - - - - - - - - - - - - -
StartOfWeek
Label
Count
Active
Next, drag the columns of the DBGrid to re-arrange the display order of the fields. Click the Show ClientDataSet Structure button once again. This time you will see something similar to that shown here:
The Structure of ClientDataSet1
- - - - - - - - - - - - - - - - -
Label
StartOfWeek
Active
Count
What is remarkable about this example is that the Columns of the DBGrid are being moved, but there is an apparent effect on the position of the Fields in the ClientDataSet, such that the field that was in the ClientDataSet.Field[0] position at one point is not necessarily there moments later. And, unfortunately, this is not distinctly a ClientDataSet issue. I performed the same test with BDE-based TTables and ADO-based AdoTables and got the same effect.
If you never need to refer to the fields in your ClientDataSet being displayed in a DBGrid, then you don't have to worry about this effect. For the rest of you, I can think of several solutions.
The simplest, though not necessary the preferable way to avoid this problem is to prevent the user from reordering fields in a DBGrid. This can be done by removing the dgResizeColumn flag from the Options property of the DBGrid. While this approach is effective, it eliminates a potentially valuable display option, from the user's perspective. Furthermore, removing this flag not only restricts column reordering, it prevents column resizing. (To learn how to limit column reordering without removing the column resizing option, see http://delphi.about.com/od/adptips2005/a/bltip0105_2.htm.)
The second workaround is to avoid referring to a DataSet's fields based on their literal position (since this is the essence of the problem). In order words, if you need to refer to the Count field, don't use DataSet.Fields[2]. So long as you know the name of the field, you can use something like DataSet.FieldByName('Count').
There is one rather big drawback to the use of FieldByName, however. Specifically, this method identifies the field by iterating through the Fields property of the DataSet, looking for a match based on the field name. Since it does this every time you call FieldByName, this is a method that should be avoided in situations where the field needs to be referenced many times, such as in a loop that navigates a large DataSet.
If you do need to refer to the field repeatedly (and a large number of times), consider using something like the following code snippet:
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;
There is a third solution, but this is only available when your DataSet is a ClientDataSet, like the one in my original example. In those situations, you can create a clone of the original ClientDataSet, and it will have the original structure. As a result, whichever field was create in the zeroeth position will still be in that position, regardless of what a user has done to a DBGrid that displays the ClientDataSets data.
This is demonstrated in the following code, which is associated with the OnClick event handler of the button labeled Show Cloned ClientDataSet Structure.
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;
If you run this project and click the button labeled Show Cloned ClientDataSet Structure, you will always get the true structure of the ClientDataSet, as shown here
The Structure of ClientDataSet1
- - - - - - - - - - - - - - - - -
StartOfWeek
Label
Count
Active
Addendum:
It is important to note that that the actual structure of the underlying data is not affected. Specifically, if, after changing the order of the columns in a DBGrid, you call the SaveToFile method of the ClientDataSet, the saved structure is the original (true internal) structure. Also, if you copy the Data property of one ClientDataSet to another, the destination ClientDataSet also shows the true structure (which is similar to the effect observed when a source ClientDataSet is cloned).
Similarly, changes to the column orders of DBGrids bound to other tested Datasets, including TTable and AdoTable, do not actually affect the structure of the underlying tables. For example, a TTable that displays data from the customer.db sample Paradox table that ships with Delphi does not actually change that table's structure (nor would you expect it to).
What we can conclude from these observations is that the internal structure of the DataSet itself remains intact. As a result, I must assume that there is a secondary representation of the DataSet's structure somewhere. And, it must be either associated with the DataSet (which would seem to be overkill, since not all uses of a DataSet need this), associated with the DBGrid (which makes more sense since the DBGrid is using this feature, but which is not supported by the observation that the TField reordering seems to persist with the DataSet itself), or is something else.
Another alternative is that the effect is associated with the TGridDataLink, which is the class that gives multirow-aware controls (like DBGrids) their data awareness. However, I am inclined to reject this explanation as well, since this class is associated with the grid, and not the DataSet, again since the effect seems persist with the DataSet classes themselves.
Which brings me back to the original question. Is this effect something internal to the TDataSet class, an artifact of the TDBGrid, or something else?
Permit me also to stress something here that I added to one of the below comments. More than anything, my post is designed to make developers aware that when they are using DBGrids whose column orders can be changed that the order of their TFields may also be changing. This artifact can introduce intermittent and serious bugs which can be very difficult to identify and fix. And, no, I don't think this is a Delphi bug. I suspect that everything is working as it was designed to work. It's just that many of us were unaware that this behavior was occurring. Now we know.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
显然这种行为是设计使然的。事实上它与dbgrid无关。它只是列设置字段索引的副作用。例如这个语句,
ClientDataSet1.Fields[0].Index := 1;
将导致“显示 ClientDataSet 结构”按钮的输出相应改变,无论是否有网格。 TField.Index 的文档指出;
“通过更改索引值来更改字段在数据集中的位置顺序。更改索引值会影响字段在数据网格中显示的顺序,但不会影响字段在物理数据库表中的位置。”
人们应该得出相反的结论也应该成立,并且更改网格中字段的顺序应该会导致字段索引发生更改。
导致此问题的代码位于 TColumn.SetIndex 中。 TCustomDBGrid.ColumnMoved 为移动的列设置新索引,TColumn.SetIndex 为该列的字段设置新索引。
Apparently the behaviour is by design. In fact it is not related to the dbgrid. It is merely a side effect of a column setting a field index. For instance this statement,
ClientDataSet1.Fields[0].Index := 1;
will cause the output of the "Show ClientDataSet Structure" button to change accordingly, either there is a grid or not. The documentation for TField.Index states;
"Change the order of a field's position in the dataset by changing the value of Index. Changing the Index value affects the order in which fields are displayed in data grids, but not the position of the fields in physical database tables."
One should conclude the reverse should also be true and changing the order of fields in a grid should cause field indexes to be changed.
The code causing this is in TColumn.SetIndex. TCustomDBGrid.ColumnMoved sets a new index for the moved column and TColumn.SetIndex sets the new index for that column's field.
Wodzu 发布了一个针对 ADO DataSet 特定的重新排序字段问题的解决方案,但他引导我找到了一个类似的解决方案,并且可用于所有 DataSet(是否在所有 DataSet 中正确实现是另一个问题)问题)。请注意,这个答案和 Wodzu 的答案实际上都不是原始问题的答案。相反,它是对所指出问题的解决方案,而问题与此工件的起源有关。
Wodzu 的解决方案引导我找到的解决方案是 FieldByNumber,它是 Fields 属性的方法。 FieldByNumber 的使用有两个有趣的方面。首先,您必须使用数据集的 Fields 属性限定其引用。其次,与采用从零开始的索引器的 Fields 数组不同,FieldByNumber 是一种采用从一开始的参数来指示要引用的 TField 位置的方法。
以下是我在原始问题中发布的 Button1 事件处理程序的更新版本。此版本使用 FieldByNumber。
对于示例项目,无论关联的 DBGrid 中的列的方向如何,此代码都会生成以下输出:
重复一下,请注意对基础 TField 的引用需要使用对 Fields 的引用来限定 FieldByNumber。此外,此方法的参数必须在 1 到 DataSet.FieldCount 范围内。因此,要引用 DataSet 中的第一个字段,请使用以下代码:
与 Fields 数组类似,FieldByNumber 返回 TField 引用。因此,如果您想要引用特定于特定 TField 类的方法,则必须将返回值转换为适当的类。例如,要将 TBlobField 的内容保存到文件中,您可能必须执行类似以下代码的操作:
请注意,我并不是建议您应该使用整数文字引用 DataSet 中的 TField。就个人而言,使用通过一次性调用 FieldByName 进行初始化的 TField 变量更具可读性,并且不受表结构物理顺序变化的影响(尽管不能不受字段名称变化的影响!)。
但是,如果您有与其列可以重新排序的 DBGrid 关联的 DataSet,并且使用整数文字作为 Fields 数组的索引器来引用这些 DataSet 的字段,则可能需要考虑将代码转换为使用 DataSet.Fields.FieldByName 方法。
Wodzu posted a solution to the reordered Field problem that was specific to ADO DataSet, but he led me to a solution that is similar, and available for all DataSets (whether it is implemented properly in all DataSets is another issue). Note that neither this answer, nor Wodzu's, is actually an answer to the original question. Instead, it is a solution to the problem noted, whereas the question relates to where this artifact originates.
The solution that Wodzu's solution lead me to was FieldByNumber, and it is a method of the Fields property. There are two interesting aspects to the use of FieldByNumber. First, you must qualify its reference with the Fields property of your DataSet. Second, unlike the Fields array, which takes a zero-based indexer, FieldByNumber is a method that takes a one-based parameter to indicate the position of the TField you want to reference.
The following is an updated version of the Button1 event handler that I posted in my original question. This version uses FieldByNumber.
For the sample project, this code produces the following output, regardless of the orientation of the Columns in the associated DBGrid:
To repeat, notice that the reference to the underlying TField required FieldByNumber to be qualified with a reference to Fields. Furthermore, the parameter for this method must lie within the 1 to DataSet.FieldCount range. As a result, to refer to the first field in the DataSet, you use the following code:
Like the Fields array, FieldByNumber returns a TField reference. As a result, if you want to refer to a method that is specific to a particular TField class, you have to cast the returned value to the appropriate class. For example, to save the contents of a TBlobField to a file, you may have to do something like the following code:
Note that I am not suggesting that you should reference TFields in a DataSet using integer literals. Personally, the use of a TField variable that gets initialized through a one time call to FieldByName is more readable, and is immune to changes in the physical order of a table's structure (though not immune to changes in the names of your fields!).
However, if you have DataSets associated with DBGrids whose Columns can be reordered, and you reference the fields of these DataSets using integer literals as indexers of the Fields array, you may want to consider converting your code to use the DataSet.Fields.FieldByName method.
卡里 我想我已经找到了解决这个问题的方法。我们需要使用 Recordset COM 对象的内部 Fields 属性,而不是使用 VCL 包装器 Fields。
以下是它的引用方式:
这些字段不受您之前描述的行为的影响。所以我们仍然可以通过索引来引用字段。
测试一下并告诉我结果是什么。这对我有用。
编辑:
当然,它仅适用于 ADO 组件,不适用于 TClientDataSet...
Edit2:
Cary 我不知道这是否是您问题的答案,但是我一直在 embarcadero 论坛上推动人们,Wayne Niddery 给了我关于所有菲尔兹运动的非常详细的答案。
长话短说:如果您在 TDBGrid 中显式定义列,则字段索引不会移动!现在更有意义了,不是吗?
在这里阅读全文:
https://forums.embarcadero.com/post!reply.jspa?messageID= 197287
Cary I think I've found a solution for this problem. Instead of using VCL wrapper Fields we need to use an internal Fields property of the Recordset COM object.
Here is how it should be referenced:
Those fields are NOT affected by the behaviour you have described earlier. So we can still refer to the fields by their index.
Test this out and tell me what was the result. It worked for me.
Edit:
Of course it will work only for ADO components, not for the TClientDataSet...
Edit2:
Cary I do not know if this is answer for your question, however I've been pushing folks on the embarcadero forums and Wayne Niddery gave me quite detailed answer about all this Fields movement.
To make a long story short: If you define your columns in TDBGrid explicitly, field indexes are not moving! Have a bit more sense now, hasn't it?
Read full thread here:
https://forums.embarcadero.com/post!reply.jspa?messageID=197287