Forwarding keyboard events from one Windows control to another - windows

In Delphi XE, I'm trying to implement an "instant search" feature - one that resembles Firefox's "search as you type" somewhat, but is better illustrated by a similar feature in an open source clipboard extender, Ditto:
There is a list of items that handles typical navigation events. However, any alphanumeric keys as well as navigation and editing commands (right/left arrows, shift+arrows, backspace, delete etc.) should be rerouted to the edit box below the list. An OnChange event of the edit box will trigger a refresh of the list.
The point of the UI is that user does not have to tab or shift-tab between the controls. The two controls (the list and the edit box) should 'feel" as if they were a single control. The behavior of the search UI should not be contingent on which control has focus.
It seems my best option is to forward certain keyboard events from the list control (I'm using TcxTreeList) to the edit box, and forward a handful of navigation keys from the edit box to the list. How can I achieve that?
Notes:
TcxTreeList supports incremental search of course, but this is not what I'm after. The search goes to an SQLite database and looks for substring matches. The list displays only the matching items from the db.
There is some overlap, e.g. both controls would normally handle VK_HOME and VK_END, but that's OK - in this case the keys would go to the list. I'll need to decide whether to forward each individual keypress, or handle it in the control that received it.
On Edit:
One obvious way seemed to be to invoke the respective KeyDown, KeyUp and KeyPress methods of the edit control, like so:
type
THackEdit = class( TEdit );
procedure TMainForm.cxTreeList1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
THackEdit( edit1 ).KeyDown( Key, Shift );
end;
Unfortunately, this has no effect. My guess is TEdit won't process key events unless it is focused. Using SendMessage( THackEdit( edit1 ).Handle, WM_KEYDOWN, Key, 0 ) has no effect, either.

You can use the message handling capability of a VCL control and send the relevant messages to one another. I don't know about a 'TcxTreeList', but the following demonstrates the idea on an edit control and a memo control responding to keyboard events synchronously (whereever possible of course).
type
TEdit = class(stdctrls.TEdit)
private
FMsgCtrl: TWinControl;
FRecursing: Boolean;
procedure WmChar(var Msg: TWMChar); message WM_CHAR;
procedure WmKeyDown(var Msg: TWMKeyDown); message WM_KEYDOWN;
procedure WmKeyUp(var Msg: TWMKeyUp); message WM_KEYUP;
end;
TMemo = class(stdctrls.TMemo)
private
FMsgCtrl: TWinControl;
FRecursing: Boolean;
procedure WmChar(var Msg: TWMChar); message WM_CHAR;
procedure WmKeyDown(var Msg: TWMKeyDown); message WM_KEYDOWN;
procedure WmKeyUp(var Msg: TWMKeyUp); message WM_KEYUP;
end;
TForm1 = class(TForm)
Edit1: TEdit;
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
private
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TEdit }
procedure TEdit.WmChar(var Msg: TWMChar);
begin
if not FRecursing then begin
inherited;
// Insert test here to see if the message will be forwarded
// exit/modify accordingly.
if Assigned(FMsgCtrl) then begin
FRecursing := True;
try
FMsgCtrl.Perform(Msg.Msg,
MakeWParam(Msg.CharCode, Msg.Unused), Msg.KeyData);
finally
FRecursing := False;
end;
end;
end;
end;
procedure TEdit.WmKeyDown(var Msg: TWMKeyDown);
begin
// exact same contents as in the above procedure
end;
procedure TEdit.WmKeyUp(var Msg: TWMKeyUp);
begin
// same here
end;
{ TMemo }
procedure TMemo.WmChar(var Msg: TWMChar);
begin
// same here
end;
procedure TMemo.WmKeyDown(var Msg: TWMKeyDown);
begin
// same here
end;
procedure TMemo.WmKeyUp(var Msg: TWMKeyUp);
begin
// same here
end;
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
Edit1.FMsgCtrl := Memo1;
Memo1.FMsgCtrl := Edit1;
end;
You might need to intervene additional messages but you get the idea.
If for one reason or another you cannot derive a new control or override message handling, you can consider sub-classing the controls. Answer to this question would show you how to do that.

