0

The canonical method of displaying hints in a status bar is via the following code:

    Constructor TMyForm.Create;
    begin
     inherited create (nil);
     ...
     Application.OnHint:= MyHint;
     ...
    end;

    procedure TMyForm.MyHint (Sender: TObject);
    begin
     sb.simpletext:= Application.Hint;
    end;  

    procedure TMyForm.FormClose(Sender: TObject; var Action: TCloseAction);
    begin
     Application.OnHint:= nil;
     ...
    end;

The above works fine when a program consists of modal forms, but it is problematic when non-modal forms are used (not necessarily MDI). In these cases, a non-modal form is created and Application.OnHint is assigned to a procedure within the non-modal form; the status bar displays hints from the form. But should another non-modal form be created, Application.OnHint is now assigned to the same procedure within the second form. Moving the mouse over the control with a hint in the first, non-active, form causes that hint to be displayed in the status bar of the second form!

How can I cause each non-modal form to display hints that originate only from its own controls? One possibility is removing hints from controls when a form becomes inactive and restoring them when the form becomes active again, but that is very inelegant. The problem is with the Application.OnHint event.

AmigoJack
  • 5,234
  • 1
  • 15
  • 31
No'am Newman
  • 6,395
  • 5
  • 38
  • 50
  • All my applications have a status bar in each non-modal form (and there may be several non-modal forms visible at the same time) and, sometimes also in the odd modal form (and there may in principle be several levels of modality, though very uncommon). And all my status bars show the hint of the control or menu item below the cursor. This is trivial to accomplish using a `TApplicationEvents` dropped on every form with such a status bar (using the component's `OnHint` property). But if I understand you correctly, this is not good enough for you: You want form X to *only* show the hints from X? – Andreas Rejbrand Jan 08 '22 at 13:55
  • @AndreasRejbrand: if your status bars show the hint of the control below the cursor, then surely, this is form X showing the hints from X. And this is what I want. Maybe make this an answer? – No'am Newman Jan 08 '22 at 16:17
  • If no menus are involved you can use Application.OnShowHint to determine the control that generates the hint, through HintInfo.HintControl, and hence the form that hosts that control, e.g. using GetParentForm. More complicated when menus are involved, I browsed an early application and found out that I had used a modified "forms.pas" to get to the menu item generated hint activation. – Sertac Akyuz Jan 08 '22 at 18:47

2 Answers2

3

It turns out that the OP simply wants each form's status bar to display all hints from that form (not minding it also displaying hints from other forms as well).

So this is trivial. Just give all your forms a status bar and drop a TApplicationEvents component onto each form. Create a handler for each component's OnHint event:

procedure TForm6.ApplicationEvents1Hint(Sender: TObject);
begin
  StatusBar1.SimpleText := Application.Hint;
end;

And then everything will just work:

Screen recording.

Update

It seems that the OP does mind that. One solution, then, is to do like this:

procedure TForm6.ApplicationEvents1Hint(Sender: TObject);
begin
  if IsHintFor(Self) then
    StatusBar1.SimpleText := Application.Hint
  else
    StatusBar1.SimpleText := '';
end;

on all your forms. But only one time do you need to define the helper function

function IsHintFor(AForm: TCustomForm): Boolean;
begin
  Result := False;
  var LCtl := FindDragTarget(Mouse.CursorPos, True);
  if Assigned(LCtl) then
    Result := GetParentForm(LCtl) = AForm;
end;

This unfortunately does waste a few CPU cycles, since it calls FindDragTarget several times each time Application.Hint is changed, in a sense needlessly since the VCL already has called it once. But this shouldn't be detectable.

Screen recording

Update 2

To make this work also for menus (which may also be navigated using the keyboard, in which case the mouse cursor may be anywhere on the screen), I think the following additions will suffice:

Declare a global variable next to the IsHintFor helper function:

var
  GCurrentMenuWindow: HWND;

function IsHintFor(AForm: TCustomForm): Boolean;

and extend this function like so:

function IsHintFor(AForm: TCustomForm): Boolean;
begin
  if GCurrentMenuWindow <> 0 then
    Result := Assigned(AForm) and (GCurrentMenuWindow = AForm.Handle)
  else
  begin
    Result := False;
    var LCtl := FindDragTarget(Mouse.CursorPos, True);
    if Assigned(LCtl) then
      Result := GetParentForm(LCtl) = AForm;
  end;
end;

Then, to make menu bars work, add the following to each form class with a menu bar:

    procedure WMEnterMenuLoop(var Message: TWMEnterMenuLoop); message WM_ENTERMENULOOP;
    procedure WMExitMenuLoop(var Message: TWMExitMenuLoop); message WM_EXITMENULOOP;
  end;

implementation

procedure TForm6.WMEnterMenuLoop(var Message: TWMEnterMenuLoop);
begin
  inherited;
  GCurrentMenuWindow := Handle;
end;

procedure TForm6.WMExitMenuLoop(var Message: TWMExitMenuLoop);
begin
  inherited;
  GCurrentMenuWindow := 0;
end;

Finally, to make context menus work, add the following to the unit with the helper function:

type
  TPopupListEx = class(TPopupList)
  protected
    procedure WndProc(var Message: TMessage); override;
  end;

{ TPopupListEx }

procedure TPopupListEx.WndProc(var Message: TMessage);
begin
  inherited;
  case Message.Msg of
    WM_INITMENUPOPUP:
      for var LMenu in PopupList do
        if TObject(LMenu) is TPopupMenu then
          if TPopupMenu(LMenu).Handle = Message.WParam then
          begin
            var LComponent := TPopupMenu(LMenu).PopupComponent;
            if LComponent is TControl then
            begin
              var LForm := GetParentForm(TControl(LComponent));
              if Assigned(LForm) then
                GCurrentMenuWindow := LForm.Handle;
            end;
            Break;
          end;
    WM_EXITMENULOOP:
      GCurrentMenuWindow := 0;
  end;
end;

initialization
  FreeAndNil(PopupList);
  PopupList := TPopupListEx.Create;

end.

Result:

Screen recording

Disclaimer: Not fully tested.

Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384
  • That's what I call a fast shiny light emitting electromagnetic radiation frog GIF. – AmigoJack Jan 08 '22 at 16:39
  • It seems that I caused myself to be misunderstood: each form should display *only the hints arising from controls on that form*. Using TApplicationEvents is just a more modern method of achieving what I had before, hooking Application.OnHint. – No'am Newman Jan 08 '22 at 17:13
  • No'am: Yes, that was what I initially thought. – Andreas Rejbrand Jan 08 '22 at 17:21
  • @AndreasRejbrand: Perfect! – No'am Newman Jan 09 '22 at 16:24
  • If you use TApplicationEvents.OnShowHint you can use: GetParentForm(HintInfo.HintControl) – FredS Jan 09 '22 at 18:51
  • 1
    @FredS: Yes, but then you get all the downsides of that approach, like delays and visible tooltips. And no support for menu items at all. – Andreas Rejbrand Jan 09 '22 at 18:57
  • @Andreas - You don't have to have visible tooltips, OnShowHint has a "CanShow" parameter. But what I want to object to your comment mainly is about menu items. Mouse position is not a good indicator to test if a menu item hint belongs to a certain form because menus can be navigated by the keyboard and the mouse can be at a completely unrelated place. Menu item hints are generated through WM_MENUSELECT, because of this it does not depend on input method. – Sertac Akyuz Jan 09 '22 at 23:33
  • @SertacAkyuz: I am aware of the menu keyboard issue (and even tested that before posting my update), but was lazy enough not to make a note about it. I think a fairly small addition to `IsHintFor` will take care of that. – Andreas Rejbrand Jan 09 '22 at 23:38
0

I'm going to give a partial answer as my researches into this topic have yielded something that works on one form but not on another.

With regard to the form where my solution works, there is a TDBGrid along with some buttons; the grid has a defined hint. The solution for this form is as follows:

    uses
      Controls;

    type
     TMyForm = class (TForm)
     ...
     public
      Procedure CMMouseEnter (var msg: TMessage); message CM_MouseEnter;
      Procedure CMMouseLeave (var msg: TMessage); message CM_MouseLeave
     end;

    Procedure TMyForm.CMMouseEnter (var msg: TMessage); 
    begin
     inherited;
     if msg.lparam = integer (dbGrid1)
      then sb.simpletext:= dbGrid1.Hint
    end;

    Procedure TMyForm.CMMouseLeave(var msg: TMessage); 
    begin
     inherited;
     if msg.lparam = integer (dbGrid1)
      then sb.simpletext:= ''
    end;

Whilst this code works, I don't like that integer (dbGrid1) cast; is there a better way of doing this?

Where doesn't this code work? Another form has a page control holding two tabsheets; on one tabsheet there are speedbuttons with hints, and on the other tabsheet there is a dbgrid with a hint. Writing similar code to the above doesn't work - the value of msg.lparam upon entering CMMouseEnter would appear to be the value of casting the page control (maybe its handle?). So how does one get to the controls with defined hints?

AmigoJack
  • 5,234
  • 1
  • 15
  • 31
No'am Newman
  • 6,395
  • 5
  • 38
  • 50