In a Delphi 11 32-bit VCL Application in Windows 10, at run-time, I right-click a control while holding down the SHIFT and CTRL modifier keys, to copy the name of the clicked control to the clipboard:
procedure TformMain.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
begin
case Msg.message of
Winapi.Messages.WM_RBUTTONDOWN:
begin
// Detect the name of the clicked control:
var ThisControl: Vcl.Controls.TWinControl;
ThisControl := Vcl.Controls.FindControl(Msg.hwnd);
if Assigned(ThisControl) then
begin
var keys: TKeyboardState;
GetKeyboardState(keys);
// when right-clicking a control, hold down the SHIFT and CTRL key to escape the possible default click behavior of the control:
if (keys[VK_SHIFT] and $80 <> 0) and (keys[VK_CONTROL] and $80 <> 0) then
begin
Handled := True;
//CodeSite.Send('TformMain.ApplicationEvents1Message: ThisControl.Name', ThisControl.Name);
Vcl.Clipbrd.Clipboard.AsText := ThisControl.Name;
end;
end;
end;
end;
end;
This works with ALMOST all controls, EXCEPT with Timage and TLabel (and possibly a few other control types). How can I make this work with Timage and TLabel too?
TImage and TLabel are derived from TGraphicControl, not TWinControl. They do not have an HWND of their own, which is why Vcl.Controls.FindControl() does not work for them. You are receiving WM_RBUTTONDOWN messages belonging to their Parent's HWND instead. Internally, when the VCL routes the message, it will account for graphical child controls. But your code is not.
Try Vcl.Controls.FindDragTarget() instead. It takes screen coordinates as input (which you can get by translating the client coordinates in WM_RBUTTONDOWN's lParam using Winapi.ClientToScreen() or Winapi.MapWindowPoints()), and then returns the TControl at those coordinates, so it works with both windowed and graphical controls.
That being said, you don't need to use Winapi.GetKeyboardState() in this situation, as WM_RBUTTONDOWN's wParam tells you whether SHIFT and CTRL keys were held down at the time the message was generated (remember, you are dealing with queued messages, so there is a delay between the time the message is generated and the time you receive it).
procedure TformMain.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
const
WantedFlags = MK_SHIFT or MK_CONTROL;
begin
if Msg.message = WM_RBUTTONDOWN then
begin
// Detect the name of the clicked control:
var Pt: TPoint := SmallPointToPoint(TSmallPoint(Msg.LParam));
Windows.ClientToScreen(Msg.hwnd, Pt);
var ThisControl: TControl := FindDragTarget(Pt, True);
if Assigned(ThisControl) then
begin
// when right-clicking a control, hold down the SHIFT and CTRL key to escape the possible default click behavior of the control:
if (Msg.wParam and WantedFlags) = WantedFlags then
begin
Handled := True;
//CodeSite.Send('TformMain.ApplicationEvents1Message: ThisControl.Name', ThisControl.Name);
Clipboard.AsText := ThisControl.Name;
end;
end;
end;
end;
Related
In Delphi 10.4, in a VCL Application, using the OnMessage event-handler of a TApplicationEvents component, I increase the font-size of the right-clicked Control:
procedure TformMain.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
var
ThisControl: TControl;
begin
if (Msg.Message = WM_RBUTTONDOWN) then
begin
ThisControl := FindDragTarget(Mouse.CursorPos, True);
CodeSite.Send('TformMain.ApplicationEvents1Message: RIGHTCLICK!', ThisControl.Name);
if ThisControl is TLabel then
TLabel(ThisControl).Font.Size := TLabel(ThisControl).Font.Size + 1
else if ThisControl is TCheckBox then
TCheckBox(ThisControl).Font.Size := TCheckBox(ThisControl).Font.Size + 1;
// ETC. ETC. ETC.! :-(
end;
end;
This is an extremely INEFFICIENT way to make this work for all Control Types because I would have to enumerate all existent Control Types, as TControl does not have a TFont property.
A better way would be to get the TFont property of the control without having to ask for the TYPE and then having to TYPECAST the Control.
But HOW?
If you redeclare the type, you get access to the protected properties of the class.
Nowadays you do that with an interposer class, but I'm still used to the old ways.
You might have to add a check, if it turns out that a particular control bombs, when you do something with the font. It has always worked for me so far.
type
TCrackControl = class(TControl);
procedure TformMain.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
var
ThisControl: TCrackControl;
begin
if (Msg.Message = WM_RBUTTONDOWN) then
begin
ThisControl := TCrackControl(FindDragTarget(Mouse.CursorPos, True));
CodeSite.Send('TformMain.ApplicationEvents1Message: RIGHTCLICK!', ThisControl.Name);
If assigned(ThisControl.Font) then
ThisControl.Font.Size := ThisControl.Font.Size + 1;
end;
end;
Using Delphi Tokyo and FireMonkey:
I have a lot of different frames on a form and would like to set some form-level variables as the focus on the form changes in and out of the different frames.
Ex. I have a Insert button on the form and want to enable it if the frame the user is in allows inserts and then again disable it upon leaving the frame's focus.
There are OnEnter and OnExit events on the frame, but they never execute.
Obviously there are edits etc. on the frames.
type
TForm1 = class(TForm)
Label1: TLabel;
procedure FormFocusChanged(Sender: TObject);
private
FFocusedFrame: TFrame;
public
{ Public declarations }
end;
...
procedure TForm1.FormFocusChanged(Sender: TObject);
var
LParent: TFmxObject;
begin
if Focused <> nil then
begin
LParent := Focused.GetObject.Parent;
while (LParent <> nil) and not (LParent is TFrame) do
LParent := LParent.Parent;
if (LParent <> nil) and (FFocusedFrame <> LParent) then
begin
FFocusedFrame := TFrame(LParent);
Label1.Text := FFocusedFrame.Name;
end;
end;
end;
end.
No need to hook up OnEnter and OnExit for every control
The frames can not receive focus, and therefore they do not fire OnEnter() or OnExit() events.
After you have placed a frame on the form, you can create two common event handlers for all edit controls (or other input controls on the frame)
procedure TForm14.Frame112EditExit(Sender: TObject);
begin
Button1.Enabled := False;
end;
procedure TForm14.Frame112EditEnter(Sender: TObject);
begin
Button1.Enabled := True;
end;
and link the OnEnter() and OnExit() events of all those edit controls to these two event handlers.
I was unsure whether the events are fired in correct order when moving from one edit control to anotherone, but a short test (on Windows) shows that OnExit() of the control we leave is fired before OnEnter() of the control we enter, as expected.
I'm trying to stop a TMemo (and also TRichEdit) control from eating Escape keys.
If the user is focused in a TEdit, pressing Escape will trigger the form to do what the form does when the user presses escape. If the user is focused in a TMemo, pressing escape is eaten by the TMemo.
Of course i could do the hack:
procedure TForm1.Memo1KeyPress(Sender: TObject; var Key: Char);
begin
if Key = #27 then
begin
//figure out how to send a key to the form
end;
end;
But that is not ideal (i have to handle the escape key, rather than letting the form handle it).
Of course i could do the hack:
Form1.KeyPreview := True;
procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
if Key = #27 then
begin
//Figure out how to invoke what the form was going to do when the user presses escape
end;
end;
But that is not ideal (i have to handle the escape key, rather than letting the form handle it).
So we'll answer the question rather than the problem
Instead we'll take this opportunity to learn something. How is it that a TMemo is even receiving a keyPress event associated with the escape key, when a TEdit doesn't:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if Key = #27 then
begin
//never happens
end;
end;
The TEdit and TMemo are the same Windows EDIT common control.
Why does escape bypass the form's KeyPreview
If i turn on the form's KeyPreview, and the user presses Escape while focused in a TEdit box, and a button's Cancel property is set, the form closes and:
the Edit1.KeyPress event is not triggered
the Form1.KeyPress event is not triggered
If an Action is created, whose Shortcut is Esc, then no KeyPress event is raised, no matter what control the user is focused in.
tl;dr: Where is the TMemo.WantEscape property?
The behaviour you observe is controlled by the handling of the WM_GETDLGCODE message. For a memo that looks like this:
procedure TCustomMemo.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
inherited;
if FWantTabs then Message.Result := Message.Result or DLGC_WANTTAB
else Message.Result := Message.Result and not DLGC_WANTTAB;
if not FWantReturns then
Message.Result := Message.Result and not DLGC_WANTALLKEYS;
end;
For an edit control the VCL does not implement special handling for WM_GETDLGCODE and the underlying Windows edit control handles it.
In a standard Win32 app the Windows dialog manager sends the WM_GETDLGCODE messages. But Delphi is not built on top of the dialog manager, and so the VCL is in charge of sending WM_GETDLGCODE. It does so in the CN_KEYDOWN handler. The code looks like this:
Mask := 0;
case CharCode of
VK_TAB:
Mask := DLGC_WANTTAB;
VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN:
Mask := DLGC_WANTARROWS;
VK_RETURN, VK_EXECUTE, VK_ESCAPE, VK_CANCEL:
Mask := DLGC_WANTALLKEYS;
end;
if (Mask <> 0) and
(Perform(CM_WANTSPECIALKEY, CharCode, 0) = 0) and
(Perform(WM_GETDLGCODE, 0, 0) and Mask = 0) and
(GetParentForm(Self).Perform(CM_DIALOGKEY,
CharCode, KeyData) <> 0) then Exit;
Notice that VK_RETURN, VK_EXECUTE, VK_ESCAPE and VK_CANCEL are all lumped together. This means that a VCL control has to decide whether or not to process these keys itself, or let the form handle them in its CM_DIALOGKEY handler.
As you can see from TCustomMemo.WMGetDlgCode you can influence that choice with the WantReturns property. So, you can persuade the VCL to let the form handle ESC by simply setting WantReturns on the memo to False. But that also stops the ENTER key reaching memo and makes it rather tricky for the user of the memo to enter new lines. They have to do it with CTRL + ENTER.
In fact WantReturns should really have been named WantReturnsAndEscapesAndExecutesAndCtrlBreaks. The VCL designers could have implemented a WantEscapes property but it's just not there.
So you are left handling it yourself one way or another. Personally, I do so with my own derived memo control. It overrides the KeyDown method and does this:
procedure TMyMemo.KeyDown(var Key: Word; Shift: TShiftState);
var
Form: TCustomForm;
Message: TCMDialogKey;
begin
inherited;
if (Key=VK_ESCAPE) and (Shift*[ssShift..ssCtrl])=[]) then begin
Form := GetParentForm(Self);
if Assigned(Form) then begin
// we need to dispatch this key press to the form so that it can 'press'
// any buttons with Cancel=True
Message.Msg := CM_DIALOGKEY;
Message.CharCode := VK_ESCAPE;
Message.KeyData := 0;
Message.Result := 0;
Form.Dispatch(Message);
end;
end;
end;
Another way to achieve this is to handle CM_WANTSPECIALKEY and WM_GETDLGCODE. Here's a crude interposer that illustrates the technique:
type
TMemo = class(StdCtrls.TMemo)
protected
procedure CMWantSpecialKey(var Msg: TCMWantSpecialKey); message CM_WANTSPECIALKEY;
procedure WMGetDlgCode(var Msg: TWMGetDlgCode); message WM_GETDLGCODE;
end;
procedure TMemo.CMWantSpecialKey(var Msg: TCMWantSpecialKey);
begin
case Msg.CharCode of
VK_ESCAPE:
Msg.Result := 0;
VK_RETURN, VK_EXECUTE, VK_CANCEL:
Msg.Result := 1;
else
inherited;
end;
end;
procedure TMemo.WMGetDlgCode(var Msg: TWMGetDlgCode);
begin
inherited;
Msg.Result := Msg.Result and not DLGC_WANTALLKEYS;
end;
I want to restrict users (based on special condition) to open a tab or not in a page control. ie, the user can click on the tab but it will not be displayed to him. Instead, a message will show to him that "he don't have the access right to see such tab".
On what event I should write the checking code, and what tab property (of TPageControl component) will allow/block user to enter such tab?
In an ideal world you would set AllowChange to False from theOnChanging event to block a page change. However, this does not appear to be viable because I can find no way of discerning, from within OnChanging, which page the user is trying to select.
Even looking at the underlying Windows notification seems to offer little hope. The TCN_SELCHANGING notification identifies the control, but not says nothing about the pages involved, so far as I can tell.
The best I can come up with is to use OnChanging to note the current active page and then do the hard work in OnChange. If the selected page has been changed to something undesirable, then just change it back.
procedure TForm1.PageControl1Changing(Sender: TObject; var AllowChange: Boolean);
begin
FPreviousPageIndex := PageControl1.ActivePageIndex;
end;
procedure TForm1.PageControl1Change(Sender: TObject);
begin
if PageControl1.ActivePageIndex=1 then begin
PageControl1.ActivePageIndex := FPreviousPageIndex;
Beep;
end;
end;
Rather messy I know, but it has the virtue of working!
The OnChanging event does not allow you to determine which tab is being selected, because Windows itself does not report that information. What you can do, however, is subclass the TPageControl.WindowProc property to intercept messages that are sent to the TPageControl before it processes them. Use mouse messages to determine which tab is being clicked on directly (look at the TPageControl.IndexOfTabAt() method), and use keyboard messages to detect left/right arrow presses to determine which tab is adjacent to the active tab (look at the TPageControl.FindNextPage() method).
Use the OnChanging event of the page control.
procedure TForm1.PageControl1Changing(Sender: TObject; var AllowChange: Boolean);
begin
if (self.PageControl1.TabIndex= 1)and
(NotAllowUser = 'SomePerson') then
begin
AllowChange:= False;
ShowMessage('Person not allow for this Tab');
end;
end;
Ok, the PageControle1.TabIndex is the activepageindex and not the one i want to select.
How can i get the clicked Page.
procedure TForm1.PageControl1Changing(Sender: TObject; var AllowChange: Boolean);
var
P: TPoint;
NewTabIndex: Integer;
begin
P := PageControl1.ScreenToClient(Mouse.CursorPos);
NewTabIndex := PageControl1.IndexOfTabAt(P.X, P.y);
if (NewTabIndex= 1) then
begin
AllowChange:= false;
Beep
end;
end;
New Attempt
TMyPageControl = Class(TPageControl)
private
FNewTabSheet: TTabSheet;
FOnMyChanging: TMyTabChangingEvent;
procedure SetOnMyChanging(const Value: TMyTabChangingEvent);
procedure CNNotify(var Message: TWMNotify); message CN_NOTIFY;
procedure CMDialogKey(var Message: TCMDialogKey); message CM_DIALOGKEY;
protected
function CanChange: Boolean; Override;
public
property OnMyChanging: TMyTabChangingEvent read FOnMyChanging write SetOnMyChanging;
End;
{ TMyPageControl }
function TMyPageControl.CanChange: Boolean;
begin
Result := True;
if Assigned(FOnMyChanging) then FOnMyChanging(Self, FNewTabSheet ,Result);
end;
procedure TMyPageControl.CMDialogKey(var Message: TCMDialogKey);
begin
if (Focused or Windows.IsChild(Handle, Windows.GetFocus)) and
(Message.CharCode = VK_TAB) and (GetKeyState(VK_CONTROL) < 0) then
begin
FNewTabSheet := FindNextPage(ActivePage, GetKeyState(VK_SHIFT) >= 0,True);
SelectNextPage(GetKeyState(VK_SHIFT) >= 0);
Message.Result := 1;
end else
inherited;
end;
procedure TMyPageControl.CNNotify(var Message: TWMNotify);
var
P: TPoint;
NewTabIndex: Integer;
begin
with Message do
case NMHdr.code of
TCN_SELCHANGE:
Change;
TCN_SELCHANGING:
begin
Result := 1;
P := self.ScreenToClient(Mouse.CursorPos);
NewTabIndex := self.IndexOfTabAt(P.X, P.y);
FNewTabSheet:= self.Pages[NewTabIndex];
if CanChange then Result := 0;
end;
end;
end;
procedure TMyPageControl.SetOnMyChanging(const Value: TMyTabChangingEvent);
begin
FOnMyChanging := Value;
end;
You can show tab and effectively disable changing in OnChanging event of TPageControl. All you need to do is set AllowChange var to False.
procedure TForm1.PageControl1(Sender: TObject; var AllowChange: Boolean);
begin
AllowChange := MyCondition;
if MyCondition
ShowMessage('User doesn''t have permission to see this tab.');
end
Sometimes it is better just to hide unwanted TabSheets with something like this:
TabSheetNN.TabVisible:=Somecondition;
than trying to prevent switching to these tabs.
Sure, it would be better if Sender in OnChanging event will be TabSheet , not TPageControl.
So I have a TMenuItem attached to a TAction on a TPopupMenu for a TDBGrid (actually 3rd party, but you get the idea). Based on the selected row in the grid, the TAction is enabled or disabled. What I want is to be able to display a hint to the user explaining why the item is disabled.
As far as why I want a hint on a disabled menu item, lets just say I am in agreement with Joel.
All TMenuItem's have a hint property, but as best I can tell they are only used the the TApplicationEvent.OnHint event handler to stick the hint in a TStatusBar or some other special processing. I found an article on how to create your own even window for a TMainMenu's TMenuItems, but it doesn't work on a TPopupMenu's TMenuItem. It works by handling the WM_MENUSELECT message, which as far as I can tell is not sent on a TPopupMenu.
WM_MENUSELECT is indeed handled for menu items in popup menus also, but not by the windows proc of the form containing the (popup) menu, but by an invisible helper window created by Menus.PopupList. Luckily you can (at least under Delphi 5) get at this HWND via Menus.PopupList.Window.
Now you can use the old-fashioned way to subclass a window, as described for example in this CodeGear article, to handle WM_MENUSELECT also for popup menus. The HWND will be valid from after the first TPopupMenu is created to before the last TPopupMenu object is destroyed.
A quick test with the demo app in the linked article in the question should reveal whether this is going to work.
Edit: It does indeed work. I changed the linked example to show hints also for the popup menu. Here are the steps:
Add a handler for OnDestroy, a member variable for the old window proc and a method for the new window proc to the form:
TForm1 = class(TForm)
...
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure ApplicationEvents1Hint(Sender: TObject);
private
miHint : TMenuItemHint;
fOldWndProc: TFarProc;
procedure WMMenuSelect(var Msg: TWMMenuSelect); message WM_MENUSELECT;
procedure PopupListWndProc(var AMsg: TMessage);
end;
Change the OnCreate handler of the form to subclass the hidden PopupList window, and implement the proper restoration of the window proc in the OnDestroy handler:
procedure TForm1.FormCreate(Sender: TObject);
var
NewWndProc: TFarProc;
begin
miHint := TMenuItemHint.Create(self);
NewWndProc := MakeObjectInstance(PopupListWndProc);
fOldWndProc := TFarProc(SetWindowLong(Menus.PopupList.Window, GWL_WNDPROC,
integer(NewWndProc)));
end;
procedure TForm1.FormDestroy(Sender: TObject);
var
NewWndProc: TFarProc;
begin
NewWndProc := TFarProc(SetWindowLong(Menus.PopupList.Window, GWL_WNDPROC,
integer(fOldWndProc)));
FreeObjectInstance(NewWndProc);
end;
Implement the subclassed window proc:
procedure TForm1.PopupListWndProc(var AMsg: TMessage);
function FindItemForCommand(APopupMenu: TPopupMenu;
const AMenuMsg: TWMMenuSelect): TMenuItem;
var
SubMenu: HMENU;
begin
Assert(APopupMenu <> nil);
// menuitem
Result := APopupMenu.FindItem(AMenuMsg.IDItem, fkCommand);
if Result = nil then begin
// submenu
SubMenu := GetSubMenu(AMenuMsg.Menu, AMenuMsg.IDItem);
if SubMenu <> 0 then
Result := APopupMenu.FindItem(SubMenu, fkHandle);
end;
end;
var
Msg: TWMMenuSelect;
menuItem: TMenuItem;
MenuIndex: integer;
begin
AMsg.Result := CallWindowProc(fOldWndProc, Menus.PopupList.Window,
AMsg.Msg, AMsg.WParam, AMsg.LParam);
if AMsg.Msg = WM_MENUSELECT then begin
menuItem := nil;
Msg := TWMMenuSelect(AMsg);
if (Msg.MenuFlag <> $FFFF) or (Msg.IDItem <> 0) then begin
for MenuIndex := 0 to PopupList.Count - 1 do begin
menuItem := FindItemForCommand(PopupList.Items[MenuIndex], Msg);
if menuItem <> nil then
break;
end;
end;
miHint.DoActivateHint(menuItem);
end;
end;
This is done for all popup menus in a loop, until the first matching item or submenu is found.
Not sure if it helps, but I have created my own multi-line hint window (for Delphi7) to be able to show more then just one line of text.
It's open source and you can find it here.
There is some work involved showing it on the right location on the screen, but you have full control over it.