Not exactly what you are asking for, but for similar results, I use the following trick.
Assume you have one TEdit Edit1 and one TListbox Listbox1.
In the OnEnter event of Listbox1, simply yield focus to Edit1
procedure TForm1.ListBox1Enter(Sender: TObject);
begin
edit1.SetFocus;
end;
And in the OnKeyDown event of Edit1, use the up and down arrows to navigate the items of the listbox and use the enter key to move the selected item to the edit box.
procedure TForm1.Edit1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
var k:word;
begin
if (Shift=[]) and (key=VK_DOWN) then
begin
listbox1.ItemIndex:=listbox1.ItemIndex+1;
key:=0;
end
else if (Shift=[]) and (key=VK_UP) then
begin
listbox1.ItemIndex:=listbox1.ItemIndex-1;
key:=0;
end
else if (Shift=[]) and (key=VK_RETURN) then
begin
edit1.text:=listbox1.items[listbox1.itemindex];
end;
end;

Related

Inno Setup Disable Next button when input is not valid

I need to disable Next button, when input is not "Admin".
Something like:
procedure EditKeyPress(Sender: TObject; var Key: Char);
begin
{ enable the next button if the value in the box is admin; disable otherwise }
WizardForm.NextButton.Enabled:=InputPage6.values[EditIndex2]‌​.Text = 'Admin'
end;
Implement the input box OnChange event. You will also need to make sure the button state is updated, when the custom page is activated. You can use OnActivate event for that (or CurPageChanged event function).
var
Page: TInputQueryWizardPage;
procedure ValidatePage;
begin
WizardForm.NextButton.Enabled := (CompareText(Page.Values[0], 'Admin') = 0);
end;
procedure EditChange(Sender: TObject);
begin
ValidatePage;
end;
procedure PageActivate(Sender: TWizardPage);
begin
ValidatePage;
end;
procedure InitializeWizard();
begin
Page := CreateInputQueryPage(...);
{ To disable the Next button initially [when box is empty] }
Page.OnActivate := #PageActivate;
Page.Add(..., False);
{ Update Next button state on any input change (typing, copy&paste, whatever) }
Page.Edits[0].OnChange := #EditChange;
end;
To combine multiple validations, see Inno Setup Disable Next button using multiple validation expressions (when input value matches one of multiple values).
For other approaches, see:
Inno Setup - Create User Input Query Page with input length and format limit and use the input;
Validate data on custom page when Next button is clicked in Inno Setup.

How do I pass a message from an object to another object in Free Pascal

