How to set a variable if any action is done (in Delphi)? - delphi

I need to somehow implement this in Delphi 2009:
The user clicks on button 2. If the user's very last action was clicking on button 1, then I want to do one thing, but if the user's very last action was anything else, I want to do another thing.
Obviously, I set up a boolean variable: UserClickedOnButton1 and set it to true when button 1 is clicked on, and I test that variable in the OnButtonClick event for Button 2.
My question is how do I set that to false whenever anything else is done by the user before clicking on button 2. (e.g. Mouse press, key press, arrow keys, switch to another program, or anything else).
... or is there a simpler way to do this that I am overlooking.

The code below seems to work (D7), but please check this for your specific situation.
type
TButton = class(StdCtrls.TButton)
private
FClickedLast: Boolean;
FNextButton: TButton;
protected
procedure WndProc(var Message: TMessage); override;
public
procedure Click; override;
property ClickedLast: Boolean read FClickedLast write FClickedLast;
property NextButton: TButton write FNextButton;
end;
TForm1 = class(TForm)
...
procedure TForm1.FormCreate(Sender: TObject);
begin
Button1.NextButton := Button2;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
if Button1.ClickedLast then
Caption := Caption + ' +'
else
Caption := Caption + ' -';
Button1.ClickedLast := False;
end;
{ TButton }
procedure TButton.Click;
begin
inherited Click;
if (FNextButton <> nil) and Focused then
FClickedLast := True;
end;
procedure TButton.WndProc(var Message: TMessage);
begin
if (FNextButton <> nil) and not (csDestroying in ComponentState) then
case Message.Msg of
CM_CANCELMODE,
WM_KEYFIRST..WM_KEYLAST:
FClickedLast := False;
WM_KILLFOCUS:
if TWMKillFocus(Message).FocusedWnd <> FNextButton.Handle then
FClickedLast := False;
end;
inherited WndProc(Message);
end;
Explanation:
CM_CANCELMODE handles mouse clicks anywhere not resulting in changing focus,
WM_KEY* handles all key events, but also switching to another application (there is a WM_SYSKEYDOWN, otherwise WM_KILLFOCUS takes care),
WM_KILLFOCUS handles everything else.

From what I think; It's not really possible unless you're willing to go and track all (or at least all possibly unwanted) of events with logic.
A key-press (Tab?) can still be valid to move on to the next button and click it; a mouse-down event, obviously is good if it's on the second button, otherwise it's not. You'd probably want to check if the 'first button is clicked' before executing a whole bunch of logic to slow down every keypress/mousedown/lostfocus event in your application.
An idea could be to use a timer, but this doesn't prevent the user from 'quickly' doing something else.
Edt1: If all other actions that are 'illegal' are actually doing something, perhaps a lostfocus event on the first button could be a start?

Related

How to make close button open a new form on Delphi

