OnNewItem event for TMainMenu - delphi

I have a TMainMenu on a form and I want to add an event when an TMenuItem is added to the TmainMenu.
TMainMenu.OnChange(Sender: TObject; Source: TMenuItem; Rebuild: Boolean) doesn't seem to work because there is no difference in params values when adding or removing or updateing items. And I need to react only to new items.
Any ideas?

Problem
Looking at the code for TMenuItem.Add() it's clearly obvious that the only event that's triggered is the OnChange. Because of that there's no easy and clean solution.
Clean solution - refactor your code
My first thought: surely you know when a menu item is added to the menu, it's your code that's adding it. The best option would be to simply re-factor the code so it doesn't directly add the menu item but calls a method of your choice. For example, if you're using code like this:
someMenu.Add(M); // where someMenu is an existing menu item and "M" is the new item
you could refactor it to something like this:
// procedure definition in private section of main form, or somewhere else relevant:
procedure AddSubMenu(const Where, What: TMenuItem);
// Refactor your code to do this:
AddSubMenu(someMenu, M);
// Then in the "AddSubMenu" you can do whatever you want to do for new items:
procedure TFormName.AddSubmenu(const Where, What: TMenuItem);
begin
// Do your thing.
// Then do the actual adding of the menu item
Where.Add(What);
end;
Alternative solution - track existing menu items
Use the OnChange item that you know gets called, recursively walk the list of existing TMenuItems and do something with them so you know you've seen them before. For example set the Tag to 1 - you'll know items with Tag = 0 are new. Or add all items into a Dictionary so you can easily test what items are new and what items are pre-existing.
Here's an example OnChange handler using the Tag property to track rather a menu item is New or Old. Make sure you handle the initial creating of the Menu properly; For example, I'd assign the OnChange at runtime, from the form's OnCreate, after the Menu has been initialized from the DFM and after setting the Tag for all the design-time menu items to 1:
procedure TForm1.MainMenu1Change(Sender: TObject; Source: TMenuItem; Rebuild: Boolean);
var i: Integer;
procedure VisitMenuItem(const M: TMenuItem);
begin
if M.Tag = 0 then
begin
// First time I see this TMenuItem!
// DO my thing
ShowMessage(M.Caption);
// Mark the item so I don't "Do my thing" again
M.Tag := 1;
end;
end;
procedure RecursivelyVisitAllMenuItems(const Root: TMenuItem);
var i:Integer;
begin
VisitMenuItem(Root);
for i:=0 to Root.Count-1 do
RecursivelyVisitAllMenuItems(Root.Items[i]);
end;
begin
for i:=0 to MainMenu1.Items.Count-1 do
RecursivelyVisitAllMenuItems(MainMenu1.Items[i]);
end;

Related

How to get Item index in FMX.TreeView with CheckBoxes if CheckBox was changed?

I'm trying to use FMX.TreeView with CheckBoxes, but can't find a way how to identify item, which fired TreeViewChangeCheck event.
All solutions I found were about VCL.ComCtrls TreeView with TTreeNode class, but I am using FMX.TreeView.
Can anybody help me? Thanks.
The OnChangeCheck event is of class TNotifyEvent. Its Sender: TObject parameter tells you who triggered the event. For example, the following code
procedure TForm19.TreeView1ChangeCheck(Sender: TObject);
begin
ShowMessage(Sender.ToString);
end;
might show TTreeViewItem 'TreeViewItem5'
Or, if you want to change a property of that item,
procedure TForm19.TreeView1ChangeCheck(Sender: TObject);
begin
if Sender is TTreeViewItem then
if TTreeViewItem(Sender).IsChecked then
TTreeViewItem(Sender).Text := 'Checked'
else
TTreeViewItem(Sender).Text := 'Not checked';
end;
Or, if you really want the index of the item:
ShowMessage(IntToStr((Sender as TTreeViewItem).Index));

Extend the event OnClick of all MenuItems in the screen to execute another block of code

