How can I see who triggered an action in Delphi?
Asked Answered
B

8

18

When a TAction event fires, the "Sender" is always the action itself. Usually that's the most useful, but is it somehow possible to find out who triggered the action's OnExecute event?

Example

Let's say you have a form with the following:

  • 2 buttons, called Button1 and Button2
  • 1 TAction called actDoStuff

The same action is assigned to both buttons. Is it possible to show which button I clicked?

Example.dfm

object Form1: TForm1
  object Button1: TButton
    Action = actDoStuff
  end
  object Button2: TButton
    Action = actDoStuff
    Left = 100
  end
  object actDoStuff: TAction
    Caption = 'Do Stuff'
    OnExecute = actDoStuffExecute
  end
end

Example.pas

unit Example;
interface
uses Windows, Classes, Forms, Dialogs, Controls, ActnList, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    actDoStuff: TAction;
    procedure actDoStuffExecute(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation    
{$R *.dfm}

procedure TForm1.actDoStuffExecute(Sender: TObject);
begin
  ShowMessage('Button X was clicked');
end;

end.

The only solution I see at the moment is to not use the action property of buttons, but having an eventhandler for each button, and calling actDoStuffExecute() from there, but that sort of defies the whole purpose of using actions in the first place.

I don't want to have a dedicated action for each separate control either. The example above is a simplified version of the problem that I'm facing. I have a menu with a variable number of menu items (file names), and each menu item basically has to do the same thing, except for loading another file. Having actions for each menu item would be a bit silly.

Bragg answered 21/7, 2010 at 18:57 Comment(2)
See that "Sender: TObject" parameter?... That is pre-populated for you... Try taking a look at Sender inside your function.Photofluorography
Yeah, but in the example above, actDoStuff would be the sender. I want to know whether button1 or button2 was pressed.Bragg
W
33

Try using the ActionComponent property:

Stores the client component that caused this action to execute.

Use ActionComponent to discern which client component caused this action to execute. For example, examine ActionComponent from an OnExecute event handler if you need to know what user action triggered this action.

When the user clicks a client control, that client sets ActionComponent before calling the action's Execute method. After the action executes, the action resets ActionComponent to nil.

For example:

  ShowMessage( (Sender as TAction).ActionComponent.Name );

Using this I get "Button1" and "Button2" when I click the first and second button respectively.

Wenda answered 21/7, 2010 at 20:43 Comment(2)
Beware: What happens when the action is triggered by a keyboard shortcut?Nostrum
Great, exactly what I was looking for!Bragg
M
10

Knowing what button triggered the action sort of goes against the point of using actions - an action may be triggered by a button click, or a menu click, or any number of other user activities. Actions exist to unify the state management of enable/disabled and click handling between buttons and menus.

If you want to know which button fired the action because you want to perform a slightly different operation, or "flavor" the operation differently, then perhaps TAction isn't the right solution for what you want to do.

Martini answered 21/7, 2010 at 19:19 Comment(2)
You propose here that TAction isn't the right solution, yet you don't propose alternative. Disabling action globally is the easiest way to disable program functions when they are not allowed to execute and quite often you need slightly different functionality across various windows so implementing own solution isn't really practical instead of using pre-made solution like TAction. Disabling program function through disabling action often requires least maintenance later (it disables all relevant menus, toolbars etc. automatically in all windows that use it).Whirlybird
I'm a huge proponent of TAction. What I'm suggesting is that a design that seeks to change behavior of an event based on which UI element was used to fire the event is a flawed design. Use TActions to handle actions uniformly regardless of trigger. If you really truly need something different to happen when the button is clicked instead of the menu item, then create a different action.Martini
E
3

Instead of actions, just use a click event. Set all buttons to use the same event handler. Ideally, NOT named after the first button (you can rename it).

Here's the code:

Procedure TMyForm.DestinationButtonClickHandlerThing(Sender: TObject); 
begin
  if Sender = Btn_ViewIt then
  begin
    // View It
  end
  else if Sender = Btn_FaxIt then
  begin
    // Fax It
  end
  else if Sender = Btn_ScrapIt then
  begin
    // Scrap It
  end
  else 
    ....   // error
   ...
end;
Edwin answered 21/7, 2010 at 20:10 Comment(2)
Using OnClick event handlers, even a single handler for several buttons, means losing the ability to enable/disable etc them all in one place (the action's or actionlist's onupdate handler). That is unless you combine them with an action. Assigning an action will overwrite the onclick handler however, so you would have to reset it for all buttons involved and possibly do that every time you change something in the action.Aceydeucy
Of course this is the right answer! That's why Sender is there.Highoctane
B
1

Ok, in the meanwhile I think I found a workable solution..

I can have all controls use the same action; I just need to override their OnClick event handler, and I just need a single handler for all of them.

I'm still interested to know if it's possible to find out which control triggered the action, but for my current application I'm using a solution that's similar to the code below:

unit Example;

interface

uses
  Windows, Classes, Forms, Dialogs, Controls, ActnList, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    actDoStuff: TAction;
    procedure actDoStuffExecute(Sender: TObject);
    procedure ButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.actDoStuffExecute(Sender: TObject);
begin
  ShowMessage('Button '+TControl(Sender).Name +' was clicked')
end;

procedure TForm1.ButtonClick(Sender: TObject);
begin
  actDoStuffExecute(Sender)
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Button1.OnClick := ButtonClick;
  Button2.OnClick := ButtonClick
end;

end.
Bragg answered 21/7, 2010 at 19:11 Comment(2)
If you are writing OnClick event handlers in the buttons, kindly unlink from the action completely. Seriously!Indoeuropean
You need the link with the action if you want to enable/disable all buttons in one place (the action's or actionlist's onupdate handler). You do however need to take care as the onclick handler may be overwritten at each update to the action.Aceydeucy
U
1

There are situations where the same action should apply to similar controls. The problem with

ShowMessage( (Sender as TAction).ActionComponent.Name );

is that when the action is invoked by a say popup menu, you get the popup menu's name. You could use:

procedure TMyForm.actMyActionExecute(Sender: TObject);
var
  LMyControl: TMyControl;
begin
  if Screen.ActiveControl.Name = 'MyControl1' then
    LMyControl = Sender as TMyControl
  else
    Exit;
  // Use the local variable for whatever needed
end;
Unmitigated answered 31/1, 2014 at 0:18 Comment(0)
P
1

I have a bunch of panels and I want to let the user right click any of those panels and perform a "delete file" action. So, I have a single pop-up menu associated with all those panels. This is how I find out which panel was right clicked:

(Note: I put lots of comments to clearly explain how it works. But if you don't like it, you can compactify the code to 2 lines (see the second procedure)).

So, if you have actions assigned to that pop-up menu:

procedure Tfrm.actDelExecute(Sender: TObject);
VAR
  PopMenu: TPopupMenu;
  MenuItem: TMenuItem;
  PopupComponent: TComponent;
begin
 { Find the menuitem associated to this action }
 MenuItem:= TAction(Sender).ActionComponent as TMenuItem;  { This will crash and burn if we call this from a pop-up menu, not from an action! But we always use actions, so.... }

 { Was this action called by keyboard shortcut? Note: in theory there should be no keyboard shortcuts for this action if the action can be applyed to multiple panels. We can call this action ONLY by selecting (right click) a panel! }
 if MenuItem = NIL then
  begin
   MsgError('This action should not be called by keyboard shortcuts!');
   EXIT;
  end;

 { Find to which pop-up menu this menuitem belongs to }
 PopMenu:= (MenuItem.GetParentMenu as TPopupMenu);

 { Find on which component the user right clicks }
 PopupComponent := PopMenu.PopupComponent;

 { Finally, access that component }
 (PopupComponent as TMonFrame).Delete(FALSE);
end;

If you only have a simple pop-up menu (no actions assigned):

procedure Tfrm.actDelHddExecute(Sender: TObject);
VAR PopupComponent: TComponent;
begin
 PopupComponent := ((Sender as TMenuItem).GetParentMenu as TPopupMenu).PopupComponent;
 (PopupComponent as TMonFrame).Delete(TRUE);
end;

You can put all that code in a single function that returns a TPanel and call it like this:

procedure Tfrm.actDelWallExecute(Sender: TObject);
begin
 if GetPanelFromPopUp(Sender) <> NIL
 then GetPanelFromPopUp(Sender).Delete(FALSE);
end;
Predilection answered 18/7, 2020 at 13:40 Comment(0)
M
0

set the Tag of the buttons as 1, 2, ... etc and then:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Button1.OnClick := ButtonClick;
  Button2.OnClick := ButtonClick;
end;

procedure TForm1.ButtonClick(Sender: TObject);
begin
  if Sender is TButton then
  begin
    Caption := 'Button: ' + IntToStr(TButton(Sender).Tag);
  end;  
end;
Margarine answered 21/7, 2010 at 19:41 Comment(1)
The point of this question is how to know who triggered a TAction, and it is not how to know a TNotifyEvent's Sender, which is quite obvious.Emendate
S
0

The tag property of the different components that use the action could be coordinated to call the desired flow. I found this post while having the same problem. This worked.

DoGetColor((Sender as TAction).ActionComponent.Tag);

Sochor answered 11/2 at 17:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.