Access 连续窗体怎么显示行号?排序筛选后仍自动编号
摘要: Access 连续窗体没有现成的行号字段,直接使用 CurrentRecord 又容易让每一行显示相同数字。本文用 RecordsetClone 按窗体当前记录顺序计算序号,排序和筛选后仍能重新编号,而且不改数据表结构。access开发|access培训|access框架|请添加edonsoft。
Hi,大家好!
前几天做一个明细列表,客户提了一个很小的要求:最左边加一列序号,像 Excel 那样显示 1、2、3、4。
听起来只是放一个文本框,但 Access 连续窗体有个特点:主体区里的控件虽然会重复显示,每一行背后却不是一套独立控件。用错方法以后,很容易出现所有行都显示同一个数字,或者编辑一条记录时整列序号突然空白。
这个序号也不应该写进业务表。用户一排序、一筛选,原来保存的号码就失去意义。我要的是显示层行号:只反映当前窗体中记录的排列位置,不参与业务计算。
下面直接做。
一、为什么不建议把序号存进表里
假设产品表有 100 条记录,按 ID 排序时,第 10 条记录显示序号 10。用户改成按产品名称排序后,这条记录可能跑到第 63 行。
如果序号是表里的真实字段,就要重新更新整张表。再筛选一次,又要更新一次。多人同时使用时,不同用户的筛选条件还不一样,这个字段根本没有统一答案。
因此,这里要区分两个概念:
-
业务顺序号:例如订单明细行号、工序号,需要保存进表里。
-
界面显示序号:只表示当前列表第几行,不保存进表里。
本文处理的是第二种。
二、直接使用 CurrentRecord 有什么问题
不少人会先在文本框的“控件来源”里写:
=CurrentRecord
在单一窗体里,这个值代表当前记录的位置。但连续窗体主体区会重复绘制同一个控件,CurrentRecord 又是窗体当前记录级别的状态。结果往往是光标落在哪条记录上,所有重复行就一起显示那个号码。
还有一种做法是用 DCount 统计“小于当前 ID 的记录数再加 1”。这在 ID 升序、不筛选、不按其他字段排序时勉强能用。一旦用户按日期、名称或金额排序,序号就不再对应屏幕上的实际顺序。
所以行号必须从窗体当前记录集本身计算,而不是猜测表里的顺序。
三、实现思路
Access 窗体的 RecordsetClone 是当前窗体记录集的副本。窗体应用了排序和筛选以后,这个副本会保持相同的记录范围与排列顺序。
处理过程如下:
-
文本框把当前行的唯一主键传给公共函数。
-
函数取得窗体的
RecordsetClone。 -
在副本中找到这条记录。
-
读取它的
AbsolutePosition。 -
因为位置从 0 开始,所以加 1 后作为显示序号。
主键可以是自动编号,也可以是唯一文本编号、GUID 或其他唯一字段。关键是不能重复,也不能为 Null。
四、建立标准模块
在 Access 中按 Alt + F11 打开 VBA 编辑器,选择:
插入 → 模块
把模块保存为:
Module_FormRowNumber
然后粘贴下面的完整代码:
Option Compare Database
Option Explicit
Public Function FormRowNumber(ByVal frm As Access.Form, _
ByVal keyValue As Variant, _
ByVal keyField As String) As Variant
On Error GoTo CleanFail
Dim rs As DAO.Recordset
Dim criterion As String
If frm Is Nothing Then Exit Function
If IsNull(keyValue) Then Exit Function
Set rs = frm.RecordsetClone
If rs.BOF And rs.EOF Then GoTo CleanExit
criterion = BracketFieldName(keyField) & " = " & _
FindLiteral(rs.Fields(keyField), keyValue)
rs.FindFirst criterion
If rs.NoMatch Then
FormRowNumber = FindRowByValue(rs, keyField, keyValue)
Else
FormRowNumber = rs.AbsolutePosition + 1
End If
CleanExit:
On Error Resume Next
If Not rs Is Nothing Then rs.Close
Set rs = Nothing
Exit Function
CleanFail:
Err.Clear
If Not rs Is Nothing Then
FormRowNumber = FindRowByValue(rs, keyField, keyValue)
End If
Resume CleanExit
End Function
Private Function FindRowByValue(ByVal rs As DAO.Recordset, _
ByVal keyField As String, _
ByVal keyValue As Variant) As Variant
Dim rowNumber As Long
If rs.BOF And rs.EOF Then Exit Function
rs.MoveFirst
rowNumber = 1
Do While Not rs.EOF
If ValuesEqual(rs.Fields(keyField).Value, keyValue) Then
FindRowByValue = rowNumber
Exit Function
End If
rowNumber = rowNumber + 1
rs.MoveNext
Loop
End Function
Private Function FindLiteral(ByVal fld As DAO.Field, _
ByVal value As Variant) As String
Select Case fld.Type
Case dbText, dbMemo, dbGUID
FindLiteral = "'" & Replace(CStr(value), "'", "''") & "'"
Case dbDate
FindLiteral = "#" & _
Format$(CDate(value), "yyyy-mm-dd hh:nn:ss") & "#"
Case dbBoolean
FindLiteral = IIf(CBool(value), "True", "False")
Case Else
FindLiteral = Replace(CStr(value), ",", ".")
End Select
End Function
Private Function ValuesEqual(ByVal leftValue As Variant, _
ByVal rightValue As Variant) As Boolean
If IsNull(leftValue) Or IsNull(rightValue) Then
ValuesEqual = IsNull(leftValue) And IsNull(rightValue)
ElseIf IsDate(leftValue) Or IsDate(rightValue) Then
ValuesEqual = (CDbl(CDate(leftValue)) = CDbl(CDate(rightValue)))
Else
ValuesEqual = (CStr(leftValue) = CStr(rightValue))
End If
End Function
Private Function BracketFieldName(ByVal fieldName As String) As String
BracketFieldName = "[" & Replace(fieldName, "]", "]]") & "]"
End Function
代码使用 DAO.Recordset。一般的 Access 数据库已经引用 DAO;如果编译时提示“用户定义类型未定义”,在 VBA 编辑器中打开“工具 → 引用”,勾选当前版本的 Microsoft Office Access database engine Object Library。
五、在连续窗体中放置序号文本框
打开目标窗体的设计视图,在主体区最左边放一个文本框。
建议设置这些属性:
| 属性 | 设置值 |
|---|---|
| 名称 | txtRowNumber |
| 控件来源 | =FormRowNumber([Form],[ID],"ID") |
| 是否锁定 | 是 |
| Tab 键索引 | 否 |
| 文本对齐 | 居中或右对齐 |
| 边框样式 | 透明或实线 |
这里假设窗体记录源的唯一主键叫 ID。如果你的主键叫 产品ID,控件来源应改为:
=FormRowNumber([Form],[产品ID],"产品ID")
如果主键叫 OrderNo,则写成:
=FormRowNumber([Form],[OrderNo],"OrderNo")
两个位置必须同时修改:第二个参数传当前行的字段值,第三个参数传字段名称字符串。
这个文本框不绑定表字段,只负责显示计算结果。不要把“序号”字段加到业务表里,也不要在窗体事件里循环给每条记录赋值。