I want to set an event OnClick to all TMenuItems on the screen to do what the event currently does, and another few lines of code. I am currently using Delphi 5
For example, say that I have a TMenuItem with the code:
procedure TdesktopForm.MenuFoo1Click(Sender: TObject);
begin
ShowMessage(TComponent(Sender).Name)
end;
and I also have the following procedure:
procedure TdesktopForm.bar;
begin
ShowMessage('extra')
end;
And I want to everytime I click the TMenuItem the program show the TMenuItem's name and also the 'extra' message.
The example shown is just a demonstration of my problem, as in the real software I have over 300 menu items, I want to do this generically, so I won't have to add extra lines of code to all current menu clicks, nor add them when I add new menu items. The order of execution (between the menu click and the extra block of code) doesn't matter.
I tried using TActionList but I couldn't retrieve the object triggering the action, hence, I can't print it's name. I tried using ActiveControl but it always return the focused currently focused object, not the actual menu that I clicked. And also, the TAction execute event overwrites my TMainMenu.OnClick event
As long as all your event handlers are assigned at some point (either at design time or at run time) and don't change afterwards, you can do something like this:
Enumerate all menu items in the menu
For each create an object like the one described below
type
TEventInterceptor = class(TComponent)
private
FOrigEvent: TNotifyEvent;
FAdditionalEvent: TNotifyEvent;
procedure HandleOnClick(_Sender: TObject);
public
constructor Create(_MenuItem: TMenuItem; _AdditionalEvent: TNotifyEvent);
end;
constructor TEventInterceptor.Create(_MenuItem: TMenuItem; _AdditionalEvent: TNotifyEvent);
begin
inherited Create(_MenuItem);
FOrigEvent := _MenuItem.OnClick;
FAdditionalEvent := _AdditionalEvent;
_MenuItem.OnClick := HandleOnClick;
end;
procedure TEventInterceptor.HandleOnClick(_Sender: TObject);
begin
FOrigEvent(_Sender);
FAdditinalEvent(_Sender);
end;
Note that this code is completely untested and may not even compile.
I'm also not sure whether this works with Delphi 5. It does with Delphi 6 though, so chances are good.
Edit:
Some additional notes (thanks for the comments):
Inheriting this class from TComponent makes the form free it automatically when it is being destroyed.
HandleOnClick should possibly check if FOrigEvent is assigned before calling it.

Dynamically create submenu

I have a TMainMenu with a menu item called mnuWindows. I wish to create submenu items dynamically. I thought this code might do it but it doesn't work:
var
mnuitm: TMenuItem;
mnuitm:=TMenuItem.Create(nil);
mnuitm.Text:='some text';
mnuWindows.AddObject(mnuitm);
When I click on mnuWIndows, nothing happens. Where am i going wrong?
EDIT:
The submenu was not displaying on clicking because each time I did so, the program had been freshly started and I didn't realize that under these circumstances, two clicks are necessary. The first click doesn't visibly do anything and the second click drops down the submenu. So, I concede the code snippet above works.
But I still have a difficulty. I need to create several submenu items so I tried the following loop inside the mnuWindows OnClick event handler:
for I := 0 to TabSet1.Tabs.Count - 1 do
begin
mnuitm := TMenuItem.Create(mnuWindows);
mnuitm.Text := TabSet1.Tabs[I].Text;
mnuitm.OnClick:=MenuItemClick;
if not mnuWindows.ContainsObject(mnuitm) then
mnuWindows.AddObject(mnuitm);
end;
The intent of the above code is that clicking the mnuWindows item displays a list of the tabs in a tabset. This code works up to a point. On first being clicked, it correctly lists the current tabs. But when I add a tab and click on mnuWindows again, the new tab is not shown in the list. The list is exactly as before. I wondered if the menu needed updating or refreshing somehow. I came across the following method
IFMXMenuService.UpdateMenuItem(IItemsContainer, TMenuItemChanges)
but it is poorly documented and I'm not sure how to use it or even whether it is relevant.
EDIT2:
I thought the two down votes on my post were harsh. I have searched the web extensively for an example of how to dynamically create submenus in Firemonkey and there is very little. I did find a solution from 2012, but syntax changes since then mean that it does not work in Tokyo 10.2.
Try something like this. As others have commented above, you need to provide an event that will happen when the menu item is clicked. Note also that my methods here require a bunch of parameters. It would have been cleaner if I had created a class and passed the details that way, but I wrote this a long time ago and now have many places in my code that use it in this form. Also, if I wrote this now, I would use a function to return the menu item created in case I needed to interact with it in particular cases (e.g., check it, assign a hot key, etc.)
procedure PopMenuAddItem(menu: TPopupMenu; sText: string; iID: integer;
clickEvent: TNotifyEvent; bEnabled: boolean = true);
var
NewMenuItem: TmenuItem;
begin
NewMenuItem := TmenuItem.create(menu);
with NewMenuItem do
begin
Caption := sText;
tag := iID;
Enabled := bEnabled;
OnClick := clickEvent;
end;
menu.Items.Add(NewMenuItem);
end;
procedure PopMenuAddSubItem(menuItem: TmenuItem; sText: string; iID: integer;
clickEvent: TNotifyEvent; bEnabled: boolean = true);
var
NewMenuItem: TmenuItem;
begin
NewMenuItem := TmenuItem.create(menuItem);
with NewMenuItem do begin
Caption := sText;
tag := iID;
Enabled := bEnabled;
OnClick := clickEvent;
end;
menuItem.Add(NewMenuItem);
end;
I have answered my own question.
As a reminder, what I wanted to do was to dynamically create a submenu under my top level menu item "Windows" (component name "mnuWindows"). In the submenu, I wished to list the names of the tabs in a tabset.
Attempting to create the submenu dynamically in the mnuWindows.OnClick event was a failure.
My eventual solution was to rebuild the submenu with the following method and to call this method immediately after creating a new tab, removing a tab, or renaming a tab:
procedure Form1.ReBuildWindowsMenu;
var
mnuitm: TMenuItem;
I: Integer;
begin
mnuWindows.Clear; // removes submenu items
for I := 0 to TabSet1.Tabs.Count - 1 do
begin
mnuitm := TMenuItem.Create(MainMenu1);
mnuitm.Caption:= TabSet1.Tabs[I].Text; // submenu item displays same text as associated tab
mnuitm.OnClick := MenuItemClick; // makes the associated tab active
mnuWindows.AddObject(mnuitm);
end;
end;
The OnClick handler contains the single statement
TabSet1.ActiveTabIndex:=(Sender as TMenuItem).Index;
This simple solution keeps my Windows list perfectly synched with the tabs in the tabset. I'm planning to use a similar approach to put a most recently used (MRU) file list into my File menu.

