How does TMemo eat an escape key, when TEdit doesn't? - delphi

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;

Related

How to make TImage and TLabel receive WM_RBUTTONDOWN messages?

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;

Delphi XE2: OnHideHint event to automatically restore the previous text in the StatusBar?

TAction has a OnHint event, but unfortunately not a OnHideHint event. This has the following drawback:
I have ToolButtons and other controls associated with actions. Whenever the mouse is over such a control, the hint of the Action is shown in the StatusBar; (I have set the StatusBar's AutoHint property to True). But when the mouse leaves the ToolButton, the previous custom text in the StatusBar (which is not from a hint) is NOT automatically restored!
Now I could write an OnMouseLeave event handler for each and every control on the form to restore my custom text in the StatusBar, but this would be cumbersome!
Isn't there something which automatically restores the previous text in the StatusBar?
An OnHideHint event in TAction would be ideal!
That is default behavior, When AutoHint is True, the status bar automatically responds to hint actions by displaying the long version of the hint's text in the first panel.
The issue that you are having is that when you leave a control with your mouse, you are basically entering another window, it's parent control. And because that parent has no Hint string value assigned to it, the HintAction is updated to an empty string.
If you want to return the default value when there is no hint to display then drop a TApplicationEvents component on the form and use the TApplication.OnHint event like this:
var
OriginalPanelText : String = 'BLA';
procedure TForm1.ApplicationEvents1Hint(Sender: TObject);
begin
if StatusBar1.SimplePanel or (StatusBar1.Panels.Count = 0)
then
if Application.Hint <> ''
then
StatusBar1.SimpleText := Application.Hint
else
StatusBar1.SimpleText := OriginalPanelText
else
if Application.Hint <> ''
then
StatusBar1.Panels[0].Text := Application.Hint
else
StatusBar1.Panels[0].Text := OriginalPanelText;
end;
The AutoHint magic all happens in TStatusBar.ExecuteAction. When the hint stops showing that code sets the status bar text to be empty. You could modify the behaviour like this:
type
TStatusBar = class(ComCtrls.TStatusBar)
private
FRestoreTextAfterHintAction: string;
public
function ExecuteAction(Action: TBasicAction): Boolean; override;
end;
function TStatusBar.ExecuteAction(Action: TBasicAction): Boolean;
var
HintText: string;
begin
if AutoHint and not (csDesigning in ComponentState) and
(Action is THintAction) and not DoHint then begin
HintText := THintAction(Action).Hint;
if SimplePanel or (Panels.Count=0) then begin
if HintText='' then begin
SimpleText := FRestoreTextAfterHintAction;
end else begin
FRestoreTextAfterHintAction := SimpleText;
SimpleText := HintText;
end;
end else begin
if HintText='' then begin
Panels[0].Text := FRestoreTextAfterHintAction;
end else begin
FRestoreTextAfterHintAction := Panels[0].Text;
Panels[0].Text := HintText;
end;
end;
Result := True;
end else begin
Result := inherited ExecuteAction(Action);
end;
end;
I've used a rather crude interposer class and a brittle instance variable to store the text to be restored. You could tart this up to be a little more robust if you wish. The code above at least shows you the place you need to add your hooks.

Make two TEdits exclusive

I have two TEdit boxes that I am using to specify file paths, one is for UNC paths, the other is for a local path. However, I would like it so if the user can only enter text in one box. If they enter text in one box, it should clear the other one. How should I go about doing this? Also, not sure if I should use an OnEnter, OnChange, or some other method.
You can do it pretty simply. Create one OnChange handler, and assign it to both TEdits using the Object Inspector's Events tab. Then you can use something like the following:
procedure TForm1.EditChanged(Sender: TObject); //Sender is the edit being changed
begin
if Sender = UNCEdit then // If it's is the UNCEdit being changed
begin
LocalPathEdit.OnChange := nil; // Prevent recursive calling!
LocalPathEdit.Text := ''; // Clear the text
LocalPathEdit.OnChange := EditChanged; // Restore the event handler
end;
else
begin
UNCEdit.OnChange := nil;
UNCEdit.Text := '';
UNCEdit.OnChange := EditChanged;
end;
end;
This can be streamlined slightly, but it's not quite as readable to others. It can also be protected with a try..finally, although for simply clearing an edit's text content it's not really needed.
procedure TForm1.EditChanged(Sender: TObject);
var
TmpEdit: TEdit;
begin
if Sender = UNCEdit then
TmpEdit := LocalPathEdit
else
TmpEdit := UNCEdit;
TmpEdit.OnChange := nil;
try
TmpEdit.Text := '';
finally
TmpEdit.OnChange := EditChanged;
end;
end;
If you want to keep the two edit boxes, this is how I would do it.
procedure TForm1.Edit1Exit(Sender: TObject);
begin
if (Edit1.text <> '') then
Edit2.text:= '';
end;
procedure TForm1.Edit2Exit(Sender: TObject);
begin
if (Edit2.text <> '') then
Edit1.text:= '';
end;
You want the value check so that you don't accidentally wipe the value when your users tab through the fields.
You could hook both edit boxes to the following KeyPress event
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
If Sender = Edit1 then
Edit2.clear
else
if Sender = Edit2 then
Edit1.clear;
end;

How to allow or forbid user to enter tab in pagecontrol?

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.

How to trigger action when use mouse middle click on TPopupMenu's items?

We use mouse left click to trigger actions in menu items of TPopupMenu. How to trigger different action on mouse middle click in these menu items? In other word, mouse left and middle click on TPopupmenu's menu items are both different action.
The global Menus.PopupList variable keeps track of all PopupMenus and handles all massages send to them. You can override this PopupList with your own instance, as follows:
type
TMyPopupList = class(TPopupList)
private
FMenuItem: TMenuItem;
protected
procedure WndProc(var Message: TMessage); override;
end;
{ TMyPopupList }
procedure TMyPopupList.WndProc(var Message: TMessage);
var
FindKind: TFindItemKind;
I: Integer;
Item: Integer;
Action: TBasicAction;
Menu: TMenu;
begin
case Message.Msg of
WM_MENUSELECT:
with TWMMenuSelect(Message) do
begin
FindKind := fkCommand;
if MenuFlag and MF_POPUP <> 0 then
FindKind := fkHandle;
for I := 0 to Count - 1 do
begin
if FindKind = fkHandle then
begin
if Menu <> 0 then
Item := GetSubMenu(Menu, IDItem)
else
Item := -1;
end
else
Item := IDItem;
FMenuItem := TPopupMenu(Items[I]).FindItem(Item, FindKind);
if FMenuItem <> nil then
Break;
end;
end;
WM_MBUTTONUP:
if FMenuItem <> nil then
begin
GetMenuItemSecondAction(FMenuItem, Action);
Menu := FMenuItem.GetParentMenu;
if Action <> nil then
begin
Menu := FMenuItem.GetParentMenu;
SendMessage(Menu.WindowHandle, WM_IME_KEYDOWN, VK_ESCAPE, 0);
Action.Execute;
Exit;
end;
end;
end;
inherited WndProc(Message);
end;
initialization
PopupList.Free;
PopupList := TMyPopupList.Create;
The GetMenuItemSecondAction routine you have to write yourself. Maybe this answer provides some help about adding your own actions to a component.
Note that the code under WM_MENUSELECT is simply copied from Menus.TPopupList.WndProc. You could also retrieve the MenuItem in the WM_MBUTTONUP handling by using MenuItemFromPoint.
But as the many comments have already said: think twice (or more) before implementing this UI functionality.
You are not notified of such an event. If you were there would be an entry for middle mouse button click in the list of menu notifications.
So perhaps you could use some sort of hack behind the back of the menu system if you really want to do this. However, as discussed in the comments, there are good reasons for thinking that your proposed UI may not be very appropriate.
If the middle click is not a suitable choice, how about using some key combination with mouse click like Ctrl-Click, to trigger another action? The TPopupMenu doesn't have any event related to customized click.
That is preferred over a middle mouse button click.
And then it is much simpler. Just check in your action execute handler if the CTRL button is pressed:
procedure TForm1.Action1Execute(Sender: TObject);
begin
if (GetKeyState(VK_CONTROL) and $8000 = 0) then
// process normal click
else
// process ctrl click
end;
I try to combine 2 answers from author NGLN and come out with the following.
Define a new class inherit from TPopupList:
TMyPopupList = class(TPopupList)
protected
procedure WndProc(var Message: TMessage); override;
end;
procedure TMyPopupList.WndProc(var Message: TMessage);
var H: HWND;
begin
case Message.Msg of
WM_MBUTTONDOWN: begin
H := FindWindow(PChar('#32768'), nil);
SendMessage(H, WM_IME_KEYDOWN, VK_RETURN, 0);
end;
end;
inherited WndProc(Message);
end;
initialization
PopupList.Free;
PopupList := TMyPopupList.Create;
end.
The Item1Click is an OnClick event handler of TMenuItem that perform based on mouse click:
procedure TForm1.Item1Click(Sender: TObject);
begin
if (GetKeyState(VK_MBUTTON) and $80 > 0) then
Caption := 'Middle Click'
else
Caption := 'Normal Click';
end;
Note: #32768 is the default window class name for a pop-up menu, see MSDN documentation.

Resources