TControlList 在使用 LiveBindings 时更改记录时滚动列表
TControlList scrolls list when a record is changed when using LiveBindings
重现:使用 LiveBindings 设置一个 TControlList
到数据集,并分配一个 OnClick
事件:
DataSet.Edit;
DataSet.Post;
现在,当您单击一个单元格时,它会将该单元格滚动到列表的最后一行(假设您已经向下滚动到足够远)。
这可能是一个错误,但有什么方法可以防止重新定位列表中的可见项?
使用 Delphi 10.4.2
在彻底调查之后,我得出的结论是
您注意到的行为是 TControlList
的 LiveBinding 机制的结果
“按设计工作”,因此不太可能有一种干净的方法来避免它(尽管有一个明显的解决方法,见下文)。
请尝试下面的测试项目,方法是创建一个新的 VCL 项目,添加一个
TClientDataSet
,一个TStringGrid
和一个包含2个TLabel
的TControlList
,然后
将 TStringGrid
的高度调整为大约 10 行,将 TControlList
的高度调整为
大约 5。不需要手动设置 LiveBindings
,因为它是全部
在下面的代码中完成。
然后,如下设置主窗体的代码,编译并运行。
在 运行 时,ClientDataSet 创建了一百个编号的记录,其中有两个
字段并将它们显示在 StringList 和 ControlList 中。
我看到的行为
是 ControlList 的行为符合预期,除非它的当前行是它的最后一行
- 如果是,则 ControlList 滚动,以便单击的行向上移动一行。
我能找到将其恢复为最后一行的唯一简单方法是单击
ControlList 的垂直滚动条的上拇指,如
代码的注释可能是一个(未实现,我怀疑,脆弱的)的基础
解决方法。
注意DoSomething
方法,点击ControList时执行,
有一个 $define 确定 ClientDataSet 是否执行 Edit; Post
或
Next; Prior
。这对操作中的哪对似乎没有任何区别
被执行,行为是一样的。这就是促使我看一看的原因
ClientDataSet 滚动时会发生什么。
如果您在 Caption := 'Scrolled
行
AfterScroll
处理程序,运行 项目,然后在单击之前启用 bp
ControlList,你会看到当它跳闸时,滚动事件已经被触发
通过 MakeValidRecNo
in Data.Bind.Scope
最终由
TLinkObservers.PositionLinkPosChanged
在 System.Classes 中。正是这个事实使得
我认为不太可能有任何干净的方法来避免问题行为
未能说服 EMBA 改变 TControlList 的行为。
顺便说一句,@fpiette 的评论是,即使在 OnClick
句柄中执行 Edit
也足以引发该行为,其原因是它最终会导致 ClientDataSet 滚动,触发上述行为。
代码:
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ControlList, Data.DB,
Datasnap.DBClient, Vcl.Grids, Data.Bind.Components, Data.Bind.DBScope, Data.Bind.Grid,
Data.Bind.EngExt, Vcl.Bind.DBEngExt, Vcl.Bind.Grid, System.Rtti,
System.Bindings.Outputs, Vcl.Bind.Editors, Vcl.Bind.ControlList, Vcl.ExtCtrls,
Vcl.DBCtrls;
type
TForm1 = class(TForm)
ControlList1: TControlList;
Button1: TButton;
StringGrid1: TStringGrid;
ClientDataSet1: TClientDataSet;
BindingsList1: TBindingsList;
Label1: TLabel;
Label2: TLabel;
DBNavigator1: TDBNavigator;
DataSource1: TDataSource;
Button2: TButton;
procedure FormCreate(Sender: TObject);
procedure ControlList1Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure ClientDataSet1AfterScroll(DataSet: TDataSet);
private
procedure DoSomething;
public
ItemIndex : Integer;
ARect : TRect;
BindSourceDB1 : TBindSourceDB;
LinkGridToDataSourceBindSourceDB1 : TLinkGridToDataSource;
LinkPropertyToFieldCaption1: TLinkPropertyToField;
LinkGridToDataSourceBindSourceDB2: TLinkGridToDataSource;
LinkPropertyToFieldCaption2: TLinkPropertyToField;
end;
[...]
procedure TForm1.DoSomething;
var
Pt : TPoint;
Rows,
Row : Integer;
begin
//Exit;
Pt := Mouse.CursorPos;
Pt := ControlList1.ScreenToClient(Pt);
Rows := ControlList1.ClientHeight div ControlList1.ItemHeight;
if Pt.Y > Rows * ControlList1.ClientHeight then
Row := Rows + 1
else
Row := Pt.Y div ControlList1.ItemHeight;
Inc(Row); // to make it 1-based
Caption := Format('Row: %d', [Row]);
{.$Define DoEdit}
{$IfDef DoEdit}
ClientDataSet1.Edit;
ClientDataSet1.Post;
{$Else}
ClientDataSet1.Next;
ClientDataSet1.Prior;
{$Endif}
if Row >= Rows then begin
// The observed behaviour is that if the clicked row was the last one in the grid
// the ClientDataSet operations above will have moved the current record's data
// will now be in the row above the bottom row. A work-around would be to simulate
// clicking the upper thumb of ControlList1's vertical scrollbar, as this shifts the
// current ror back down to where it was, in the last row of the grid.
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
DoSomething;
end;
procedure TForm1.ClientDataSet1AfterScroll(DataSet: TDataSet);
begin
Caption := 'Scrolled';
end;
procedure TForm1.ControlList1Click(Sender: TObject);
begin
DoSomething;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
AField : TField;
i : Integer;
begin
AField := TIntegerField.Create(Self);
AField.FieldName := 'Field1';
AField.FieldKind := fkData;
AField.DataSet := ClientDataSet1;
AField := TStringField.Create(Self);
AField.FieldName := 'Field2';
AField.Size := 20;
AField.FieldKind := fkData;
AField.DataSet := ClientDataSet1;
ControlList1.ClientHeight := ControlList1.ItemHeight * 5;
BindSourceDB1 := TBindSourceDB.Create(Self);
BindSourceDB1.DataSet := ClientDataSet1;
LinkGridToDataSourceBindSourceDB1 := TLinkGridToDataSource.Create(Self);
LinkGridToDataSourceBindSourceDB1.DataSource := BindSourceDB1;
LinkGridToDataSourceBindSourceDB1.GridControl := StringGrid1;
LinkGridToDataSourceBindSourceDB2 := TLinkGridToDataSource.Create(Self);
LinkGridToDataSourceBindSourceDB2.DataSource := BindSourceDB1;
LinkGridToDataSourceBindSourceDB2.GridControl := ControlList1;
LinkPropertyToFieldCaption1 := TLinkPropertyToField.Create(Self);
LinkPropertyToFieldCaption1.DataSource := BindSourceDB1;
LinkPropertyToFieldCaption1.FieldName := 'Field1';
LinkPropertyToFieldCaption1.Component := Label1;
LinkPropertyToFieldCaption1.ComponentProperty := 'Caption';
LinkPropertyToFieldCaption2 := TLinkPropertyToField.Create(Self);
LinkPropertyToFieldCaption2.DataSource := BindSourceDB1;
LinkPropertyToFieldCaption2.FieldName := 'Field2';
LinkPropertyToFieldCaption2.Component := Label2;
LinkPropertyToFieldCaption2.ComponentProperty := 'Caption';
ClientDataSet1.IndexFieldNames := 'Field1';
ClientDataSet1.CreateDataSet;
for i := 1 to 100 do
ClientDataSet1.InsertRecord([i, 'Row ' + IntToStr(i)]);
ClientDataSet1.First;
end;
我找不到一个“好的”解决方案,所以我选择了 RTTI 路线。我们需要访问私有 FScrollPos
字段,在我们更改数据集中的记录之前记住它是什么,然后将其重置。
procedure TForm7.ControlList1Click(Sender: TObject);
var
OldScrollPos : integer;
begin
inherited;
OldScrollPos := TRttiContext.Create.GetType(TControlList).GetField('FScrollPos').GetValue(Sender).AsInteger;
DataSet.Edit;
DataSet.Post;
TRttiContext.Create.GetType(TControlList).GetField('FScrollPos').SetValue(Sender, OldScrollPos);
end;
这会在我们更改记录时删除“将项目滚动到底部行”的行为。您需要将 System.RTTI
添加到您的用途中。
重现:使用 LiveBindings 设置一个 TControlList
到数据集,并分配一个 OnClick
事件:
DataSet.Edit;
DataSet.Post;
现在,当您单击一个单元格时,它会将该单元格滚动到列表的最后一行(假设您已经向下滚动到足够远)。
这可能是一个错误,但有什么方法可以防止重新定位列表中的可见项?
使用 Delphi 10.4.2
在彻底调查之后,我得出的结论是
您注意到的行为是 TControlList
的 LiveBinding 机制的结果
“按设计工作”,因此不太可能有一种干净的方法来避免它(尽管有一个明显的解决方法,见下文)。
请尝试下面的测试项目,方法是创建一个新的 VCL 项目,添加一个
TClientDataSet
,一个TStringGrid
和一个包含2个TLabel
的TControlList
,然后
将 TStringGrid
的高度调整为大约 10 行,将 TControlList
的高度调整为
大约 5。不需要手动设置 LiveBindings
,因为它是全部
在下面的代码中完成。
然后,如下设置主窗体的代码,编译并运行。
在 运行 时,ClientDataSet 创建了一百个编号的记录,其中有两个 字段并将它们显示在 StringList 和 ControlList 中。
我看到的行为 是 ControlList 的行为符合预期,除非它的当前行是它的最后一行
- 如果是,则 ControlList 滚动,以便单击的行向上移动一行。 我能找到将其恢复为最后一行的唯一简单方法是单击 ControlList 的垂直滚动条的上拇指,如 代码的注释可能是一个(未实现,我怀疑,脆弱的)的基础 解决方法。
注意DoSomething
方法,点击ControList时执行,
有一个 $define 确定 ClientDataSet 是否执行 Edit; Post
或
Next; Prior
。这对操作中的哪对似乎没有任何区别
被执行,行为是一样的。这就是促使我看一看的原因
ClientDataSet 滚动时会发生什么。
如果您在 Caption := 'Scrolled
行
AfterScroll
处理程序,运行 项目,然后在单击之前启用 bp
ControlList,你会看到当它跳闸时,滚动事件已经被触发
通过 MakeValidRecNo
in Data.Bind.Scope
最终由
TLinkObservers.PositionLinkPosChanged
在 System.Classes 中。正是这个事实使得
我认为不太可能有任何干净的方法来避免问题行为
未能说服 EMBA 改变 TControlList 的行为。
顺便说一句,@fpiette 的评论是,即使在 OnClick
句柄中执行 Edit
也足以引发该行为,其原因是它最终会导致 ClientDataSet 滚动,触发上述行为。
代码:
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ControlList, Data.DB,
Datasnap.DBClient, Vcl.Grids, Data.Bind.Components, Data.Bind.DBScope, Data.Bind.Grid,
Data.Bind.EngExt, Vcl.Bind.DBEngExt, Vcl.Bind.Grid, System.Rtti,
System.Bindings.Outputs, Vcl.Bind.Editors, Vcl.Bind.ControlList, Vcl.ExtCtrls,
Vcl.DBCtrls;
type
TForm1 = class(TForm)
ControlList1: TControlList;
Button1: TButton;
StringGrid1: TStringGrid;
ClientDataSet1: TClientDataSet;
BindingsList1: TBindingsList;
Label1: TLabel;
Label2: TLabel;
DBNavigator1: TDBNavigator;
DataSource1: TDataSource;
Button2: TButton;
procedure FormCreate(Sender: TObject);
procedure ControlList1Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure ClientDataSet1AfterScroll(DataSet: TDataSet);
private
procedure DoSomething;
public
ItemIndex : Integer;
ARect : TRect;
BindSourceDB1 : TBindSourceDB;
LinkGridToDataSourceBindSourceDB1 : TLinkGridToDataSource;
LinkPropertyToFieldCaption1: TLinkPropertyToField;
LinkGridToDataSourceBindSourceDB2: TLinkGridToDataSource;
LinkPropertyToFieldCaption2: TLinkPropertyToField;
end;
[...]
procedure TForm1.DoSomething;
var
Pt : TPoint;
Rows,
Row : Integer;
begin
//Exit;
Pt := Mouse.CursorPos;
Pt := ControlList1.ScreenToClient(Pt);
Rows := ControlList1.ClientHeight div ControlList1.ItemHeight;
if Pt.Y > Rows * ControlList1.ClientHeight then
Row := Rows + 1
else
Row := Pt.Y div ControlList1.ItemHeight;
Inc(Row); // to make it 1-based
Caption := Format('Row: %d', [Row]);
{.$Define DoEdit}
{$IfDef DoEdit}
ClientDataSet1.Edit;
ClientDataSet1.Post;
{$Else}
ClientDataSet1.Next;
ClientDataSet1.Prior;
{$Endif}
if Row >= Rows then begin
// The observed behaviour is that if the clicked row was the last one in the grid
// the ClientDataSet operations above will have moved the current record's data
// will now be in the row above the bottom row. A work-around would be to simulate
// clicking the upper thumb of ControlList1's vertical scrollbar, as this shifts the
// current ror back down to where it was, in the last row of the grid.
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
DoSomething;
end;
procedure TForm1.ClientDataSet1AfterScroll(DataSet: TDataSet);
begin
Caption := 'Scrolled';
end;
procedure TForm1.ControlList1Click(Sender: TObject);
begin
DoSomething;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
AField : TField;
i : Integer;
begin
AField := TIntegerField.Create(Self);
AField.FieldName := 'Field1';
AField.FieldKind := fkData;
AField.DataSet := ClientDataSet1;
AField := TStringField.Create(Self);
AField.FieldName := 'Field2';
AField.Size := 20;
AField.FieldKind := fkData;
AField.DataSet := ClientDataSet1;
ControlList1.ClientHeight := ControlList1.ItemHeight * 5;
BindSourceDB1 := TBindSourceDB.Create(Self);
BindSourceDB1.DataSet := ClientDataSet1;
LinkGridToDataSourceBindSourceDB1 := TLinkGridToDataSource.Create(Self);
LinkGridToDataSourceBindSourceDB1.DataSource := BindSourceDB1;
LinkGridToDataSourceBindSourceDB1.GridControl := StringGrid1;
LinkGridToDataSourceBindSourceDB2 := TLinkGridToDataSource.Create(Self);
LinkGridToDataSourceBindSourceDB2.DataSource := BindSourceDB1;
LinkGridToDataSourceBindSourceDB2.GridControl := ControlList1;
LinkPropertyToFieldCaption1 := TLinkPropertyToField.Create(Self);
LinkPropertyToFieldCaption1.DataSource := BindSourceDB1;
LinkPropertyToFieldCaption1.FieldName := 'Field1';
LinkPropertyToFieldCaption1.Component := Label1;
LinkPropertyToFieldCaption1.ComponentProperty := 'Caption';
LinkPropertyToFieldCaption2 := TLinkPropertyToField.Create(Self);
LinkPropertyToFieldCaption2.DataSource := BindSourceDB1;
LinkPropertyToFieldCaption2.FieldName := 'Field2';
LinkPropertyToFieldCaption2.Component := Label2;
LinkPropertyToFieldCaption2.ComponentProperty := 'Caption';
ClientDataSet1.IndexFieldNames := 'Field1';
ClientDataSet1.CreateDataSet;
for i := 1 to 100 do
ClientDataSet1.InsertRecord([i, 'Row ' + IntToStr(i)]);
ClientDataSet1.First;
end;
我找不到一个“好的”解决方案,所以我选择了 RTTI 路线。我们需要访问私有 FScrollPos
字段,在我们更改数据集中的记录之前记住它是什么,然后将其重置。
procedure TForm7.ControlList1Click(Sender: TObject);
var
OldScrollPos : integer;
begin
inherited;
OldScrollPos := TRttiContext.Create.GetType(TControlList).GetField('FScrollPos').GetValue(Sender).AsInteger;
DataSet.Edit;
DataSet.Post;
TRttiContext.Create.GetType(TControlList).GetField('FScrollPos').SetValue(Sender, OldScrollPos);
end;
这会在我们更改记录时删除“将项目滚动到底部行”的行为。您需要将 System.RTTI
添加到您的用途中。