Enabling a form while another form is being shown modally

 Delphi  Comments Off on Enabling a form while another form is being shown modally
Apr 092016
 

There was one shortcoming in my Delphi IDE explorer that has irked me since the beginning: It was disabled while a modal dialog was shown in the IDE, so there was no way to inspect the controls on the current dialog. The option to follow the focus ameliorated this a bit because it was now possible to click on the various controls in the dialog and see their properties, but still: There was no way to inspect non-clickable controls and how they were related to other controls on the form or to switch the different pages showing the properties, events, hierarchy or parents of the current control.

Delphi-Ide-Explorer-with-modal-form

(Click on the picture to see an animation how it works.)

Today, I remembered that I had asked a question on StackOverflow a few days ago: Why do modal Delphi forms not receive WM_SYSCOMMAND when the user clicks the task bar button?

In his answer David Heffernan mentioned the Windows API function EnableWindow and that it is being used by the VCL to disable all windows of an application when one of them is shown modally (and re-enable them afterwards). He explicitly warned me to meddle with that “The consequences are somewhat dire.”.

But being who I am, could of course not resist trying it anyway.

When I asked that question it was about a program at work that was showing a modal progress form which I wanted to allow to minimize the application and restore it when the user clicks on the task bar button. It did not work because the WM_SYSCOMMAND message never reached the program while the modal form was active. I worked around this by re-enabling the main window when the application was minimized (so it would receive the WM_SYSCOMMAND message again), and disabling it again once it got restored. It worked, and so far I have not seen any problems with that approach (but that might still be in store for me).

Today, I used a similar approach to enable the IDE explorer window while a modal form is active in the IDE. My first try was a TTimer that every second called EnabledWindow(Self.Handle, True). While this worked, I thought it might be a bit excessive to call that API every second, so I looked for a better place to put this code. How can a plugin be notified when a modal dialog is being shown? It turned out to be quite easy: Hook Screen.OnActiveFormChange, something I have done before.

And lo and behold: It works and again I have not yet experienced any problems with that approach.

EDIT:
As Ondrej Kelle pointed out in his comment on my Google+ post:

Watching for WM_ENABLE with wParam = 0 in your explorer form, checking if Application.ModalLevel > 0 (meaning a ShowModal call is currently executing), and re-enabling yourself might work, too, and you could avoid the Screen.OnActiveFormChange event hook. (Just an idea.)

He even provided an implementation for this:

/// [...]
interface
/// [...]
type
  TForm1 = class(TForm)
    /// [...]
  private
   procedure WMEnable(var _Msg: TWMEnable); message WM_ENABLE;
   /// [...]
  end;
/// [...]
implementation
/// [...]
procedure TExplorerForm.WMEnable(var _Msg: TWMEnable);
begin
  inherited;
  if not _Msg.Enabled and (Application.ModalLevel > 0) then
    EnableWindow(Self.Handle, True);
end;

And yes, it works. One hook less is good. Thanks Ondrey!

 Posted by on 2016-04-09 at 20:27

Do not enumerate on TTreeNode.Item

 Delphi  Comments Off on Do not enumerate on TTreeNode.Item
Apr 092016
 

Note to self: Do not enumerate on TTreeNode.Item, it’s highly inefficient.

Consider this code:

procedure SelectFocusedControl(_ActCtrl: TWinControl; _Parent: TTreeNode);
var
  j: Integer;
  CtrlItem: TTreeNode;
begin
  for j := 0 to _Parent.Count - 1 do begin
    CtrlItem := _Parent.Item[j];
    if CtrlItem.Data = _ActCtrl then begin
      CtrlItem.Selected := true;
      break;
    end else
      SelectFocusedControl(_ActCtrl, CtrlItem);
  end;
end;

This code is from my Delphi IDE Explorer expert (a bit simplified for readability).
It does something very simple: It goes through all child nodes of the given Parent TTreeNode and looks for one whose Data property matches the given ActCrl parameter. This node then gets selected.

I always wondered why this takes so long until I had a look at the implementation of TTreeNode.GetItem today:

function TTreeNode.GetItem(Index: Integer): TTreeNode;
begin
  Result := GetFirstChild;
  while (Result <> nil) and (Index > 0) do
  begin
    Result := GetNextChild(Result);
    Dec(Index);
  end;
  if Result = nil then TreeViewError(Format(SListIndexError, [Index]));
end;

This is the current implementation in Delphi 10 Seattle.

As you can see, in order to get the Index’ed item, it enumerates through all child items counting them until reaching the given Index. And since my code calls it for each item, this is highly inefficient.

Now, consider this alternative code:

procedure SelectFocusedControl(_ActCtrl: TWinControl; _Parent: TTreeNode);
var
  CtrlItem: TTreeNode;
begin
  CtrlItem := _Parent.getFirstChild;
  while Assigned(CtrlItem) do begin
    if CtrlItem.Data = _ActCtrl then begin
      CtrlItem.Selected := true;
      break;
    end else
      SelectFocusedControl(_ActCtrl, CtrlItem);
    CtrlItem := CtrlItem.getNextSibling;
  end;
end;

This uses basically the same code as TTreeNode.GetItem, so it only loops through the items once. Much faster.

EDIT:
I deleted the rest of this post regarding TTreeNodesEnumerator. It was clearly wrong.

 Posted by on 2016-04-09 at 15:32