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. EnterInteger(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.
精彩评论