October 15, 2013

Where is that MenuItem displayed ?

I had the need to display a form when the user clicks on an item in a menu. Easy! But the form, which
is small had to be displayed where the MenuItem was displayed. After Googleling a little bit, I did not
find what I needed. So I wrote it. Here is the result.

In Delphi, a TMenuItem has no property to tell where is has been displayed. I had to find how to get
that information. Among TMenuItem events is OnDrawItem. This event is called when the menu has
the OwnDraw property set to true. The corresponding event handler has a TCanvas property and a
TRect property. That was enough. A TCanvas has a handle which can be used to get hand on the
window where it is drawn. And the rectangle gives the position and size of the menu item in that
window. I had everything I needed.

But wait! When the OwnerDraw property of a menu is set to true, all menu items having an
OnDrawItem event assigned are no more drawn. That was annoying because I want it to be drawn as
usual.

Looking at TMenuItem source code, I found that drawing an item is done by AdvancedDrawItem
method. So I had to call it. There was a problem: this is a protected method and I cannot call it from
my TForm1 class. I can’t? Not really.

A protected class member can only be accessed by the class itself or any derived class, or any class in
the same source code. This feature has been considered as a bug by OOP purist but it is “as
designed”. In recent Delphi version, there is now a new “strict” keyword which restrict the
accessibility like OOP purist want it. This isn’t used for AdvancedDrawItem. To access a protected
method, you just have to declare a class deriving from the target class without changing anything. If
that declaration is located in the same source file as you code that need to call it, it can be called.

That empty class is declared like this:
  THackMenuItem = class(TMenuItem)
    // Intentionally left empty
  end;

The final code is the following:
procedure TForm1.ServiceMnuClick(Sender: TObject);
begin
    Form2.Top  := FServiceMnuRect.Top;
    Form2.Left := FServiceMnuRect.Left;
    Form2.ShowModal;
end;

procedure TForm1.ServiceMnuDrawItem(
    Sender   : TObject;
    ACanvas  : TCanvas;
    ARect    : TRect;
    Selected : Boolean);
var
    State    : TOwnerDrawState;
    MenuItem : TMenuItem;
    Handler  : TMenuDrawItemEvent;
begin
    MenuItem := Sender as TMenuItem;
    GetWindowRect(WindowFromDc(ACanvas.Handle), FServiceMnuRect);
    FServiceMnuRect.Top := FServiceMnuRect.Top  + ARect.Top;

    Handler := MenuItem.OnDrawItem;
    try
        MenuItem.OnDrawItem := nil;
        if Selected then
            State := [odSelected]
        else
            State := [];
        THackMenuItem(MenuItem).AdvancedDrawItem(ACanvas, ARect, State, FALSE);
    finally
        MenuItem.OnDrawItem := Handler;
    end;
end;

ServiceMnuClick is where the secondary form is displayed by calling ShowModal. His position is first
set from the top-left corner of FServiceMnuRect (A member variable of TForm1).

ServiceMnuDrawItem has the responsibility of initializing FServiceMnuRect. Basically, it make use of
the canvas handle (which is a “HDC” or Handle to Device Context in Windows terminology) to get the
rectangle of the window behind the canvas. I used two Windows API calls: WindowFromDc and
GetWindowRect. WindowFromDc fetch the window handle given an HDC and GetWindowRect fetch
the bounding rectangle of a given window.
The bounding rectangle has to be fixed by the drawing rectangle passed to the OnDrawItem event
handler. This drawing rectangle is where the menu item is really drawn.

To call AdvancedDrawItem which drawn the menu item, I used the trick explained above. But there
was another issue: Normally AdvancedDrawItem will call OnDrawItem event handler, so we end up
with a recursive call and a nice stack overflow. So before calling AdvancedDrawItem, I save and clear
the event handler and restore it after.

Complete source file is as follow:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Forms, Dialogs, Menus, Unit2;

type
  THackMenuItem = class(TMenuItem)
    // Intentionally left empty
  end;

  TForm1 = class(TForm)
    MainMenu1: TMainMenu;
    FileMnu: TMenuItem;
    OptionsMnu: TMenuItem;
    BrolMnu: TMenuItem;
    N1: TMenuItem;
    ExitMnu: TMenuItem;
    ServiceMnu: TMenuItem;
    TestMnu: TMenuItem;
    procedure ServiceMnuClick(Sender: TObject);
    procedure ServiceMnuDrawItem(Sender: TObject; ACanvas: TCanvas; ARect: TRect;
      Selected: Boolean);
  private
    FServiceMnuRect : TRect;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.ServiceMnuClick(Sender: TObject);
begin
    Form2.Top  := FServiceMnuRect.Top;
    Form2.Left := FServiceMnuRect.Left;
    Form2.ShowModal;
end;

procedure TForm1.ServiceMnuDrawItem(
    Sender   : TObject;
    ACanvas  : TCanvas;
    ARect    : TRect;
    Selected : Boolean);
var
    State    : TOwnerDrawState;
    MenuItem : TMenuItem;
    Handler  : TMenuDrawItemEvent;
begin
    MenuItem := Sender as TMenuItem;
    GetWindowRect(WindowFromDc(ACanvas.Handle), FServiceMnuRect);
    FServiceMnuRect.Top := FServiceMnuRect.Top  + ARect.Top;

    Handler := MenuItem.OnDrawItem;
    try
        MenuItem.OnDrawItem := nil;
        if Selected then
            State := [odSelected]
        else
            State := [];
        THackMenuItem(MenuItem).AdvancedDrawItem(ACanvas, ARect, State, FALSE);
    finally
MenuItem.OnDrawItem := Handler; end; end; end.



Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be

4 comments:

Jolyon Smith said...

Just curious... would GetMenuItemRect() not have been simpler ?

http://msdn.microsoft.com/en-us/library/windows/desktop/ms647981(v=vs.85).aspx

Jens Borrisholt said...

You could have achived this a bit simpler:

type
TMenuItem = class(Menus.TMenuItem);

TForm1 = class(TForm)
...
implementation
...

procedure TForm1.ServiceMnuDrawItem( ...
begin
...
MenuItem.AdvancedDrawItem(ACanvas, ARect, State, FALSE);
...
end;

By doing it this way you don't have at typecast your variable each time.

François Piette said...

+jens Borrisholt I don't like this way because it hides that fact that there is a trick. A simple copy/paste of that source fragment won't work and there is nothing helping the user to understand why it doesn't work. My version doesn't suffer from that. Easier maintenance.

François Piette said...

+Jolyon Smith Not sure which rectangle it refers. Anyway it needs the hwnd so it is not really simpler IMO.