You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FireDAC TFDQuery RecNo工作异常 首尾记录行号显示为0

问题描述

在Delphi开发中,通过TFDQuery的OnCalcFields事件调用数据集RecNo属性为DBGrid生成行号时,dataset.RecNo出现取值异常:

  • 首记录行号始终显示为0
  • 当记录总数超过40条时,最后一条记录行号也显示为0
  • 其余记录在TDBGrid中的行号显示均正常
  • 异常仅在通过DBGrid滚动条浏览时稳定复现,使用鼠标滚轮滚动浏览时,首尾记录的RecNo可正常显示正确值
复现步骤

1. 创建测试数据库

新建名为SQLiteTestDB.db的SQLite数据库,执行以下建库SQL:

BEGIN TRANSACTION;
DROP TABLE IF EXISTS "Table1";
CREATE TABLE IF NOT EXISTS "Table1" (
"ID"    INTEGER NOT NULL UNIQUE,
"EmployeeName"  varchar(255) NOT NULL,
PRIMARY KEY("ID" AUTOINCREMENT));
COMMIT;

2. 搭建测试VCL程序

创建仅包含单个窗体、窗体内放置TDBGrid控件的Delphi VCL工程,所有FireDAC组件(包括FDConnection、FDQuery)均使用默认属性值,不做额外属性修改,完整代码如下:

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.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def,
FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.VCLUI.Wait,
FireDAC.Stan.Param, FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt,
FireDAC.Stan.ExprFuncs, FireDAC.Phys.SQLiteWrapper.Stat,
FireDAC.Phys.SQLiteDef, Data.DB, FireDAC.Phys.SQLite, Vcl.Grids, Vcl.DBGrids,
FireDAC.Comp.DataSet, FireDAC.Comp.Client;

type
TForm1 = class(TForm)
  FDConnection1: TFDConnection;
  FDQuery1: TFDQuery;
  DataSource1: TDataSource;
  DBGrid1: TDBGrid;
  FDPhysSQLiteDriverLink1: TFDPhysSQLiteDriverLink;
  FDQuery1RecordNumberField: TIntegerField;
  FDQuery1ID: TIntegerField;
  FDQuery1EmployeeName: TStringField;
  procedure FDQuery1CalcFields(DataSet: TDataSet);
  procedure FormCreate(Sender: TObject);
private
  Procedure InserDummyData;
public
  { Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FDQuery1CalcFields(DataSet: TDataSet);
begin
FDQuery1RecordNumberField.AsInteger := FDQuery1.RecNo;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
FDConnection1.close;
// 数据库文件存放在应用程序同级目录
FDConnection1.Params.Database := ExtractFilePath(ParamStr(0)) + 'SQLiteTestDB.db';
FDConnection1.Params.DriverID :='SQLite';
FDConnection1.open;
InserDummyData;
FDQuery1.SQL.Text:='Select * From Table1';
FDQuery1.open;
end;

procedure TForm1.InserDummyData;
var i : integer;
begin
FDConnection1.ExecSQL('Delete from Table1');
for I := 1 to 10 do
begin
   FDConnection1.ExecSQL('Insert into Table1(EmployeeName) VALUES(''an employee Name'');');
end;
end;

end.
  • 初始插入10条测试数据时,仅首记录行号显示为0
  • 将InserDummyData过程中的测试数据插入循环上限从10修改为40时,首记录、末记录的RecNo取值均为0
问题成因

该异常是FireDAC默认的记录预取机制和OnCalcFields触发时机不匹配导致的:

  1. FireDAC连接SQLite时默认采用按需拉取模式,单次从数据库读取的记录块大小默认值为40条,不会一次性把所有查询结果加载到内存。RecNo属性只有在游标完全稳定停留在某条有效记录上时,才会返回从1开始的正确序号;如果游标正处于BOF(数据集开头之前)、EOF(数据集末尾之后)位置,或者正处在记录块预取的移动中间状态,RecNo会直接返回0。
  2. OnCalcFields事件并非只在游标稳定停留在记录上时触发:拖动DBGrid滚动条时,为了计算网格布局、预加载相邻区域的记录块,会在游标移动的中间状态就触发计算字段逻辑,这时候读取到的RecNo自然是0。
    • 首记录始终显示0:数据集打开后网格初始化阶段,第一次触发首记录的计算字段逻辑时,游标还没完全离开BOF位置,计算得到0之后没有触发该记录的字段重算,就会一直保留0值。
    • 记录数超过40条时末记录显示0:总记录数小于等于40时,打开数据集就会一次性拉取所有记录,滚动到末尾不需要额外读取数据,不会触发游标临时进入EOF的中间状态;超过40条后滚动到最后一块记录时,需要触发新的记录块拉取动作,拉取过程中游标会临时停在EOF位置,这时候触发计算读取到的RecNo就是0。
    • 鼠标滚轮滚动时显示正常:滚轮操作是逐行移动游标,每次移动都会等游标完全落到目标记录上之后,才会触发重绘和计算字段逻辑,不会出现中间状态触发计算的情况。
修复方案

不要直接在OnCalcFields中无判断读取RecNo,可选择以下两种稳定实现方式:

  • 方案1(最轻量):不需要添加计算字段,直接处理DBGrid的OnDrawColumnCell事件,在绘制序号列时直接读取当前数据集的RecNo显示即可,完全绕开计算字段的触发时机问题。
  • 方案2(保留计算字段逻辑):赋值前先过滤BOF/EOF无效状态,同时将数据集设置为全量拉取,避免中间态干扰:
// 在打开数据集前设置全量拉取模式
FDQuery1.FetchOptions.Mode := fmAll;

procedure TForm1.FDQuery1CalcFields(DataSet: TDataSet);
begin
  // 游标不在有效记录位置时直接退出,不执行赋值
  if DataSet.BOF or DataSet.EOF then Exit;
  FDQuery1RecordNumberField.AsInteger := DataSet.RecNo;
end;

内容的提问来源于stack exchange,提问作者HasDev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 15:45:34