开发者

How to know if a TToolButton's MenuItem is dropped down?

开发者_Go百科In the context of an owner-draw toolbar used to host menu entries (TToolButtons with their MenuItem and Grouped properties set), I want to know if the corresponding menuitem is dropped. The problem is that the State property in the OnAdvancedCustomDrawButton doesn't reflect that information.

When the toolbutton is clicked, its Down property is true, but in the particular case above (MenuItem set and Grouped=True), just after the menu is dropped, another OnAdvancedCustomDrawButton is fired but this time with Down set to false.

This means that I end up drawing the button with a NOT down state.

Looking at the source of the VCL, it seems the information about which toolbutton is dropped is stored in the TToolBar's FMenuButton private field, and Windows is notified of the hot state by a Perform(TB_SETHOTITEM), however neither of these provide read-access...

Also the VCL performs the dropdown via a private FTempMenu, whose handle is thus not accessible.

PS: FWIW if using the hacky solution, the only private field usable seems to be FButtonMenu which will have to be compared against your Button.MenuItem in the CustomDraw, the others private fiels are either not set early enough (like FMenuButton) or are private variables (like MenuButtonIndex) with a variable location. Still not too satisfying though.


Getting the menu-dropped-down status is problematic, the code that makes the menu pop up is pretty convoluted, makes use of some message hooks. It's generally not the code you'd want to touch. Fortunately the Toolbar itself keeps track of the drop-down menu status, using the FMenuDropped variable. Unfortunately that variable is private, you can't access it from outside, the "hacked" trick doesn't work. Being private it also doesn't offer RTTI!

There are two possible solutions:

Modify the VCL and add a property that makes FMenuDropped available from outside

Go to ComCtrls.pas, find the TToolBar = class(TToolWindow) declaration, go to the public section and add this:

property MenuDropped:Boolean read FMenuDropped;

From your code you'll then be able to check the toolbar if it has a dropped down menu or not. The unfortunate part of this is that it requires modifications to the VCL. Never a good idea, difficult to synchronize amongst several programmers.

Use a hack to access the FMenuDropped field directly, without changing the VCL

To do this you need to get the offset of the FMenuDropped field. Once you get that you can write something like this:

if PBoolean(Integer(Toolbar1) + 865)^ then
   DoStuffIfMenuIsDropped
else
   OtherStuffIfMenuIsNotDropped;

The 865 is actually the correct constant for Delphi 2010! Here's a very quick way of getting the constant.

  • Go to compiler settings, check "compile using debug DCU's"
  • Open ComCtrls.pas, go to procedure TToolButton.Paint, place a brakepoint in there.
  • Start the application, take a piece of paper and a pen. When the program stops at the brakepoint open up Debug Inspector. To do that simply place the cursor on the name of a field, any field, and hit Alt+F5. With the Debug Inspector window hit Ctrl+N to show the generic Inspect editor that allows you to inspect anything. Enter Integer(FToolbar). Note the result on the piece of paper.
  • Hit Ctrl+N again, this time enter Integer(@FToolBar.FMenuDropped). Note this second number.
  • The constant you need is the difference between the second and the first. That's it!

There are of course some possible problems. First of all this depends on the exact Delphi version you're using. If the code needs to be compiled on different versions of the Delphi compiler, clever $IFDEF need to be used. None the less this is workable.

(Edit): You can use this same technique to access any Private field of any class. But you'll need to think many times before doing this, because private fields are made private for a reason.


Use a class helper.

For example.

TToolBarHelper = class helper for TToolBar
private
    function GetMenuDropped: Boolean;
public
    property MenuDropped: Boolean read GetMenuDropped;
end;

...

function TToolBarHelper.GetMenuDropped: Boolean;
begin
    Result := Self.FMenuDropped;
end;

Now, anywhere you use a TToolBar, you can now access new property called MenuDropped.


When a dropdown button is clicked, the form is sent a TBN_DROPDOWN notification. This can be used to track the button that launched a menu:

type
  TForm1 = class(TForm)
    [...]
  private
    FButtonArrowDown: TToolButton;
    procedure WmNotify(var Msg: TWmNotify); message WM_NOTIFY;
  [...]

uses
  commctrl;

procedure TForm1.WmNotify(var Msg: TWmNotify);

  function FindButton(Bar: TToolBar; Command: Integer): TToolButton;
  var
    i: Integer;
  begin
    Result := nil;
    for i := 0 to Bar.ButtonCount - 1 do
      if Bar.Buttons[i].Index = Command then begin
        Result := Bar.Buttons[i];
        Break;
      end;
  end;

begin
  if (Msg.NMHdr.code = TBN_DROPDOWN) and
      (LongWord(Msg.IDCtrl) = ToolBar1.Handle) then begin
    FButtonArrowDown := FindButton(ToolBar1, PNMToolBar(Msg.NMHdr).iItem);
    inherited;
    FButtonArrowDown := nil;
  end else
    inherited;
end;


procedure TForm1.ToolBar1AdvancedCustomDrawButton(Sender: TToolBar;
  Button: TToolButton; State: TCustomDrawState; Stage: TCustomDrawStage;
  var Flags: TTBCustomDrawFlags; var DefaultDraw: Boolean);
var
  DroppedDown: Boolean;
begin
  DroppedDown := Button = FButtonArrowDown;
  [...]
 


Note that the 'DroppedDown' variable in 'OnAdvancedCustomDrawButton' is not synchronous with the 'Down' state of the button, it only reflects the 'down' state of the dropdown-arrow.

This, I believe, is the cause of the problem in this question: when a toolbar has the TBSTYLE_EX_DRAWDDARROWS extended style and its buttons do not have the BTNS_WHOLEDROPDOWN style, only the dropdown-arrow part of the button is depressed when its menu is launched. The button, in fact, is not 'down'. AFAIU, you want to draw the button pressed even so. Unfortunately the VCL does not expose any property to have the buttons 'wholedropdown'.

It is possible to set this style on the buttons:

var
  ButtonInfo: TTBButtonInfo;
  i: Integer;
  Rect: TRect;
begin
  ButtonInfo.cbSize := SizeOf(ButtonInfo);
  ButtonInfo.dwMask := TBIF_STYLE;
  for i := 0 to ToolBar1.ButtonCount - 1 do begin
    SendMessage(ToolBar1.Handle, TB_GETBUTTONINFO, ToolBar1.Buttons[i].Index,
        LPARAM(@ButtonInfo));
    ButtonInfo.fsStyle := ButtonInfo.fsStyle or BTNS_WHOLEDROPDOWN;
    SendMessage(Toolbar1.Handle, TB_SETBUTTONINFO, ToolBar1.Buttons[i].Index,
        LPARAM(@ButtonInfo));
  end;

  // Tell the VCL the actual positions of the buttons, otherwise the menus
  // will launch at wrong offsets due to the separator between button face
  // and dropdown arrow being removed.
  for i := 0 to ToolBar1.ButtonCount - 1 do begin
    SendMessage(ToolBar1.Handle, TB_GETITEMRECT, 
                ToolBar1.Buttons[i].Index, Longint(@Rect));
    ToolBar1.Buttons[i].Left := Rect.Left;
  end;
end;


Then the dropdown part will not act separately from the button, or more correctly, there won't be a separate dropdown part, hence the down/pressed state of a button will be set whenever its menu is launched.

But due to the VCL being unaware of the state of the buttons will pose one problem; whenever the VCL updates the buttons, re-setting the styles would be necessary.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