I have a form and then I have a 'TPageControl' object (named 'MyPages') and a 'TButton' object (named 'MyButton') placed on it at design time.
Then I have a new class called 'TTab' which extends 'TTabSheet'. 'TTab' class has a 'TButton' object as one of its member variables like below.
class TTab = class(TTabSheet)
private
m_btnCloseTab: TButton;
end;
When I click on the 'MyButton', it would create a new 'TTab' object, init the tab (like instantiating the 'm_btnCloseTab') and add it to 'MyPages' at run time.
Procedure TForm1.MyButtonClick(Sender:TObject);
var
newTab: TTab;
newCaption: AnsiString;
begin
newCaption:= 'Tab' + IntToStr(count); //count is a global var
inc(count);
newTab:= TTab.Create(nil);
newTab.Init(newCaption);
newTab.Parent(MyPages);
end;
This is what the TTab.Init(newCaption: AnsiString) Procedure looks like.
Procedure TTab.Init(newCaption: AnsiString);
begin
Self.Caption:= newCaption;
m_btnCloseTab:= TButton.Create(nil);
with m_btnCloseTab do begin
Parent:= Self;
Left:= 10;
Top:= 10;
Caption:= 'Close Tab';
Visible:= True;
OnClick:= #closeTab;
end;
end;
That adds a new tab alright. The close button is also shown on each tab.
How do I click on the 'm_btnCloseTab' on each tab to close that particular tab?
If I define a destructor (by overriding the destructor of the TTabSheet) for TTab like below, I can call it from outside.
Destructor TTab.Destroy;
begin
if m_btnCloseTab <> nil then begin
m_btnCloseTab.Destroy;
m_btnCloseTab:= nil;
end;
inherited;
end;
But I cannot call the Destructor from inside the tab (well, you can). If I do it, I cannot free the m_btnCloseTab object as it would give an exception, because we are still its event handler. If I don't free it, the tab gets closed fine, but the memory gets leaked (because we did not free m_btnCloseTab).
I believe I have to trigger an event so that the destructor can be called from the outside of 'TTab'. I don't know how to do it.
Any help would be appreciated.
Thanks.
You can find Notification methods all over the LCL sources (and in Delphi as well, of course). A simple example is a TLabeledEdit: this is some kind of "TEdit" which contains a TLabel. If the Label is destroyed the LabeledEdit is notified of this because it must set the reference to the label to nil. Otherwise the destructor of TLabeledEdit would attempt to destroy the label again - BOOM. Here the method is like this (pasted from ExtCtrls):
procedure TCustomLabeledEdit.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FEditLabel) and (Operation = opRemove) then
FEditLabel := nil;
end;
And here you can see what you have to do in your case:
procedure TTab.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = m_BtnCloseTab) and (Operation = opRemove) then
m_BtnCloseTab := nil;
end;
Please note that Notification is a virtual method and must be declared with the attribute "override" in the protected section of the component.
I would use a single button for this task.
Take m_btnCloseTab declaration out of TTab and put it in private main form.
Then on your main form's FormCreate:
m_btnCloseTab := TButton.Create( MyPages );
(the above assumes MyPages is a component placed on the form, if not it must be created first.)
Give the button a top and left that makes sense for your TTab's.
Now m_btnCloseTab will be freed when MyPages is freed which is freed when the form is closed.
Now all you have to do is create your new tabs as you like and when one is focused simply make that tab the parent of your button. You could do this, for instance, in the MyPages OnChange method or whatever it has like that.
When the button is clicked it does something like TTab( Parent ).Free;
However, you may need to store Parent in a local variable in the button's OnClick, say:
TempTab: TTab
Then simply set TempTab := TTab( Parent ), set Button's Parent to nil, then call TempTab.Free;
I would also give your Tabs an owner. That way if the user closes the form with tabs still open (that is, your button hasn't been clicked) the owner will free them.
So declare your tabs like:
newTab:= TTab.Create( MyPages );
This should solve all your problems and, after a bit of fiddling, is quite easy to manage.
One final recommendation I would use the method .Free and/or FreeAndNil( ) rather than calling .destroy directly.

How to detect when the form is being maximized?

I would like to detect when the form is going to be maximized to save certain settings (not related to the size nor position) and modify the size and position a little bit. Is there an universal way to do it ?
I've tried to catch the WM_SYSCOMMAND message like in this article. It works well for maximization from menu, by maximize button, but it's not fired when I press the WIN + UP keystroke. Does anyone know an universal way how to catch the maximization event including the case with WIN + UP keystroke ?
Thanks
You can use the WM_GETMINMAXINFO message to save the state of the window and then use the WMSize message to check if the window was maximized.
in you form declare the mesage handler like so :
procedure WMSize(var Msg: TMessage); message WM_SIZE;
And handle like this :
procedure TForm57.WMSize(var Msg: TMessage);
begin
if Msg.WParam = SIZE_MAXIMIZED then
ShowMessage('Maximized');
end;
WIN+UP does not generate WM_SYSCOMMAND messages, that is why you cannot catch them. It does generate WM_GETMINMAXINFO, WM_WINDOWPOSCHANGING, WM_NCCALCSIZE, WM_MOVE, WM_SIZE, and WM_WINDOWPOSCHANGED messages, though. Like RRUZ said, use WM_GETMINMAXINFO to detect when a maximize operation is about to begin and WM_SIZE to detect when it is finished.
IMO, You cannot use WM_GETMINMAXINFO to "detect when a maximize operation is about to begin" as #Remy stated.
In-fact the only message that can is WM_SYSCOMMAND with Msg.CmdType=SC_MAXIMIZE or undocumented SC_MAXIMIZE2 = $F032 - but it's not being sent via Win+UP, or by using ShowWindow(Handle, SW_MAXIMIZE) for example.
The only way I could detect that a window is about to be maximized is via WM_WINDOWPOSCHANGING which is fired right after WM_GETMINMAXINFO:
type
TForm1 = class(TForm)
private
procedure WMWindowPosChanging(var Message: TWMWindowPosChanging); message WM_WINDOWPOSCHANGING;
end;
implementation
const
SWP_STATECHANGED = $8000;
procedure TForm1.WMWindowPosChanging(var Message: TWMWindowPosChanging);
begin
inherited;
if (Message.WindowPos^.flags and (SWP_STATECHANGED or SWP_FRAMECHANGED)) <> 0 then
begin
if (Message.WindowPos^.x < 0) and (Message.WindowPos^.y < 0) then
ShowMessage('Window state is about to change to MAXIMIZED');
end;
end;

Add a border icon to the form

Using Delphi I would like to add another button to the border icon buttons; close, maximize, minimize. Any ideas on how to do this?
This was easy to do prior to Windows Aero. You simply had to listen to the WM_NCPAINT and WM_NCACTIVATE messages to draw on top of the caption bar, and similarly you could use the other WM_NC* messages to respond to mouse clicks etc, in particular WM_NCHITTEST, WM_NCLBUTTONDOWN, and WM_NCLBUTTONUP.
For instance, to draw a string on the title bar, you only had to do
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs;
type
TForm1 = class(TForm)
protected
procedure WMNCPaint(var Msg: TWMNCPaint); message WM_NCPAINT;
procedure WMNCActivate(var Msg: TWMNCActivate); message WM_NCACTIVATE;
private
procedure DrawOnCaption;
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TForm1 }
procedure TForm1.WMNCActivate(var Msg: TWMNCActivate);
begin
inherited;
DrawOnCaption;
end;
procedure TForm1.WMNCPaint(var Msg: TWMNCPaint);
begin
inherited;
DrawOnCaption;
end;
procedure TForm1.DrawOnCaption;
var
dc: HDC;
begin
dc := GetWindowDC(Handle);
try
TextOut(dc, 20, 2, 'test', 4);
finally
ReleaseDC(Handle, dc);
end;
end;
end.
Now, this doesn't work with Aero enabled. Still, there is a way to draw on the caption bar; I have done that, but it is far more complicated. See this article for a working example.
Chris Rolliston wrote a detailed blog about creating a custom title bar on Vista and Windows 7.
He also wrote a follow up article and posted example code on CodeCentral.
Yes, set the form's border style property to bsNone and implement your own title bar with all the buttons and custom behaviour you like.