I need that "x" button on any form would not close the form but instead open another 3 random forms on delphi, i have no idea how to do that, please help
Just use the form's OnCloseQuery event to detect the user's trying to close your form (by clicking the close button in the top-right corner, by double-clicking the form's title bar icon, by selecting the Close system menu item, by pressing Alt+F4, etc.).
Then set CanClose to False and instead open your three new forms:
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
CanClose := False;
Form2.Show;
Form3.Show;
Form4.Show;
end;
As suggested by #AndreasRejbrand's answer, you could use the Form's OnCloseQuery event. But, the problem with that approach is that the event is also triggered during system reboot/shutdown, and you don't want to block that. If OnCloseQuery returns CanClose=False during a system shutdown, the shutdown is canceled.
Another option is to use the Form's OnClose event instead, setting its Action parameter to caNone, eg:
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caNone;
Form2.Show;
Form3.Show;
Form4.Show;
end;
However, the best option would to be to handle only user-initiated closures (the X button, ALT-F4, etc) by having the Form handle the WM_SYSCOMMAND message looking for SC_CLOSE notifications, eg:
procedure TForm1.WndProc(var Message: TMessage);
begin
if (Message.Msg = WM_SYSCOMMAND) and (Message.WParam and $FFF0 = SC_CLOSE) then
begin
Message.Result := 0;
Form2.Show;
Form3.Show;
Form4.Show;
end
else
inherited;
end;
This way, system-initiated closures are unhindered.

OnClick Event for TListbox, Calls while using the arrow Keys to change a selected item

I Have a ListBox on my From with several items in it. When the User clicks an item (OnClick Event) the Users Status is changed and a TCP server is notified. If I use the Arrow Keys On the Keyboard, the Same Event Is called, like an OnChange Event. However There is no OnChange Event.
The Problem with using the arrow keys is that If a User rapidly moves across several items, my Notify Server Method is called several times. (this is not good)
To Get around this I Put a Timer on the OnKeyPress Event. When The arrow keys are pressed If the user stops pushing the arrow key for 2seconds, the Notify Server Method is called, Notifying the server once. (In theory)
Both OnKeyPress and OnClick are still called.
Is anyone familiar enough with TListbox to explain to me why this happens, and if there is a better way of thinking about this problem? The User Requirements are to use a Listbox, and to Not disable the arrow keys.
The OnClick event is triggered when the user clicks on the ListBox, but it is also triggered when the selection actually changes for any reason. This is a design flaw (IMHO) in how TListBox is implemented. It should have exposed actual OnChanging and OnChange events instead (since the underlying ListBox control provides such notifications), like other components do.
However, you can the use the following approach to distinguish between a mouse click and a keyboard arrow keypress:
Set a flag in the OnKeyDown event if an up/down arrow is being held down.
Clear the flag in the OnKeyUp event for the same arrow key.
You can then check for that flag in the OnClick event (or better, subclass the ListBox to intercept the LBN_SELCHANGING/LBN_SELCHANGE notification directly). If the flag is set, start your timer to delay your server action, otherwise perform your action immediately.
For example:
type
TForm1 = class(TForm)
...
private
IsArrowDown: Boolean;
...
end;
...
procedure TForm1.ListBox1Click(Sender: TObject);
begin
if IsArrowDown then
begin
Timer1.Enabled := False;
Timer1.Interval := 1000;
Timer1.Enabled := True;
end else
UpdateUserStatus;
end;
procedure TForm1.ListBox1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [VK_DOWN, VK_UP] then
IsArrowDown := True;
end;
procedure TForm1.ListBox1KeyUp(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [VK_DOWN, VK_UP] then
IsArrowDown := False;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
UpdateUserStatus;
end;
procedure TForm1.UpdateUserStatus;
begin
// notify server as needed...
end;
Update: a double-click also triggers the OnClick event before the OnDblClick event. So if you need to differentiate between single-clicking and double-clicking, you will have to use a timer for that as well:
type
TForm1 = class(TForm)
...
private
IsArrowDown: Boolean;
...
end;
...
procedure TForm1.ListBox1Click(Sender: TObject);
begin
if IsArrowDown then
begin
Timer1.Enabled := False;
Timer1.Interval := 1000;
Timer1.Enabled := True;
end else
begin
Timer1.Enabled := False;
Timer1.Interval := GetDoubleClickTime() + 500;
Timer1.Enabled := True;
end;
end;
procedure TForm1.ListBox1DblClick(Sender: TObject);
begin
Timer1.Enabled := False;
UpdateUserStatus;
end;
procedure TForm1.ListBox1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [VK_DOWN, VK_UP] then
IsArrowDown := True;
end;
procedure TForm1.ListBox1KeyUp(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [VK_DOWN, VK_UP] then
IsArrowDown := False;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
UpdateUserStatus;
end;
procedure TForm1.UpdateUserStatus;
begin
// notify server as needed...
end;
Rather than performing the action automatically each time a selection or onChange event occurs, either make the action explicit with a button, as suggested elsewhere here; or reset a timer, then when the timer goes off, if the selection is still valid, trigger the action on the current selection (effectively clicking the button in the timer handler). This approach lends itself to a nice user-configurable option where you can enable automatic notification after ___ seconds or require the button to be clicked manually.
This is documented behaviour:
This event can also occur when the user selects an item in a grid, outline, list, or combo box by pressing an arrow key.
From the perspective of the user, why should using the keyboard be discriminated against. If I want to select the item immediately below the current selection then why does it matter whether I use the mouse or the keyboard. Some users don't even have mice.
You need to design your program to be resilient to such actions. Your current approach is not unreasonable. I'd take the same approach even if the user clicked with the mouse. Users often miss and need to click again. So, wait for a short period of time after OnClick before responding.
Another approach might be to make the user actively invoke the action. So, provide a button, perhaps captioned Apply and only do work when the user presses it.
If I understand you correctly your problem is that your program can send to manny notifications to your server.
If this is true then you should not be considering of how TListBox events work but how can you prevent to manny nitifications being sent to your server.
So first thing you should do is move all the code related to nitifying your server into a seperate method if you haven't done so.
Then this method should check when the last notification to the server was sent in order to determine if another server notification is allowed.
For this you can simply store the last time that notification to the server was sent either by using Now (TTime format) to get current system time whic would be good for one second or larger intervals or GetTickCount if you are interested in intervals shorter than one second. Technically you could also use Now for less tahan a second intervals but would require you to call special methods to get the time in milliseconds format.
After you have the last notification time stored all you need to do is check if certain interval has already passed.
And if you need to really log every event you can configure your client to store them in some que and then send the whole que to the server.

OnKeyDown not working on Dialog invoked from main form (which also uses OnKeyDown)

Delphi 2010
I am using a OnFormKeyDown event on my main form, and I basically use the same event on a Dialog
//main form
procedure TfrmMain.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
case Key of
VK_DOWN: btnLast.OnClick(Self);
VK_Up: btnFirst.OnClick(Self);
VK_Left: btnPrev.OnClick(Self);
VK_Right: btnNext.OnClick(Self);
end;
end;
procedure TfrmMain.mniShowOwnedClick(Sender: TObject);
var
I: Integer;
begin
frmMain.KeyPreview:= False;
frmOwned.KeyPreview:= True;
frmOwned.Owned2.Clear;
for I := 0 to Tags.Count - 1 do
if Owned.IndexOf(Tags.Names[I]) <> -1 then
frmOwned.Owned2.Add(Tags[I]);
if frmOwned.ShowModal = mrOK then
begin
frmMain.KeyPreview:= True;
frmOwned.KeyPreview:= False;
end;
end;
//Dialog
procedure TfrmOwned.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
case Key of
VK_DOWN: btnLast.OnClick(Self);
VK_Up: btnFirst.OnClick(Self);
VK_Left: btnPrev.OnClick(Self);
VK_Right: btnNext.OnClick(Self);
end;
end;
The Form's OnKeyDown works fine, but I can't seem to get the dialogs to work
The problem is that these keys are used as dialog navigation keys. And as such, they never make their way to the OnKeyDown event.
To be honest I had a hard time understanding why they are firing for your main form's OnKeyDown event. I could not make that happen in my test environment. That's because I had added a button to the form. That's enough to mean that the arrow keys are treated as navigation keys. Try creating a an app with a single form and adding a few buttons. Then run the app and use the arrow keys to move the focus between the buttons. That's what I mean when I say that the arrow keys are treated as navigation keys.
I expect that the difference between your two forms is that the main form has nothing that can be navigated around by arrow keys, but the modal form does.
Now, you could stop the arrow keys being treated as navigation keys. Like this:
type
TMyForm = class(TForm)
....
protected
procedure CMDialogKey(var Message: TCMDialogKey); message CM_DIALOGKEY;
....
end;
....
procedure TMyForm.CMDialogKey(var Message: TCMDialogKey);
begin
case Message.CharCode of
VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN:
Message.Result := 0;
else
inherited;
end;
end;
However, a better solution, in my view, is to stop trying to implement shortcuts using OnKeyDown events. That seems like the wrong solution. The right solution is to use actions. Create an action list. Add actions for first, last, previous and next actions. Give them the appropriate ShortCut properties. Assign those actions to your buttons. And the job is done.
One of the benefits of this is that you can stop trying to fake button click events. For what it is worth, calling OnClick is the wrong way to do that. Call the button's Click method if ever your really need to do that. However, use an action and it's all taken care of.
Another benefit is that you'll no longer need to pfaff around with KeyPreview. Simply put, if you want to implement short cuts, use TAction.ShortCut.

Displaying hints

I have added hints to components on my form. When the components receive the focus, I'd like to set the caption of a label component to display the hint.
I have added a TApplicationEvents object and set the OnShowHint event to
procedure TImportFrm.ApplicationEvents1ShowHint(var HintStr: string;
var CanShow: Boolean; var HintInfo: THintInfo);
begin
HelpLbl.Caption := HintStr;
end;
However it seems that the ShowHint event only fires with mouse movements. Is there a way to fire the hint code when components receive focus, without having to implement the OnEnter event for every single component on the form?
Add a handler for TScreen.OnActiveControlChange in your main form's creation, and handle the hints in that event:
type
TForm2=class(TForm)
...
private
procedure ScreenFocusControlChange(Sender: TObject);
end;
implementation
procedure TForm2.FormCreate(Sender: TObject);
begin
Screen.OnActiveControlChange := ScreenFocusControlChange;
end;
procedure TForm2.ScreenFocusControlChange(Sender: TObject);
begin
Label1.Caption := ActiveControl.Hint;
Label1.Update;
end;
Note that Sender won't do you much good; it's always Screen. You can filter (for instance, to only change the Label.Caption for edit controls) by testing the ActiveControl:
if (ActiveControl is TEdit) then
// Update caption of label with ActiveControl.Hint
Note that if you'll need to reassign the event when you show child forms (to an event on that child form), or you'll always be updating the original form's label with the hints. The easiest way to do the reassignment is to give every form an OnActiveControlChange handler, and assign it in the form's OnActivate event and unassign it in the OnDeactivate event:
procedure TForm1.FormActivate(Sender: TObject);
begin
Screen.OnActiveControlChange := Self.ScreenActiveControlChange;
end;
procedure TForm1.FormDeactivate(Sender: TObject);
begin
Screen.OnActiveControlChange := nil;
end;
This will allow you to update controls other than Label1 on each form, and only use the hint changes on forms you want to do so.
A simple solution is to use OnIdle event:
procedure TForm1.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
begin
if Assigned(ActiveControl) then
Label1.Caption:= ActiveControl.Hint;
end;
A more advanced solution is to override protected ActiveChanged method of TForm:
type
TForm1 = class(TForm)
...
protected
procedure ActiveChanged; override;
end;
...
procedure TForm1.ActiveChanged;
begin
inherited;
if Assigned(ActiveControl) then
Label1.Caption:= ActiveControl.Hint;
end;
Receiving focus and OnShowHint are quite different events; OnShowHint can be triggered for non-focused control as well.
Why would you need to implement the OnEnter event for every single component? You can create one generic method / event handler like:
procedure TForm1.AnyControlEnter(Sender: TObject);
begin
lbl1.Caption := TControl(Sender).Hint;
end;
and assign it to every component you placed on the form.
You said:
it seems that the ShowHint event only fires with mouse movements
This is a normal behaviour. The problem you have ( it's a guess) is that hints are not fired directly. Don't try to make a workaround, what you try to do with MouseEnter is exactly what is already happening...the only difference is that you'be forget something...
Keep the event ApplicationEvents1ShowHint() as you've initially done but add this in the form constructor event:
Application.HintPause := 1;
And then hints will be displayed (almost) without delay.

TMenuItem-Shortcuts overwrite Shortcuts from Controls (TMemo)

What can I do that shortcuts for menu items don't overwrite those from local controls?
Imagine this simple app in the screenshot. It has one "undo" menu item with the shortcut CTRL+Z (Strg+Z in German) assigned to it. When I edit some text in the memo and press CTRL+Z I assume that the last input in the memo is reverted, but instead the menu item is executed.
This is especially bad in this fictional application because the undo function will now delete my last added "Item 3" which properties I was editing.
CTRL+Z is just an example. Other popular shortcuts cause similar problems (Copy&Paste: CTRL+X/C/V, Select all: CTRL+A).
Mini Demo with menu item with CTRL+Z short-cut http://img31.imageshack.us/img31/9074/ctrlzproblem.png
The VCL is designed to give menu item shortcuts precedence. You can, however, write your item click handler (or action execute handler) to do some special handling when ActiveControl is TCustomEdit (call Undo, etc.)
Edit: I understand you don't like handling all possible special cases in many places in your code (all menu item or action handlers). I'm afraid I can't give you a completely satisfactory answer but perhaps this will help you find a bit more generic solution. Try the following OnShortCut event handler on your form:
procedure TMyForm.FormShortCut(var Msg: TWMKey; var Handled: Boolean);
var
Message: TMessage absolute Msg;
Shift: TShiftState;
begin
Handled := False;
if ActiveControl is TCustomEdit then
begin
Shift := KeyDataToShiftState(Msg.KeyData);
// add more cases if needed
Handled := (Shift = [ssCtrl]) and (Msg.CharCode in [Ord('C'), Ord('X'), Ord('V'), Ord('Z')]);
if Handled then
TCustomEdit(ActiveControl).DefaultHandler(Message);
end
else if ActiveControl is ... then ... // add more cases as needed
end;
You could also override IsShortCut method in a similar way and derive your project's forms from this new TCustomForm descendant.
You probably need an alike solution as below. Yes, feels cumbersome but this is the easiest way I could think of at the time. If only Delphi allowed duck-typing!
{ you need to derive a class supporting this interface
for every distinct control type your UI contains }
IEditOperations = interface(IInterface)
['{C5342AAA-6D62-4654-BF73-B767267CB583}']
function CanCut: boolean;
function CanCopy: boolean;
function CanPaste: boolean;
function CanDelete: boolean;
function CanUndo: boolean;
function CanRedo: boolean;
function CanSelectAll: Boolean;
procedure CutToClipBoard;
procedure Paste;
procedure CopyToClipboard;
procedure Delete;
procedure Undo;
procedure Redo;
procedure SelectAll;
end;
// actions....
procedure TMainDataModule.actEditCutUpdate(Sender: TObject);
var intf: IEditOperations;
begin
if Supports(Screen.ActiveControl, IEditOperations, intf) then
(Sender as TAction).Enabled := intf.CanCut
else
(Sender as TAction).Enabled := False;
end;
procedure TMainDataModule.actEditCutExecute(Sender: TObject);
var intf: IEditOperations;
begin
if Supports(Screen.ActiveControl, IEditOperations, intf) then
intf.CutToClipBoard;
end;
....

Resources