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个TLabelTControlList,然后 将 TStringGrid 的高度调整为大约 10 行,将 TControlList 的高度调整为 大约 5。不需要手动设置 LiveBindings,因为它是全部 在下面的代码中完成。

然后,如下设置主窗体的代码,编译并运行。

在 运行 时,ClientDataSet 创建了一百个编号的记录,其中有两个 字段并将它们显示在 StringList 和 ControlList 中。

我看到的行为 是 ControlList 的行为符合预期,除非它的当前行是它的最后一行

  • 如果是,则 ControlList 滚动,以便单击的行向上移动一行。 我能找到将其恢复为最后一行的唯一简单方法是单击 ControlList 的垂直滚动条的上拇指,如 代码的注释可能是一个(未实现,我怀疑,脆弱的)的基础 解决方法。

注意DoSomething方法,点击ControList时执行, 有一个 $define 确定 ClientDataSet 是否执行 Edit; PostNext; Prior。这对操作中的哪对似乎没有任何区别 被执行,行为是一样的。这就是促使我看一看的原因 ClientDataSet 滚动时会发生什么。

如果您在 Caption := 'ScrolledAfterScroll 处理程序,运行 项目,然后在单击之前启用 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 添加到您的用途中。