Runtime assigned action's ShortCut does not fire in custom component - delphi

I am having a problem getting an Action assigned to a custom component's inherited Action property to work when the code is entirely created at run time (i.e. no form designer components). If I use an ActionList in the form designer and then use the same code things work fine.
Here is my constructor of a component derived from TCustomControl:
self.FButtonSCActionList := TActionList.Create( self.Parent );
self.FButtonSCActionList.Name := 'ButtonSCActionList';
self.FButtonSCAction := TAction.Create( self.FButtonSCActionList );
self.FButtonSCAction.Name := 'ClickShortcutAction';
self.FButtonSCAction.OnExecute := self.ExecuteButtonShortcut;
self.FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
self.FButtonSCAction.Enabled := TRUE;
self.FButtonSCAction.Visible := TRUE;
self.FButtonSCAction.ActionList := self.FButtonSCActionList;
self.Action := FButtonSCAction;
If I create the custom control with this code, add it to the toolbar, place it on a form in a new VCL Forms application and then run the application, when I press the shortcut key nothing happens. If I create the control without this code, place it on a form and assign an Actionlist to the form, and then put the code lines just involving creating an action and assigning it to the component's Action property into an onclick event handler for the button, it then responds to the shortcut keypress correctly. For the life of me I can't see what is different, but hopefully you Actions Delphi gurus can...
The purpose of this Action is to allow the developer to assign a custom shortcut to the button in the Object Inspector via a property. I would like to assign directly to the "built in" Action but cannot find out how to access its Shortcut Property. (Obviously I could do this via the other HotKey delphi functionality and will if I have to but I also want to understand Actions and this seems a good place to start...)

You don't need to create ActionList at design time. Use following code in your Create method:
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.SetSubComponent(true);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
FButtonSCAction.Enabled := TRUE;
FButtonSCAction.Visible := TRUE;
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
FButtonSCActionList := TActionList.Create(aOwner);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
During run-time creation of control, you can have situation where aOwner passed to your control will not be form itself, but another control. In that case instead of creating action list with aOwner you would have to call function that will give you the form from the aOwner parameter.
function GetOwnerForm(Component: TComponent): TComponent;
begin
Result := Component;
while (Result <> nil) and (not (Result is TCustomForm)) do
begin
Result := Result.Owner;
end;
end;
FButtonSCActionList := TActionList.Create(GetOwnerForm(aOwner));