六、排序和筛选后的表现
做好以后,可以直接测试这几种情况:
-
按 ID 升序,序号从 1 开始排列。
-
单击其他字段表头排序,序号跟随新的显示顺序。
-
给窗体应用筛选,只对筛选后的可见记录从 1 编号。
-
编辑现有记录时,其他行的序号不会整列消失。
-
新增记录的主键尚未生成时,序号暂时为空;保存后按记录所在位置显示。
这里有个容易踩的坑:不要在函数开头使用 frm.NewRecord 判断是否为新增记录。
连续窗体中,frm.NewRecord 描述的是窗体当前记录状态,不是每个重复文本框所在行的独立状态。用户进入新增行后,这个值可能让整列计算控件一起返回空白。本文代码只判断传入的主键 keyValue 是否为 Null,因此只影响真正没有主键的那一行。
也不需要在 Form_Current、Form_AfterUpdate 里反复调用 Recalc。正常排序、筛选和记录显示由 Access 自己处理,额外事件代码反而容易造成不必要的重算。


七、为什么代码还要准备逐行查找
正常情况下,FindFirst 找到记录后,直接读取 AbsolutePosition + 1 就能得到行号。
但 Access 项目里的主键类型不一定相同:可能是数字、文本、日期、是/否或 GUID。代码中的 FindLiteral 会按 DAO 字段类型生成查找条件,文本里的单引号也会转义。
如果某些数据源或驱动不支持对应的查找表达式,代码还会退回 FindRowByValue,从第一条记录开始逐行比较。这条后备路径速度没有 FindFirst 快,但兼容性更好。
因此,普通业务列表用这个方案没问题。如果窗体一次加载几万条记录,每一行又反复计算序号,就要注意性能。我的习惯是先通过查询、日期范围或分页把记录数量控制下来。行号是界面辅助信息,不值得为了它扫描一个超大结果集。
八、使用边界
这个方法适合:
-
连续窗体中的产品、客户、库存、订单列表。
-
数据表窗体中的临时显示序号。
-
需要随排序和筛选变化的界面行号。
-
不希望修改表结构的老项目。
它不适合替代真实的业务行号。例如销售订单明细中的第 1 行、第 2 行,可能参与打印、对账、接口传输,这种编号应该存入明细表,并由业务规则维护。
另外,传入字段必须唯一。如果两条记录使用相同值,函数只能找到第一条匹配记录,两行就可能显示同一个序号。最稳妥的选择仍然是自动编号主键或 GUID 主键。
九、小结
Access 连续窗体的行号看起来简单,真正的难点是分清“当前记录位置”和“每一行的显示位置”。把序号存进表里,会被排序和筛选打乱;直接用 CurrentRecord,又会受到连续窗体重复控件机制影响。
用 RecordsetClone 查找当前行,再取 AbsolutePosition + 1,序号就能跟着窗体当前记录集变化。整个功能只需要一个标准模块和一个计算文本框,不改表、不自动创建控件,也不依赖窗体事件。
浙公网安备 33010602011771号