Background changes by itself and procedure repeats many times until I release the mouse button

I am a student, and I'm working on a little slots game (if the same random number comes up 3 timed, you win). I use Borland Pascal 7. I use graph to make this a bit more visual, but when I start the game my background turns from black to grey, and the other problem is that if I click the game start button, the game runs many times until I release the mouse button. How can I solve this?
Here is my full program:
program slots;
uses mymouse,graph,crt;
var gdriver,gmode,coin:integer;
m:mouserec;
a,b,c,coins:string;
procedure gomb(x1,y1,x2,y2:integer;szoveg:string);
var j,n:integer;
begin
setcolor(blue);
rectangle(x1,y1,x2,y2);
setfillstyle(1,blue);
floodfill(x1+2,y1+2,blue);
setcolor(0);
outtextxy((x1+x2)div 2 -textwidth(szoveg) div 2 ,(y1+y2) div 2-textheight(szoveg) div 2,szoveg);
end;
procedure randomal(var a,b,c:string);
begin
randomize;
STR(random(2)+1,a);
STR(random(2)+1,b);
STR(random(2)+1,c);
end;
procedure menu;
begin;
settextstyle(0,0,1);
outtextxy(20,10,'Meno menu');
gomb(20,20,90,50,'Teglalap');
gomb(20,60,90,90,'Inditas');
gomb(20,100,90,130,'Harmadik');
gomb(20,140,90,170,'Negyedik');
end;
procedure teglalap(x1,x2,y1,y2,tinta:integer);
begin
setcolor(tinta);
rectangle(x1,x2,y1,y2);
end;
procedure jatek(var a,b,c:string;var coin:integer;coins:string);
begin;
clrscr;
menu;
randomal(a,b,c);
if ((a=b) AND (b=c)) then coin:=coin+1 else coin:=coin-1;
settextstyle(0,0,3);
setbkcolor(black);
outtextxy(200,20,a);
outtextxy(240,20,b);
outtextxy(280,20,c);
STR(coin,coins);
outtextxy(400,400,coins);
end;
procedure eger;
begin;
mouseinit;
mouseon;
menu;
repeat
getmouse(m);
if (m.left) and (m.x>20) ANd (m.x<90) and (m.y>20) and (m.y<50) then teglalap(90,90,300,300,blue);
if (m.left) and (m.x>20) AND (m.x<90) and (m.y>60) and (m.y<90) then jatek(a,b,c,coin,coins);
until ((m.left) and (m.x>20) ANd (m.x<140) and (m.y>140) and (m.y<170));
end;
begin
coin:=50;
gdriver:=detect;
initgraph(gdriver, gmode, '');
eger;
end.
I have many years to use Turbo Pascal :)
I used this snippet to init BGI (graphic) mode:
Gd := Detect;
InitGraph(Gd, Gm, 'bgi');
if GraphResult <> grOk then
Halt(1);
SetBkColor(black);
Cleardevice;
If I recall correctly, ClearDevice is proper for clearing the screen, ClrScr is for text mode.
Now, GetMouse(m); probably returns immediately the mouse data thus the code
in the repeat loop runs again and again with no delay, even if you don't use the mouse.
One solution is to check if the mouse button is up before you execute that code or
add some kind of delay before calling the GetMouse.

Resources