Grid Scroll Behind Modal Window

2019-09-13 10:12发布

问题:

I have built a MCV of this problem, and I'm happy to upload it. I'll try to describe the problem first.

  1. Create a main window and place a TDBGrid connected to a table through any database available. OnShow of the window connect to the database and open the table.

  2. Create a button on the main window that launches a non-modal window.

  3. On the non-modal window create a button that launches a modal window.

Follow these steps carefully to replicate the problem.

  1. Run the application.

  2. Put focus into the grid and use your mouse wheel to scroll up and down.

  3. Press the button to launch the non-modal window.

  4. While the non-modal window is open, click back into the grid in the main window and again use your mouse wheel to scroll up and down.

  5. While still focused in the grid, click the button on the non-modal window to launch the modal window.

  6. While the modal window is open, hover over the grid and use your mouse wheel. You'll see that the grid scrolls up and down.

This does not happen on Windows 7, but does on Windows 10. It may seem innocuous, but it's particularly dangerous when you have a few layers of parent child relationships built across the 3 windows.

Let's say the modal window contains grand-children of the main window. If the user launches the modal window with the intent of editing specific grand-children, and accidentally uses their mouse wheel and moves the grand-parent on the main window, they are now editing grand-children that they didn't intend to.

It should be noted that between step 4 and 5, if you do not put focus in the grid before launching the modal window, this problem does not occur. I have tried setting focus programmatically into a control on the non-modal window before showing the modal window with no success.

回答1:

This is an error in VCL code. To duplicate the problem in a normal application, as noted in the comments, one need to have Windows 10's inactive window scrolling feature enabled, or software providing similar functionality in earlier OS.

However it is possible to demonstrate the problem without special requirements. This would be a simpler reproduction than in the question.

Drop a stringgrid and a button on a form, the button having the following code in its click handler:

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

Click the button and hover your mouse over the grid and scroll. Disabled as it is, the grid shouldn't scroll, but it does.

Below is a more appropriate problem reproduction to the case in the question. That's because the modal window disables the form containing the grid which is then posted the mouse wheel message. The wheel message is synthesized for environments that doesn't have the mentioned requirements.

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;

Press Ctrl+F2 after you observe that the grid scrolls.


The root cause of the problem is, VCL does not care if a control is enabled or not while deciding if it is the focused control, and while making it perform a mouse wheel message. Mutating the mouse wheel message to a CM_MOUSEWHEEL and completely bypassing the default window procedure, VCL should have been performing these checks.

For a workaround, you can set the focused control to a different control before launching the modal form:

MainForm.SetFocusedControl(MainForm);
OtherForm.ShowModal;

You can't set ActiveControl here, because that one has the necessary visibility/state check in place.

If you don't want to be coupled with the main form, you can put a mouse wheel message handler to the main form:

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;