如果是选中了新行,一旦其他控件获得焦点,比如BUTTON1 2026/6/11 0:30:03--e.RowIndex=9,IsNewRow=True 2026/6/11 0:30:04--e.RowIndex=8,IsNewRow=False 系统会自动选中最后一行,(只是触发了事件)但表面上没看出来
已完成思考
这是 DataGridView 存在了 15 年的"幽灵焦点" 官方 bug**,而且是绑定数据源模式专属:当你点击新增行(带 *)后,再点击任何其他控件(按钮、文本框、菜单),DataGridView 会偷偷在后台把内部焦点从新增行跳到最后一行真实数据,触发RowEnter事件,但UI 界面完全不变(看起来还是选中新增行)。
为什么会这样?(微软的设计缺陷)
- 新增行是虚拟行,不是真正的 DataRow,不能持有持久焦点
- 当 DataGridView 失去焦点时,微软的代码会强制将焦点 "修正" 到一个真实存在的 DataRow
- 但这个修正只改了内部状态,没有更新 UI 高亮、没有滚动条,所以你完全看不出来
- 这就是为什么你会看到日志:
e.RowIndex=9→ 点击按钮 →e.RowIndex=8,但界面没变化
一行代码彻底修复(永久禁用自动跳转)
在窗体构造函数或 Load 事件里加这一行,从根源上禁止自动跳转:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load ' 【核心修复】禁止失去焦点时自动跳转到最后一行 DataGridView1.StandardTab = True End Sub✅ 这个属性是微软专门为这个 bug 留的后门,设置为True后:
- 点击新增行再点击其他控件,不会再自动跳转到最后一行
RowEnter事件不会再被错误触发- 所有焦点行为完全符合预期
- 不影响任何其他功能(包括新增、编辑、删除)
配合你的 RowEnter 事件,最终完整代码
' 全局变量存当前选中的ID(任何地方都能取) Private currentSelectedID As Integer = 0 Private isFormLoaded As Boolean = False Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load ' 【核心修复】禁止失去焦点时自动跳转到最后一行 DataGridView1.StandardTab = True ' 其他初始化代码... isFormLoaded = True End Sub Private Sub DataGridView1_RowEnter(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.RowEnter If Not isFormLoaded Then Exit Sub If e.RowIndex < 0 Then currentSelectedID = -1 Exit Sub End If ' 1. 底部带*的新增行 → 返回0 If e.RowIndex = DataGridView1.NewRowIndex Then currentSelectedID = 0 Debug.Print($"[{DateTime.Now:HH:mm:ss}] 选中新增行,ID=0") Exit Sub End If ' 2. 未保存的数据源新增行 → 返回0 Dim drv = TryCast(DataGridView1.Rows(e.RowIndex).DataBoundItem, DataRowView) If drv IsNot Nothing AndAlso drv.Row.RowState = DataRowState.Added Then currentSelectedID = 0 Debug.Print($"[{DateTime.Now:HH:mm:ss}] 选中未保存新增行,ID=0") Exit Sub End If ' 3. 正常数据行 → 读取ID Dim val As Object = DataGridView1.Rows(e.RowIndex).Cells("ID").Value currentSelectedID = If(IsDBNull(val) OrElse val Is Nothing, 0, CInt(val)) Debug.Print($"[{DateTime.Now:HH:mm:ss}] 选中行索引={e.RowIndex},ID={currentSelectedID}") End Sub如果 StandardTab 不生效(极少数情况)
用这个兜底方案,手动保存最后一次选中的行号,完全忽略自动跳转的事件:
Private lastValidRowIndex As Integer = -1 Private Sub DataGridView1_RowEnter(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.RowEnter If Not isFormLoaded Then Exit Sub If e.RowIndex < 0 Then Exit Sub ' 【兜底修复】忽略失去焦点时的自动跳转 If Not DataGridView1.Focused Then ' DataGridView没有焦点时,所有RowEnter事件都是假的,直接用上次保存的值 e.Cancel = True ' 取消这次无效的行切换 Exit Sub End If ' 保存有效的行号 lastValidRowIndex = e.RowIndex ' 后面的判断逻辑和上面完全一样... End Sub验证方法
- 点击新增行(索引 9),日志显示
ID=0 - 点击 Button1 按钮,不会再触发任何 RowEnter 事件
- 再点击 DataGridView,焦点还是在新增行,日志正常显示
ID=0 - 点击最后一行真实数据(索引 8),日志显示正确的 ID
补充说明
- 这个 bug 在.NET 2.0 到.NET 10 全版本存在,微软一直没修
StandardTab = True还有一个副作用:按 Tab 键会离开 DataGridView,而不是在单元格间循环,这其实是更符合用户习惯的行为- 如果你一定要保留 Tab 键在单元格间循环的行为,就用第二个兜底方案