TDbCtrlGrid动态加载缩略图时OnPaintPanel调用Edit/Post致索引失步
TDbCtrlGrid OnPaintPanel中写库导致无限重绘问题
问题表现
需要在TDbCtrlGrid中实现缩略图懒加载:预加载全量缩略图耗时过长,因此计划在OnPaintPanel事件中为当前显示的面板生成对应缩略图并存入数据库,后续展示同一条记录时直接读取已存储的缩略图,避免重复生成。
实际在OnPaintPanel事件中执行数据集Edit/Post操作时,会出现RecNo与PanelIndex失去同步的问题:控件滚动条无限循环重定位,注释掉Edit/Post相关代码后控件行为恢复正常。
根本原因
OnPaintPanel属于TDbCtrlGrid内部绘制流程的触发事件,事件执行时控件正处于逐面板移动数据集游标、同步当前绘制面板索引与数据集RecNo对应关系的阶段。此时主动调用Edit/Post会触发数据集的状态变更、索引重算、数据感知控件刷新通知,直接打断控件原有的绘制同步流程,造成面板索引与数据集记录号错位;错位后控件会自动触发重绘尝试修正状态,重绘过程中又会触发Edit/Post逻辑再次造成错位,最终形成无限重绘循环。
如果数据集设置了活动索引(如复现代码中按Caption字段建立的索引),Post操作引发的记录排序位置变动会进一步放大游标错位的问题。
可行解决方案
- 延迟执行写库逻辑,禁止在绘制事件上下文中直接操作数据集编辑
OnPaintPanel中仅做读取判断:如果当前记录的缩略图字段为空,记录下该条记录的唯一标识(主键/RecNo),通过PostMessage向窗体发送自定义消息,等当前完整绘制流程结束、控件退出绘制状态后,再在自定义消息的处理函数中定位到对应记录,生成缩略图、执行Edit/Post操作,写入完成后触发控件对应区域重绘即可。需要增加重入判断标记,避免同一条记录重复触发缩略图生成流程。 - 增加独立缓存层,异步生成写入缩略图
分离缩略图生成、存储与绘制逻辑:OnPaintPanel触发时优先读取内存缓存中的缩略图,无缓存时将生成任务丢给后台线程异步处理,生成完成后先写入内存缓存、异步持久化到数据库,再通知主线程重绘对应面板。该方案完全不会阻塞UI绘制流程,也不会打断TDbCtrlGrid的游标同步逻辑,大数量下性能表现最优。 - 临时屏蔽数据通知(仅用于快速验证,不推荐生产环境使用)
执行Edit/Post前临时将关联DataSource的Enabled属性设为False,切断数据集向TDbCtrlGrid发送的状态变更通知,完成Post操作后再将Enabled设回True,强制控件重新同步全量记录位置。该方式属于硬中断通知链,数据量大、索引逻辑复杂时容易出现记录跳位、面板内容错位问题。
复现代码
Unit1.pas
unit Unit1; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf, Data.DB, Vcl.DBCGrids, FireDAC.Comp.DataSet, FireDAC.Comp.Client, Vcl.StdCtrls; type TForm1 = class(TForm) FDMemTable1: TFDMemTable; DBCtrlGrid1: TDBCtrlGrid; DataSource1: TDataSource; Label1: TLabel; Label2: TLabel; procedure FormCreate(Sender: TObject); procedure DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid; Index: Integer); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid; Index: Integer); begin Label1.Caption := FDMemTable1.FieldByName('caption').AsString; // 取消注释以下三行会触发无限重绘 // FDMemTable1.Edit; // FDMemTable1.FieldByName('thumbnail').AsString := FDMemTable1.FieldByName('caption').AsString; // FDMemTable1.Post; Label2.Caption := FDMemTable1.FieldByName('thumbnail').AsString; end; procedure TForm1.FormCreate(Sender: TObject); var i: Integer; begin with FDMemTable1 do begin LogChanges := False; CreateDataset; Open; end; with FDMemTable1 do try AddIndex('Caption', 'caption', '', []); finally IndexName := 'Caption'; end; for i := 1 to 100 do begin FDMemTable1.Insert; FDMemTable1.FieldByName('caption').AsString := i.ToString; FDMemTable1.Post; end; FDMemTable1.First; end; end.
Unit1.dfm
object Form1: TForm1 Left = 0 Top = 0 Caption = 'Form1' ClientHeight = 939 ClientWidth = 1245 Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -12 Font.Name = 'Segoe UI' Font.Style = [] Position = poDesigned OnCreate = FormCreate TextHeight = 15 object DBCtrlGrid1: TDBCtrlGrid Left = 245 Top = 0 Width = 1000 Height = 939 Align = alRight ColCount = 5 DataSource = DataSource1 PanelHeight = 187 PanelWidth = 193 TabOrder = 0 RowCount = 5 OnPaintPanel = DBCtrlGrid1PaintPanel ExplicitLeft = 474 ExplicitHeight = 1429 object Label1: TLabel Left = 0 Top = 172 Width = 193 Height = 15 Align = alBottom Alignment = taCenter Caption = 'Label1' ExplicitTop = 270 ExplicitWidth = 34 end object Label2: TLabel Left = 0 Top = 0 Width = 193 Height = 172 Align = alClient Alignment = taCenter Caption = 'Label2' Layout = tlCenter ExplicitLeft = 64 ExplicitTop = 64 ExplicitWidth = 34 ExplicitHeight = 15 end end object FDMemTable1: TFDMemTable FieldDefs = < item Name = 'Caption' DataType = ftString Size = 20 end item Name = 'Thumbnail' DataType = ftString Size = 20 end> IndexDefs = <> FetchOptions.AssignedValues = [evMode] FetchOptions.Mode = fmAll ResourceOptions.AssignedValues = [rvSilentMode] ResourceOptions.SilentMode = True UpdateOptions.AssignedValues = [uvCheckRequired, uvAutoCommitUpdates] UpdateOptions.CheckRequired = False UpdateOptions.AutoCommitUpdates = True StoreDefs = True Left = 112 Top = 280 end object DataSource1: TDataSource DataSet = FDMemTable1 Left = 168 Top = 368 end end
内容的提问来源于stack exchange,提问作者Ronald Ruijs
相关产品推荐
相关产品推荐