Summary
There is no built-in Action component in TControl. It is an Action property that is unassigned by default. The user of the control can assign the property with whatever Action is desired. The designer of the control (you) does not have to provide an Action nor ActionList.
The actual problem
I would like to assign directly to the "built in" Action but cannot find out how to access its Shortcut Property.
That built-in Action is by default just an unassigned TAction property. And if the property is not assigned, i.e. the property does not point to an Action component, then its ShortCut property does not exist.
The purpose of this Action is to allow the developer (red. the user of your component/control) to assign a custom shortcut to the button in the Object Inspector via a property.
If that is your sole goal, then simply publish the Action property and do nothing further:
type
TMyControl = class(TCustomControl)
published
property Action;
end;
This will result in the appearance of the property in the developer's Object Inspector. The developer simply has to assign one of his own actions to it, and to set the ShortCut property of thát action. Thus the actual solution is to get rid of all your current code.
Why your current code doesn't work
self.FButtonSCActionList := TActionList.Create( self.Parent );
Self.Parent is nil during the constructor. Two things about that:
Unless you destroy the ActionList yourself in de destructor, you have a memory leak.
For default ShortCut processing, the application traverses all ActionLists which are (indirectly) owned by the currently focussed form or by the MainForm. Your ActionList has no owner, thus its ShortCuts are never evaluated.
Solution for the current code
First, some well-intentioned comments on your code:
Self is implicit and is not needed, nor customary.
Runtime made components do not need a Name property set.
The Visible and Enabled properties of an action are True by default.
Secondly, as Dalija Prasnikar already said, the ActionList is not needed at design time. And the ActionList has to be indirectly owned by the form that the control owns. So the control can own the ActionList too (XE2).
constructor TMyControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
FButtonSCActionList := TActionList.Create(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
end;
Somehere before XE2, at least still in D7, the ActionList had to be registered by the form that the control owns. (There is more to it, but since it is unlikely that the control is parented by another form nor that the action is invoked when another form is focussed, this simplification can be made). Registration could be done by making the form the owner of the ActionList. Since you give ownership of the ActionList beyond the control, let the ActionList notify its possibly destruction to the control with FreeNotification. (Ok, this is far-fetched, since typically the control then will be destroyed as well, but this is how it strictly should be done).
type
TMyControl = class(TCustomControl)
private
FButtonSCActionList: TActionList;
FButtonSCAction: TAction;
protected
procedure ExecuteButtonShortcut(Sender: TObject);
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
constructor Create(AOwner: TComponent); override;
end;
constructor TMyControl.Create(AOwner: TComponent);
var
Form: TCustomForm;
function GetOwningForm(Component: TComponent): TCustomForm;
begin
repeat
if Component is TCustomForm then
Result := TCustomForm(Component);
Component := Component.Owner;
until Component = nil;
end;
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
Form := GetOwningForm(Self);
if Form <> nil then
begin
FButtonSCActionList := TActionList.Create(Form);
FButtonSCActionList.FreeNotification(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
end;
end;
procedure TMyControl.ExecuteButtonShortcut(Sender: TObject);
begin
//
end;
procedure TMyControl.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FButtonSCActionList) and (Operation = opRemove) then
FButtonSCActionList := nil;
end;
Note that when GetOwningForm returns False (when the developer creates the control without owner), the ActionList is not created because it cannot resolve the owning form. Overriding SetParent could fix that.
Because transfering ownership to another component feels unnecessary (and could give problems with the IDE's streaming system when the code is run if csDesigning in ComponentState), there is another way to register the ActionList to the form by adding it to the protected FActionLists field:
type
TCustomFormAccess = class(TCustomForm);
constructor TMyControl.Create(AOwner: TComponent);
var
Form: TCustomForm;
function GetOwningForm(Component: TComponent): TCustomForm;
begin
repeat
if Component is TCustomForm then
Result := TCustomForm(Component);
Component := Component.Owner;
until Component = nil;
end;
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
Form := GetOwningForm(Self);
if Form <> nil then
begin
FButtonSCActionList := TActionList.Create(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
if TCustomFormAccess(Form).FActionLists = nil then
TCustomFormAccess(Form).FActionLists := TList.Create;
TCustomFormAccess(Form).FActionLists.Add(FButtonSCActionList)
end;
end;
end;
Reflection on this solution:
This approach is not desirable. You should not create action components within your custom control. If you have to, offer them seperately so that the user of your control can decide to which ActionList the custom Action will be added. See also: How do I add support for actions in my component?
TControl.Action is a public property, and TControl.SetAction is not virtual. This means that the user of the control can assign a different Action, rendering this Action useless, and you cannot do anything about nor against it. (Not publishing is not enough). Instead, declare another Action property, or - again - offer a separate Action component.

Thanks so much for all the help! For those who will use this question for later google-fu (I live in google these days when not in the Delphi IDE...) here is the final fully functional code for a custom component:
unit ActionTester;
interface
uses
Winapi.windows,
Vcl.ExtCtrls,
System.Types,
System.SysUtils ,
System.Classes,
Vcl.Controls,
Vcl.Forms,
Vcl.Graphics,
Messages,
Vcl.Buttons,
System.Variants,
System.UITypes,
Dialogs,
Vcl.ExtDlgs,
Generics.Collections,
System.Actions,
Vcl.ActnList,
Clipbrd,
TypInfo,
Rtti,
Menus;
type
TActionTester = class(TCustomControl)
private
{ Private declarations }
protected
{ Protected declarations }
FButtonSCActionList: TActionList;
FButtonSCAction: TAction;
procedure ExecuteButtonShortcut(Sender: TObject);
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
Procedure Paint; override;
Destructor Destroy; Override;
published
{ Published declarations }
Property OnClick;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TActionTester]);
end;
{ TActionTester }
constructor TActionTester.Create(AOwner: TComponent);
var
Form: TCustomForm;
function GetOwningForm(Component: TComponent): TCustomForm;
begin
result := NIL;
repeat
if Component is TCustomForm then
Result := TCustomForm(Component);
Component := Component.Owner;
until Component = nil;
end;
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
FButtonSCAction.SetSubComponent(true);
if not (csDesigning in ComponentState) then
begin
Form := GetOwningForm(Self);
if Form <> nil then
begin
FButtonSCActionList := TActionList.Create(Form);
FButtonSCActionList.FreeNotification(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
end;
end;
destructor TActionTester.Destroy;
begin
FreeAndNil( self.FButtonSCAction );
inherited;
end;
procedure TActionTester.ExecuteButtonShortcut(Sender: TObject);
begin
if assigned( self.OnClick ) then self.OnClick( self );
end;
procedure TActionTester.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FButtonSCActionList) and (Operation = opRemove) then
FButtonSCActionList := nil;
end;
procedure TActionTester.Paint;
begin
inherited;
self.Canvas.Brush.Color := clGreen;
self.Canvas.Brush.Style := bsSolid;
self.Canvas.FillRect( self.GetClientRect );
end;
end.
works like a charm! Major kudos to NGLN, David and Dalija!

Related

How to publish a subcomponent's property in a compound component?

In a compound component derived from TPanel I'm trying to publish a property whose only pourpose is that to sets and gets a linkage property of a subcomponent. Each time I add my compound component to a form, an access violation is raised:
Access violation at address 12612D86 in module 'MyRuntimePackage.bpl'. Read of address 00000080.
I've prepared a simplified example using a TLabel and its PopupMenu property but I still have the same problem when placing the compound component on a form/frame.
Runtime package:
uses
StdCtrls, Menus, ExtCtrls, Classes;
type
TTestCompoundComponent = class(TPanel)
private
FSubCmp : TLabel;
function GetLabelPopupMenu() : TPopupMenu;
procedure SetLabelPopupMenu(AValue : TPopupMenu);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
constructor Create(AOwner : TComponent); override;
destructor Destroy(); override;
published
property LabelPopupMenu : TPopupMenu read GetLabelPopupMenu write SetLabelPopupMenu;
end;
...
function TTestCompoundComponent.GetLabelPopupMenu() : TPopupMenu;
begin
Result := FSubCmp.PopupMenu;
end;
procedure TTestCompoundComponent.SetLabelPopupMenu(AValue : TPopupMenu);
begin
if(GetLabelPopupMenu() <> AValue) then
begin
if(GetLabelPopupMenu() <> nil)
then GetLabelPopupMenu().RemoveFreeNotification(Self);
FSubCmp.PopupMenu := AValue;
if(GetLabelPopupMenu() <> nil)
then GetLabelPopupMenu().FreeNotification(Self);
end;
end;
procedure TTestCompoundComponent.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if((AComponent = GetLabelPopupMenu()) AND (Operation = opRemove))
then SetLabelPopupMenu(nil);
end;
constructor TTestCompoundComponent.Create(AOwner : TComponent);
begin
inherited;
FSubCmp := TLabel.Create(nil);
FSubCmp.Parent := Self;
end;
destructor TTestCompoundComponent.Destroy();
begin
FSubCmp.Free;
inherited;
end;
Designtime package:
procedure Register;
begin
RegisterComponents('MyTestCompoundComponent', [TTestCompoundComponent]);
end;
#kobik's answer explains the root cause of the AV (accessing the FSubCmp.PopupMenu property before FSubCmp is created). However, your entire component code is overly complicated for what you are trying to achieve.
You should be setting your component as the TLabel's Owner, then you can remove your destructor completely. And you should also be calling FSubCmp.SetSubComponent(True) in your constructor (especially if you ever intend to expose the TLabel in the Object Inspector at a later time, so the user can customize its properties at design-time):
constructor TTestCompoundComponent.Create(AOwner : TComponent);
begin
inherited;
FSubCmp := TLabel.Create(Self);
FSubCmp.SetSubComponent(True);
FSubCmp.Parent := Self;
end;
Your Notification() method should be setting FSubCmp.PopupMenu := nil directly in response to opRemove, instead of calling SetLabelPopupMenu(nil). You already know the PopupMenu is assigned and that it is in progress of being destroyed, so the extra code to retrieve the PopupMenu (repeatedly), check it for nil, and call RemoveFreeNotification(), is all overkill for an opRemove operation:
procedure TTestCompoundComponent.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = LabelPopupMenu) then
FSubCmp.PopupMenu := nil;
end;
And your SetLabelPopupMenu() method is just an eyesore in general, with all those redundant calls to GetLabelPopupMenu(). Call it only one time and store the returned object pointer to a local variable that you can then use as needed:
procedure TTestCompoundComponent.SetLabelPopupMenu(AValue: TPopupMenu);
var
PM: TPopupMenu;
begin
PM := LabelPopupMenu;
if (PM <> AValue) then
begin
if (PM <> nil) then
PM.RemoveFreeNotification(Self);
FSubCmp.PopupMenu := AValue;
if (AValue <> nil) then
AValue.FreeNotification(Self);
end;
end;
However, your Notification() method is actually completely redundant and should be removed altogether. TLabel already calls FreeNotification() on its own PopupMenu property, and has its own Notification() implementation that will set the PopupMenu property to nil if the TPopupMenu object is freed. You don't need to handle this manually at all. And so, all of the extra code in SetLabelPopupMenu() is redundant and should be removed:
procedure TTestCompoundComponent.SetLabelPopupMenu(AValue: TPopupMenu);
begin
FSubCmp.PopupMenu := AValue;
end;
This also means the fix proposed by #kobik is redundant and can be removed as well 1:
function TTestCompoundComponent.GetLabelPopupMenu: TPopupMenu;
begin
Result := FSubCmp.PopupMenu;
end;
1: Unless you want to handle the case where a user decides to free your TLabel directly (which is foolish, and no one would ever really do that in practice, but it is still technically possible), then you would need Notification() to handle that situation (assigning your component as the TLabel's Owner will call FreeNotificatio() for you):
function TTestCompoundComponent.Notification(AComponent: TComponent; Opration: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = FSubCmp) then
FSubCmp := nil;
end;
function TTestCompoundComponent.GetLabelPopupMenu: TPopupMenu;
begin
if FSubCmp <> nil then
Result := FSubCmp.PopupMenu
else
Result := nil;
end;
That being said, here is a simplified version of your code:
uses
StdCtrls, Menus, ExtCtrls, Classes;
type
TTestCompoundComponent = class(TPanel)
private
FSubCmp: TLabel;
function GetLabelPopupMenu: TPopupMenu;
procedure SetLabelPopupMenu(AValue: TPopupMenu);
public
constructor Create(AOwner: TComponent); override;
published
property LabelPopupMenu: TPopupMenu read GetLabelPopupMenu write SetLabelPopupMenu;
end;
...
constructor TTestCompoundComponent.Create(AOwner : TComponent);
begin
inherited;
FSubCmp := TLabel.Create(Self);
FSubCmp.SetSubComponent(True);
FSubCmp.Parent := Self;
end;
function TTestCompoundComponent.GetLabelPopupMenu: TPopupMenu;
begin
Result := FSubCmp.PopupMenu;
end;
procedure TTestCompoundComponent.SetLabelPopupMenu(AValue: TPopupMenu);
begin
FSubCmp.PopupMenu := AValue;
end;
Or even just this:
uses
StdCtrls, Menus, ExtCtrls, Classes;
type
TTestCompoundComponent = class(TPanel)
private
FSubCmp: TLabel;
public
constructor Create(AOwner: TComponent); override;
published
property SubLabel: TLabel read FSubCmp;
end;
...
constructor TTestCompoundComponent.Create(AOwner : TComponent);
begin
inherited;
FSubCmp := TLabel.Create(Self);
FSubCmp.SetSubComponent(True);
FSubCmp.Parent := Self;
end;
In GetLabelPopupMenu(), FSubCmp is nil when Notification() receives an opInsert notification during construction before FSubCmp has been created. If FSubCmp is nil, referring to its PopupMenu property will cause the AV. So, you need to check for that in GetLabelPopupMenu(), eg:
if FSubCmp = nil then
Result := nil
else
Result := FSubCmp.PopupMenu;
Otherwise, change the order of the and logic in Notification() to this instead:
if (Operation = opRemove) and (AComponent = GetLabelPopupMenu())
If the condition (Operation = opRemove) is false, the right side condition will not be evaluated (short-circuit).

Delphi: delete inherited TStringGrid

I want to have a custom StringGrid element.
I created a class:
type
TClassStringGrid = class(TCustomControl)
...
with
constructor TClassStringGrid.Create(AOwner: TForm);
begin
inherited Create(nil);
myGroupBox1 := TGroupBox.Create(AOwner);
myGroupBox1.Parent := AOwner;
myStringGrid1 := TStringGrid.Create(self);
myStringGrid1.Parent := myGroupBox1;
myStringGrid1.Options := myStringGrid1.Options + [goEditing];
end;
destructor TClassStringGrid.Destroy;
begin
if myStringGrid1 <> nil then begin
FreeAndNil(myStringGrid1);
end;
if myGroupBox1 <> nil then begin
DestroyComponents;
FreeAndNil(myGroupBox1);
end;
// Call the parent class destructor
inherited;
end;
I created a class in Form1 and show it. It works. But if I put some value into the StringGrid (Form1) and then try to close Form1 I get an exception "the element has no parent window" in FreeAndNil(myStringGrid1);.
What is wrong by Destroy?
I would be thankfull for any information you can provide me.
Assuming you want to show a String grid in a Group box on this control, then this is how it should look like:
type
TMyStringGrid = class(TCustomControl)
private
FGroupBox: TGroupBox;
FStringGrid: TStringGrid;
public
constructor Create(AOwner: TComponent); override;
end;
constructor TMyStringGrid.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FGroupBox := TGroupBox.Create(Self);
FGroupBox.Parent := Self;
FStringGrid := TStringGrid.Create(Self);
FStringGrid.Parent := FGroupBox;
end;
In this manner, your newly designed control is owner and parent of the sub controls. Destruction is done automatically because of that.

How to set CreateParams after the constructor has run?

TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
end;
TForm2 = class(TForm)
private
FAppWindow: Boolean;
protected
procedure CreateParams(var Params: TCreateParams); override;
public
property AppWindow: Boolean read FAppWindow write FAppWindow;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Form2 := TForm2.Create(Self);
Form2.AppWindow := True;
Form2.Show;
end;
procedure TForm2.CreateParams(var Params: TCreateParams);
begin
inherited;
if FAppWindow then begin
Params.Style := Params.Style or WS_EX_APPWINDOW;
Params.WndParent := 0;
end;
end;
This doesn't work, because the window handle is created during the constructor of TForm, so CreateParams is run too early and FAppWindow is always False.
Writing a custom constructor also doesn't work since you have to eventually call the inherited constructor which creates the handle before you can save any data to the instance:
constructor TForm2.CreateAppWindow(AOwner: TComponent);
begin
inherited Create(AOwner);
FAppWindow := True;
end;
Is there a way to:
Delay the creation of the window handle?
Alter the window style after creation of the window handle?
Recreate the window handle after the constructor has run?
Some other option I haven't thought of, yet?
How can I change the style of a form from the "outside" of the class?
The simplest solution is to pass the parameter to the form in its constructor, rather than wait until it has finished being created.
That means you need to introduce a constructor for TForm2 that accepts as parameters whatever information you need to pass on in CreateParams.
Make a note of any state before you call the inherited constructor. Also, there's no need to set WS_EX_APPWINDOW when you are setting the owner to be zero.
The nice thing about Delphi is that a derived constructor DOES NOT have to call the inherited constructor as its first statement. So you can set your FAppWindow member first, THEN call the inherited constructor to stream the DFM and create the window, eg:
procedure TForm1.FormCreate(Sender: TObject);
begin
Form2 := TForm2.CreateAppWindow(Self);
Form2.Show;
end;
constructor TForm2.CreateAppWindow(AOwner: TComponent);
begin
FAppWindow := True;
inherited Create(AOwner);
end;
This seems to work to recreate the handle, I got the idea from the RecreateAsPopup VCL method:
procedure TForm2.SetAppWindow(const Value: Boolean);
begin
FAppWindow := Value;
if HandleAllocated then
RecreateWnd
else
UpdateControlState;
end;

What is the best way to add long press event to button class?

By Long Press, I mean pressing a button / panel and hold for a period (say 2 seconds) without releasing or dragging around. It is common in mobile phone and touch device.
I had tried using Gesture, checked toPressAndHold in TabletOptions and Checked all in InteractiveGestureOptions but long pressing cause no OnGesture Call.
Another implementation I can think of is adding a timer, start it in MouseDown and end it in either Timer Fired, StartDrag, MouseUp or MouseLeave. However, as I want to add this behavior to several different buttons and panel component, I would have to override a brunch of procedure in each class and copy the code around for each component.
Is there a better way of achieving that?
Edit :
To NGLN
Woo, great piece of work! Together with your answer to those scrolling effects, VCL can almost achieve mobile OS look and feel!
Your code work perfectly with common controls but I got 2 issues in my case
Long Clicking on the form cannot be detected (of cause as the form
is not parent of itself) I shift the Find FChild Code to separate
procedure and call from both WMParentNotify and FormMouseDown to
solve it.
I got some custom button which has some disabled HTML
labels (Header, Caption, Footer) covering up the label original
surface, Using your code, FChild will be one of those label but it
do not get MouseCapture. I add the below line to overcome it :
while not TControlAccess(FChild).Enabled do
FChild := FChild.Parent;
Finally, for some more complicated controls like TCategoryButtons or TListBox, the user of the event might need to check not against the whole control but a specify item in the control. So I think we need to save the original CursorPos and fire another event when the timer triggered to let manual determination of whether it meet the long press condition or not. If yes or event not assigned, then use your default code for determination.
All in all, we can just create a LongPress supported form / panel to host all other controls. This is much more easier then implementing the LongPress feature Component by Component! Great Thanks!
Edit2 :
To NGLN
Thanks again for your component version, which is excellent approach, not needing to do any modification to existing components and can detect long press everywhere!
For your information, I had do several modification to suit my own need.
TCustomForm vs TWinControl : As most of my application has only 1 main form and all other visual units are my own created frame (not from TFrame but TScrollingWinControl with ccpack support), assuming TCustomForm do not work for me. So I had deleted property form (but retain FForm for ActiveControl) and create a published property Host : TWinControl to act as the parent host. In that way, I can also limit the detection to some limited panel. When Assigning Host, I check and find the FForm using GetParentForm(FHost).
Disabled Controls : As I said previously, I got some disabled TJvHTLabel covering my buttons and your component work on the labels. I can of cause find back the button by the label, but I think it would be more convenient if it had been handled by the new component. So I add a property SkipDisabled and if set to turn, loop in its parent line to find first enabled control.
I add a PreserveFocus property to let component user choose to keep last activecontrol or not.
Controls with items. I changed your TLongPressEvent, adding the ClickPos as the 2nd parameter. So, I can now use the ClickPos to find which item in a list box or the like had been long held.
It seems to me that FindVCLWindow is having same effect with your FindControlAtPos?
Thank you again for your great work.
At every left mouse button click, WM_PARENTNOTIFY is send to all (grand) parents of the clicked control. So this can be used for tracking the starting point of a long press, and the duration of a press can be timed with a timer. What is left is to decide when a press should be called a long press. And to wrap this all up in a nice component of course.
In the component written below, the OnLongPress event handler is fired when the following conditions are met:
after the interval, the control still has mouse capture, or still has focus, or is disabled,
after the interval, the mouse has not moved more then Mouse.DragThreshold.
Some explanation on the code:
It temporarily replaces the control's OnMouseUp event handler, otherwise consecutive clicks might also result in a long press. The intermediate event handler disables the tracking timer, calls the original event handler and replaces it back.
After the long press, the active control is reset, because I thought a long press is not done with the intention to focus the control. But that's just my guess, and it might be candidate for a property.
Also tracks for long presses on the form itself (rather then only its childs).
Has a customized FindControlAtPos routine which performs a deep search on an arbitrary window. Alternatives were (1) TWinControl.ControlAtPos, but it searches just one level deep, and (2) Controls.FindDragTarget, but despite the AllowDisabled parameter, it is not able of finding disabled controls.
unit LongPressEvent;
interface
uses
Classes, Controls, Messages, Windows, Forms, ExtCtrls;
type
TLongPressEvent = procedure(Control: TControl) of object;
TLongPressTracker = class(TComponent)
private
FChild: TControl;
FClickPos: TPoint;
FForm: TCustomForm;
FOldChildOnMouseUp: TMouseEvent;
FOldFormWndProc: TFarProc;
FOnLongPress: TLongPressEvent;
FPrevActiveControl: TWinControl;
FTimer: TTimer;
procedure AttachForm;
procedure DetachForm;
function GetDuration: Cardinal;
procedure LongPressed(Sender: TObject);
procedure NewChildMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure NewFormWndProc(var Message: TMessage);
procedure SetDuration(Value: Cardinal);
procedure SetForm(Value: TCustomForm);
procedure StartTracking;
protected
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property Form: TCustomForm read FForm write SetForm;
published
property Duration: Cardinal read GetDuration write SetDuration
default 1000;
property OnLongPress: TLongPressEvent read FOnLongPress
write FOnLongPress;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TLongPressTracker]);
end;
function FindControlAtPos(Window: TWinControl;
const ScreenPos: TPoint): TControl;
var
I: Integer;
C: TControl;
begin
for I := Window.ControlCount - 1 downto 0 do
begin
C := Window.Controls[I];
if C.Visible and PtInRect(C.ClientRect, C.ScreenToClient(ScreenPos)) then
begin
if C is TWinControl then
Result := FindControlAtPos(TWinControl(C), ScreenPos)
else
Result := C;
Exit;
end;
end;
Result := Window;
end;
{ TLongPressTracker }
type
TControlAccess = class(TControl);
procedure TLongPressTracker.AttachForm;
begin
if FForm <> nil then
begin
FForm.HandleNeeded;
FOldFormWndProc := Pointer(GetWindowLong(FForm.Handle, GWL_WNDPROC));
SetWindowLong(FForm.Handle, GWL_WNDPROC,
Integer(MakeObjectInstance(NewFormWndProc)));
end;
end;
constructor TLongPressTracker.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FTimer := TTimer.Create(Self);
FTimer.Enabled := False;
FTimer.Interval := 1000;
FTimer.OnTimer := LongPressed;
if AOwner is TCustomForm then
SetForm(TCustomForm(AOwner));
end;
destructor TLongPressTracker.Destroy;
begin
if FTimer.Enabled then
begin
FTimer.Enabled := False;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
DetachForm;
inherited Destroy;
end;
procedure TLongPressTracker.DetachForm;
begin
if FForm <> nil then
begin
if FForm.HandleAllocated then
SetWindowLong(FForm.Handle, GWL_WNDPROC, Integer(FOldFormWndProc));
FForm := nil;
end;
end;
function TLongPressTracker.GetDuration: Cardinal;
begin
Result := FTimer.Interval;
end;
procedure TLongPressTracker.LongPressed(Sender: TObject);
begin
FTimer.Enabled := False;
if (Abs(FClickPos.X - Mouse.CursorPos.X) < Mouse.DragThreshold) and
(Abs(FClickPos.Y - Mouse.CursorPos.Y) < Mouse.DragThreshold) and
(((FChild is TWinControl) and TWinControl(FChild).Focused) or
(TControlAccess(FChild).MouseCapture or (not FChild.Enabled))) then
begin
FForm.ActiveControl := FPrevActiveControl;
if Assigned(FOnLongPress) then
FOnLongPress(FChild);
end;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
procedure TLongPressTracker.NewChildMouseUp(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
FTimer.Enabled := False;
if Assigned(FOldChildOnMouseUp) then
FOldChildOnMouseUp(Sender, Button, Shift, X, Y);
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
procedure TLongPressTracker.NewFormWndProc(var Message: TMessage);
begin
case Message.Msg of
WM_PARENTNOTIFY:
if TWMParentNotify(Message).Event = WM_LBUTTONDOWN then
StartTracking;
WM_LBUTTONDOWN:
StartTracking;
end;
with Message do
Result := CallWindowProc(FOldFormWndProc, FForm.Handle, Msg, WParam,
LParam);
end;
procedure TLongPressTracker.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FForm) and (Operation = opRemove) then
DetachForm;
if (AComponent = FChild) and (Operation = opRemove) then
begin
FTimer.Enabled := False;
FChild := nil;
end;
end;
procedure TLongPressTracker.SetDuration(Value: Cardinal);
begin
FTimer.Interval := Value;
end;
procedure TLongPressTracker.SetForm(Value: TCustomForm);
begin
if FForm <> Value then
begin
DetachForm;
FForm := Value;
FForm.FreeNotification(Self);
AttachForm;
end;
end;
procedure TLongPressTracker.StartTracking;
begin
FClickPos := Mouse.CursorPos;
FChild := FindControlAtPos(FForm, FClickPos);
FChild.FreeNotification(Self);
FPrevActiveControl := FForm.ActiveControl;
FOldChildOnMouseUp := TControlAccess(FChild).OnMouseUp;
TControlAccess(FChild).OnMouseUp := NewChildMouseUp;
FTimer.Enabled := True;
end;
end.
To get this component working, add it to a package, or use this runtime code:
...
private
procedure LongPress(Control: TControl);
end;
...
procedure TForm1.FormCreate(Sender: TObject);
begin
with TLongPressTracker.Create(Self) do
OnLongPress := LongPress;
end;
procedure TForm1.LongPress(Control: TControl);
begin
Caption := 'Long press occurred on: ' + Sender.ClassName;
end;