Multiselect using mouse in TListview?

Say you have a form with a TListView, with MultiSelect enabled. Normally you have to press shift or control to select multiple items. If I'd like to have the listview select/de-select additional items with only a mouse-click, how do I do that?
Like if you click item1 and then item3 both would be selected, and then if you click item1 again, it would leave only item3 selected.
I don't see any built in properties in the Object Inspector that look relevant. Do I need to modify my ListviewOnMouseDown or ListviewOnSelectItem to change the selection?
This kind of selection is not implemented in listview controls, maybe because they support checkboxes which can be independently checked, I don't know...
You have to modify the behavior yourself. But using OnMouseDown or OnSelectItem events are not really appropriate, as the selection have already been carried out by the time they are fired. Below example intercepts left mouse button down message.
type
TListView = class(vcl.comctrls.TListView)
protected
procedure WMLButtonDown(var Message: TWMLButtonDown);
message WM_LBUTTONDOWN;
end;
procedure TListView.WMLButtonDown(var Message: TWMLButtonDown);
begin
Message.Keys := MK_CONTROL;
inherited;
end;
You can intercept the message by any other means, assigning to WindowProc, deriving a new control... Of course you can also implement the behavioral change conditionally, or would like to test and preserve other virtual keys/buttons. See documentation in that case.
A ListView does not natively support what you are asking for. You will have to maintain your own list of "selected" items, using the OnClick or OnMouseDown event to detect which item the user is clicking on so you can toggle the contents of your list accordingly and then reset the ListView's selection to match your updated list as needed.
Set the ExtendedSelect property of the ListView to False.
Update:
The ListView does not have an ExtendedSelect property. It is only available for the ListBox.
But it would be possible to add that to the ListView. Here's an improved version of the code posted by Sertac that adds ExtendedSelect. I also improved it so it is a bit more user friendly than the original because it keeps the shift key for multi selection working. (I hope I may post that improved version here, it is a bit easier to read than in my comment).
type
TListView = class(Vcl.ComCtrls.TListView)
private
FExtendedSelect: Boolean;
procedure SetExtendedSelect(const Value: Boolean);
protected
procedure WMLButtonDown(var Message: TWMLButtonDown);
message WM_LBUTTONDOWN;
public
property ExtendedSelect: Boolean read FExtendedSelect write SetExtendedSelect;
end;
procedure TListView.SetExtendedSelect(const Value: Boolean);
begin
FExtendedSelect := Value;
end;
procedure TListView.WMLButtonDown(var Message: TWMLButtonDown);
begin
if not FExtendedSelect then
begin
if Message.Keys and MK_CONTROL <> 0 then
Message.Keys := Message.Keys and (not MK_CONTROL)
else if Message.Keys and MK_SHIFT = 0 then
Message.Keys := MK_CONTROL;
end;
inherited;
end;

How to let all items in multi-level PopupMenu act as one radiogroup?

I have a PopupMenu with submenus and only one item in total shall be checked at a time. GroupIndex and RadioItem properties do not work outside of the respective submenus as far as I have tried.
I have found this piece of code to check a PopupMenu and its direct sub-components but I haven't had any luck with creating a popup-wide variety of this.
I need a solution that is fast - the PopupMenu has 4x14 entries, always iterating through all menus and subentries can't be the best solution for this, I suppose.
Is there a simple property for this that I am missing or is the rocky path of iteration my only option?
Add all 56 items as actions to one ActionList and give all GroupIndex properties the same value.
Now, add menu-items, sub-menu's and sub-sub-menu's in any tree-like fashion and link each of them to an action. Checking one menu-item, wherever positioned, will automatically uncheck all others.
Et voilĂ !
NGLN's answer is better, but if you really don't want or don't like to use an ActionList, then this routine will also do:
procedure CheckMenuItem(Item: TMenuItem);
procedure UncheckMenu(Menu: TMenuItem; GroupIndex: Byte);
var
I: Integer;
begin
if Menu.RadioItem and (Menu.GroupIndex = GroupIndex) then
Menu.Checked := False;
for I := 0 to Menu.Count - 1 do
UncheckMenu(Menu[I], GroupIndex);
end;
begin
if (not Item.Checked) and Item.RadioItem and (Item.GroupIndex <> 0) then
begin
UncheckMenu(Item.GetParentMenu.Items, Item.GroupIndex);
Item.Checked := True;
end;
end;

Resources