模态后面的网格滚动 Window

Grid Scroll Behind Modal Window

我已经构建了这个问题的 MCV,我很乐意上传它。我会先描述问题。

  1. 创建一个主 window 并通过任何可用的数据库放置一个连接到 table 的 TDBGrid。 window 的OnShow 连接到数据库并打开table。

  2. 在主按钮 window 上创建一个启动 non-modal window 的按钮。

  3. 在 non-modal window 上创建一个启动模式的按钮 window。

仔细按照这些步骤重现问题。

  1. 运行 应用程序。

  2. 将焦点放在网格上并使用鼠标滚轮上下滚动。

  3. 按下按钮启动 non-modal window。

  4. 当 non-modal window 打开时,单击回到主 window 中的网格并再次使用鼠标滚轮上下滚动。

  5. 在仍然聚焦在网格中的同时,单击 non-modal window 上的按钮启动模式 window.

  6. 当模式 window 打开时,将鼠标悬停在网格上并使用鼠标滚轮。您会看到网格上下滚动。

这不会发生在 Windows 7 上,但会发生在 Windows 10 上。这可能看起来无害,但当你有几层 parent child 在 3 windows 中建立的关系。

假设模态 window 包含主要 window 的 grand-children。如果用户启动模式 window 以编辑特定的 grand-children,并且不小心使用了他们的鼠标滚轮并将 grand-parent 移动到主 window 上,他们现在正在编辑grand-children 他们不打算这样做。

需要注意的是,在第 4 步和第 5 步之间,如果在启动模态 window 之前没有将焦点放在网格中,则不会出现此问题。在显示模式 window 之前,我尝试以编程方式将焦点设置到 non-modal window 上的控件中,但没有成功。

这是 VCL 代码中的一个错误。如评论中所述,要在普通应用程序中重现该问题,需要启用 Windows 10 的非活动 window 滚动功能,或在早期 OS.[= 中提供类似功能的软件。 16=]

不过没有特殊要求也可以演示问题。这将是比问题中更简单的复制。

在表单上放置一个 stringgrid 和一个按钮,该按钮在其点击处理程序中具有以下代码:

procedure TForm1.Button1Click(Sender: TObject);
begin
  StringGrid1.Enabled := False;
  SetFocusedControl(StringGrid1);
end;

单击按钮并将鼠标悬停在网格上并滚动。禁用后,网格不应滚动,但它会滚动。

以下是问题中案例的更合适的问题再现。那是因为模式 window 禁用了包含网格的表单,然后发布了鼠标滚轮消息。 wheel 消息是为没有上述要求的环境合成的。

procedure TForm1.Button1Click(Sender: TObject);
var
  Pt: TPoint;
begin
  Enabled := False;
  Pt := Point(1, 1);
  MapWindowPoints(StringGrid1.Handle, HWND_DESKTOP, Pt, 1);
  SetFocusedControl(StringGrid1);
  Perform(WM_MOUSEWHEEL, MakeWParam(0, WORD(-120)), MakeLParam(Pt.X, Pt.Y));
end;

观察到网格滚动后按 Ctrl+F2。


问题的根本原因在于,VCL 在判断控件是否是焦点控件以及使其执行鼠标滚轮消息时并不关心控件是否启用。将鼠标滚轮消息变为 CM_MOUSEWHEEL 并完全绕过默认的 window 过程,VCL 应该已经执行了这些检查。

作为解决方法,您可以在启动模式窗体之前将焦点控件设置为不同的控件:

MainForm.SetFocusedControl(MainForm);
OtherForm.ShowModal;

你不能在这里设置 ActiveControl,因为那里有必要的 visibility/state 检查。

如果不想和主窗体耦合,可以给主窗体放一个鼠标滚轮消息处理程序:

type
  TMainForm = class(TForm)
    ...
  protected
    procedure WMMouseWheel(var Message: TWMMouseWheel); message WM_MOUSEWHEEL;

...

procedure TMainForm.WMMouseWheel(var Message: TWMMouseWheel);
begin
  if IsWindowEnabled(Handle) then
    inherited;
end;