How to make subcomponent TAction-s available at design time?

In my custom component I created some TAction-s as subcomponents. They're all published, but I could not assign them at design time since they were not available through object inspector.
How do you make them "iterable" by the object inspector? I have tried to set the Owner of the actions to the Owner of the custom component (which is the hosting Form) to no success.
EDIT: It looks like Embarcadero changed Delphi IDE behaviour related with this problem. If you are using Delphi versions prior XE, you should use solution from my own answer. For XE and above, you should use solution from Craig Peterson.
EDIT: I've added my own answer that solves the problem, i.e. by creating a TCustomActionList instance in my custom component and setting its Owner to the hosting form (owner of the custom component). However I am not too happy with this solution, since I think the instance of TCustomActionList is kind of redundant. So I am still hoping to get better solution.
EDIT: Add code sample
uses
.., ActnList, ..;
type
TVrlFormCore = class(TComponent)
private
FCancelAction: TBasicAction;
FDefaultAction: TBasicAction;
FEditAction: TBasicAction;
protected
procedure DefaultActionExecute(ASender: TObject); virtual;
procedure CancelActionExecute(ASender: TObject); virtual;
procedure EditActionExecute(ASender: TObject); virtual;
public
constructor Create(AOwner: TComponent); override;
published
property DefaultAction: TBasicAction read FDefaultAction;
property CancelAction : TBasicAction read FCancelAction;
property EditAction : TBasicAction read FEditAction;
end;
implementation
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FDefaultAction := TAction.Create(Self);
with FDefaultAction as TAction do
begin
SetSubComponent(True);
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
FCancelAction := TAction.Create(Self);
with FCancelAction as TAction do
begin
SetSubComponent(True);
Caption := 'Cancel';
OnExecute := Self.CancelActionExecute;
end;
FEditAction := TAction.Create(Self);
with FEditAction as TAction do
begin
SetSubComponent(True);
Caption := 'Edit';
OnExecute := Self.EditActionExecute;
end;
end;
As far as I can tell you're not supposed to do it that way.
The easy way to do what you want is to create new standalone actions that can work with any TVrlFormCore component and set the target object in the HandlesTarget callback. Take a look in StdActns.pas for examples. The actions won't be available automatically when sommeone drops your component on the form, but they can add them to their action list manually using the New Standard Actions... command. There's a good article on registering standard actions here.
If you really want to auto-create the actions you need to set the action Owner property to the form and you need to set the Name property. That's all that's necessary, but it does introduce a bunch of issues you need to work around:
The form owns the actions so it will add them its declaration's published section and will auto-create them as part of the streaming process. To work around that you can just disable streaming by overwriting the action's WriteState method and skip the inherited behavior.
Since you aren't writing the state, none of the properties will be persisted. To avoid confusing your users you should switch make the actions descend from TCustomAction instead of TAction, so it doesn't expose anything. There may be way to make the action stream properly, but you didn't say whether it was necessary.
You need to register for free notifications in case the form frees the action before you can.
If someone drops more than one of your component on the action names will conflict. There's multiple ways to handle that, but the cleanest would probably be to override the component's SetName method and use its name as a prefix for the actions' names. If you do that you need to use RegisterNoIcon with the new class so they don't show up on the form.
In the IDE's Structure pane the actions will show up directly under the form, rather than nested like ActionList shows. I haven't found a way around that; none of SetSubComponent, GetParentComponent/HasParent, or GetChildren have any effect, so this may be hard-coded behavior. You can delete the action from the structure pane, separate from the component, too.
I'm sure it can be improved, but this works without any custom property editors:
type
TVrlAction = class(TCustomAction)
protected
procedure WriteState(Writer: TWriter); override;
end;
TVrlFormCore = class(TComponent)
private
FDefaultAction: TVrlAction;
protected
procedure DefaultActionExecute(ASender: TObject); virtual;
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
procedure SetName(const NewName: TComponentName); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
public
property DefaultAction: TVrlAction read FDefaultAction;
end;
procedure Register;
implementation
// TVrlAction
procedure TVrlAction.WriteState(Writer: TWriter);
begin
// No-op
end;
// TVrlFormCore
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FDefaultAction := TVrlAction.Create(AOwner);
with FDefaultAction do
begin
FreeNotification(Self);
Name := 'DefaultAction';
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
end;
destructor TVrlFormCore.Destroy;
begin
FDefaultAction.Free;
inherited;
end;
procedure TVrlFormCore.DefaultActionExecute(ASender: TObject);
begin
end;
procedure TVrlFormCore.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if Operation = opRemove then
if AComponent = FDefaultAction then
FDefaultAction := nil;
end;
procedure TVrlFormCore.SetName(const NewName: TComponentName);
begin
inherited;
if FDefaultAction <> nil then
FDefaultAction.Name := NewName + '_DefaultAction';
end;
procedure Register;
begin
RegisterComponents('Samples', [TVrlFormCore]);
RegisterNoIcon([TVrlAction]);
end;
EDIT: Use this solution for Delphi versions prior to Delphi XE. For XE and later, use Craig Peterson answer (which does not require redundant TCustomActionList instance).
After meddling around and using information from Craig Peterson's answer, I've decided to instantiate a TCustomActionList in my custom component. So far it is the only way to get list of actions in Object Inspector.
Here is the code:
uses
..., ActnList, ...;
type
TVrlAction=class(TCustomAction)
protected
procedure WriteState(Writer: TWriter); override;
published
property Caption;
end;
TVrlActionList=class(TCustomActionList)
protected
procedure WriteState(Writer: TWriter); override;
end;
TVrlFormCore = class(TVrlItemSource)
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
procedure SetName(const NewName: TComponentName); override;
public
constructor Create(AOwner: TComponent); override;
end;
implementation
{ TVrlAction }
procedure TVrlAction.WriteState(Writer: TWriter);
begin
end;
{ TVrlActionList }
procedure TVrlActionList.WriteState(Writer: TWriter);
begin
end;
{ TVrlFormCore }
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FActions := TVrlActionList.Create(AOwner);
FDefaultAction := TVrlAction.Create(AOwner);
with FDefaultAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
FActions.AddAction(TContainedAction(FDefaultAction));
FCancelAction := TVrlAction.Create(AOwner);
with FCancelAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'Cancel';
OnExecute := Self.CancelActionExecute;
end;
FActions.AddAction(TContainedAction(FCancelAction));
FEditAction := TVrlAction.Create(AOwner);
with FEditAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'Edit';
OnExecute := Self.EditActionExecute;
end;
FActions.AddAction(TContainedAction(FEditAction));
end;
procedure TVrlFormCore.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if Operation=opRemove then
begin
if AComponent = FMaster then
FMaster := nil
else if (AComponent is TVrlFormCore) then
FDetails.Remove(TVrlFormCore(AComponent))
else if AComponent=FDefaultAction then
FDefaultAction := nil
else if AComponent=FCancelAction then
FCancelAction := nil
else if AComponent=FEditAction then
FEditAction := nil;
end;
end;
procedure TVrlFormCore.SetName(const NewName: TComponentName);
begin
inherited;
if FActions<>nil then
FActions.Name := NewName + '_Actions';
if FDefaultAction <> nil then
FDefaultAction.Name := NewName + '_DefaultAction';
if FCancelAction <> nil then
FCancelAction.Name := NewName + '_CancelAction';
if FEditAction <> nil then
FEditAction.Name := NewName + '_EditAction';
end;
You cannot assign them because they are read only by design:
property DefaultAction: TBasicAction read FDefaultAction;
property CancelAction : TBasicAction read FCancelAction;
property EditAction : TBasicAction read FEditAction;
You should change your class' interface to:
property DefaultAction: TBasicAction read FDefaultAction write FDefaultAction;
property CancelAction : TBasicAction read FCancelAction write FCancelAction;
property EditAction : TBasicAction read FEditAction write FEditAction;
or write appropriate setter for each action.
Edit:
What you need is then
to implement your 3 custom actions as Predefined Actions (See StdActns.pas for samples).
to register them by calling ActnList.RegisterActions. (See RAD Studio documentation)
to add to the form a TActionList and/or TActionManager to allow your Predefined Actions appear in the list of predefined actions in the action list editor of every TControl's descendent.
You may do extensive search on google for the topic and find some concrete example.

Resources