In a button-click event, I am sending a command to a server. I don't want the server to be flooded with requests, so I want to prevent the user from clicking the button too often.
I've thought about counting how many times the button is clicked, like this:
button.tag := button.tag + 1;
if button.Tag = 5 then
exit;
But this will not help because I have to set the tag to 0 each time I need to detect the fast clicks, and this will also not help because it will detect the tag number every 5 clicks, whereas I just need to know if the button was clicked repeatedly.
How do I detect fast button clicks without using the button tag?
If you want to restrict how often to send commands from your client to the server, disable the button a short while after each click. This will give the user feedback that something is processing. I'm assuming there is no way the server can give feedback when it is ready.
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Enabled := false;
Timer1.Interval := 500; // Pick a suitable interval
Timer1.Enabled := true;
SendCommand(); // Make call to server
end;
procedure TForm1.Timer1Timer(Sender: TObject);
// Timer1 is disabled at startup
begin
Button1.Enabled := true;
Timer1.Enabled := false;
end;
Note: if you are trying to do some load balancing on the server, there must be a way for the clients to know how to restrict the calls. But this is all a backwards way of doing it, the server should be capable of handling this without any restrictions on the clients.
You've correctly recognized that merely counting the clicks won't be sufficient. You need to account for the time, too. So use a timer.
When the button is clicked, disable the button and enable a timer. When the timer fires, re-enable the button.
procedure TExampleForm.SendButtonClick(Sender: TObject);
begin
Assert(SendButton = Sender);
SendButton.Enabled := False;
SendButtonTimer.Enabled := True;
SendCommandToServer(...);
end;
procedure TExampleForm.SendButtonTimerTimer(Sender: TObject);
begin
Assert(SendButtonTimer = Sender);
SendButtonTimer.Enabled := False;
SendButton.Enabled := True;
end;
Set the timer's Interval property to the number of milliseconds that you want the button to remain disabled.
There's no need to count the clicks in this scenario because the implicit limit is one click per timer interval.
You could just put some kind of timestamp into the Tag, and check whether enough time has elapsed.
Better yet, derive your own class, (TDontPressMeTooFastButton ;-), and you can do whatever you want, ie.:
track both how much time elapsed since last click and how many "fast clicks" the user attempted
you could make the button contain the timer component, not leaking this implementation detail into the form
Just about anything, in a much cleaner way than using the .Tag or global/form variables ...
Related
I am using a TEdit to allow the user to enter a number, e.g. 10.
I convert the TEdit.Text to an integer and a calculation procedure is called.
In this calc procedure, a check was built-in to make sure no numbers below 10 are processed.
Currently I use the OnChange event. Suppose the user wants to change '10' into e.g.'50'. But as soon as the '10' is deleted or the '5' (to be followed by the 0) is typed, I trigger my warning that the minimum number is 10. I.e. the program won't wait until I have fully typed the 5 and 0.
I tried OnEnter, OnClick, OnExit, but I seem not to overcome this problem. The only solution is to add a separate button that will trigger the calculation with the new number. It works, but can I do without the button?
Use a timer for a delayed check, e.g.:
procedure TForm1.Edit1Change(Sender: TObject);
begin
// reset the timer
Timer1.Enabled := false;
Timer1.Enabled := true;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := false;
// do your check here
end;
Setting the timer to 500 ms should be fine for most users.
And as David suggested in the comments to the question: Do not show an error dialog, use something less intrusive instead, e.g. an error message in a label near the edit or change the background color. And also: Do not prevent the focus to be moved away from that control and do not play a sound, that's also very annoying.
For our in house software we set the background of a control to yellow if there is an error and display the error message of the first such error in the status bar and also as a hint of the control. If you do that, you probably don't even need to have the delay.
Thanks, for your help. I tried the timer option, but could not get that to work. I now have this code, which works (almost - see below), but requires the used to always type a CR:
procedure Calculate;
begin
// this is my calculation procedure
ShowMessage('calculation running correctly');
end;
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
N : integer;
begin
if Key = #13 then
begin
N := StrtoInt(Edit1.Text);
if N<10 then ShowMessage('<10!') else
if N>100 then ShowMessage('>100!') else Calculate;
end;
end;
I used the ShowMessage() here only to see if the sample code worked. In the real program I have left that out, as you all suggested.
I also included the 'turn yellow on wrong entry' (thanks David). The only issue is that if the user runs this I get a beep from my computer. I can't see what went wrong. But what is causing the beep?
I need to embed a trivial MP3 player in a Delphi7 application.
I will simply scan a directory and play all the files in a random order.
I found two possible solutions: one using the Delphi MediaPlayer, and one using the PlaySound Windows API.
None are working.
The problem seems to be in the missing "stop" notification.
Using PlaySound like this:
playsound(pchar(mp3[r].name), 0, SND_ASYNC or SND_FILENAME);
I could not find a way to (politely) ask Windows to inform me when a song stopped playing.
Using the Delphi MediaPlayer, internet is full of suggestions copy/pasted one from the other, like here:
http://www.swissdelphicenter.ch/en/showcode.php?id=689
http://delphi.cjcsoft.net/viewthread.php?tid=44448
procedure TForm1.FormCreate(Sender: TObject);
begin
MediaPlayer1.Notify := True;
MediaPlayer1.OnNotify := NotifyProc;
end;
procedure TForm1.NotifyProc(Sender: TObject);
begin
with Sender as TMediaPlayer do
begin
case Mode of
mpStopped: {do something here};
end;
//must set to true to enable next-time notification
Notify := True;
end;
end;
{
NOTE that the Notify property resets back to False when a
notify event is triggered, so inorder for you to recieve
further notify events, you have to set it back to True as in the code.
for the MODES available, see the helpfile for MediaPlayer.Mode;
}
My problem is that I do get a NotifyValue == nvSuccessfull when a song is over but ALSO when I start a song, so I cannot rely on it.
Furthermore, I never, ever receive a change in state of the "mode" property, that should become mpStopped according to all the examples I found.
There is a similar question here
How can I repeat a song?
but it does not work because, as said, I receive the nvSuccessfull twice, without a way to distinguish between the start and the stop.
Last but not least, this app should work from XP to Win10, that's why I am developing with Delphi7 on WinXP.
Thank you and sorry for the length of this post, but I really tried many solutions before asking for help.
To detect when to load a new file for playing, you can use the OnNotify event and the EndPos and Position properties of the TMediaPlayer (hereafter called MP)
First setup the MP and select a TimeFormat, for example
MediaPlayer1.Wait := False;
MediaPlayer1.Notify := True;
MediaPlayer1.TimeFormat := tfFrames;
MediaPlayer1.OnNotify := NotifyProc;
When you load a file for playing, set the EndPos property
MediaPlayer1.FileName := OpenDialog1.Files[NextMedia];
MediaPlayer1.Open;
MediaPlayer1.EndPos := MediaPlayer1.Length;
MediaPlayer1.Play;
And the OnNotify() procedure
procedure TForm1.NotifyProc(Sender: TObject);
var
mp: TMediaPlayer;
begin
mp:= Sender as TMediaPlayer;
if not (mp.NotifyValue = TMPNotifyValues.nvSuccessful) then Exit;
if mp.Position >= mp.EndPos then
begin
// Select next file to play
NextMedia := (NextMedia + 1) mod OpenDialog1.Files.Count;
mp.FileName := OpenDialog1.Files[NextMedia];
mp.Open;
mp.EndPos := mp.Length;
mp.Position := 0;
mp.Play;
// Set Notify, important
mp.Notify := True;
end;
end;
Finally a comment to your attempt to use the MP.Mode = mpStopped mode to change to a new song. The mode is changed when the buttons are operated, iow mpStopped when the user presses the Stop button. Changing the song and starting to play it would likely not be what the user expected.
In Delphi 10.1.2, inside a TActionList I have created a TAction with these properties and assigned a shortcut Ctrl+F12 to it:
At runtime, when I keep the shortcut keys Ctrl+F12 pressed, the action is executed repeatedly (with speed depending on the system keyboard repeating speed).
So how can I make the action execute only ONCE (until these keys are lifted up), even if the user keeps the keys pressed down or if the user's system has a high keyboard repeat speed setting?
You can retrieve system keyboard settings by using SystemParametersInfo. SPI_GETKEYBOARDDELAY gives the repeat delay; the time between the first and second generated events. SPI_GETKEYBOARDSPEED gives keyboard repeat rate; the duration between events after the initial delay. The documentation has approximate values of the slowest and fastest settings which may help you decide on an acting point.
Suppose you decided to act on. Since shortcuts expose no direct relation to the event that generated them, they have no property or anything that could reveal information about if it is an initial, delayed, or repeated execution.
Then, one option is to disable a shortcut's execution as soon as it is entered and re-enable after the appropriate key has been released. You have to set KeyPreview of the form to true to implement this solution as any of the controls on the form might be the one with the focus when a shortcut is generated.
A less cumbersome solution I would find is to prevent generation of the shortcut when it's not generated by an initial key-down. You have to watch for key down events this time.
One possible implementation can be to install a local keyboard hook.
var
KeybHook: HHOOK;
procedure TForm1.FormCreate(Sender: TObject);
begin
KeybHook := SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, 0, GetCurrentThreadId);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
UnhookWindowsHookEx(KeybHook);
end;
It's probably tempting to test for the repeat count of the interested key in the callback, however, as mentioned in the documentation for WM_KEYDOWN, the repeat count is not cumulative. What that practically means is that the OS does not supply this information. The previous key state information is provided though. That would be bit 30 of the "lParam" of the callback. You can prevent any keyboard message when your shortcut keys are down, and the primary shortcut has been previously down, from reaching the window procedure of the focused control.
function KeyboardProc(code: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT;
stdcall;
begin
if code > 0 then begin
if (wParam = vk_f12) and (GetKeyState(VK_CONTROL) < 0) and
((lParam and $40000000) > 0) then begin
Result := 1;
Exit;
end;
end;
Result := CallNextHookEx(KeybHook, code, wParam, lParam);
end;
Lastly, don't disregard the probability that if a user's system has been set up with a fast keyboard repeat, it's the user's choice rather than not.
I am trying to program a "cough button" in a communications program that does not always have focus. I have the code working to mute and unmute the mic (MMDevApi) and it works perfect. I set up a global hot key and this works perfect to set the mute. Now the problem. How do I tell when the Hotkey is released ? I tried a timer as shown in the code but it has an odd behavior. I Hold down my hotkey and the mic mutes immediately then after the interval of the timer it unmutes for a what appears to be half the interval of the timer and then it mutes and stays muted. Without the timer it mutes and stays muted perfectly. I really don't want ( or think it would be any good ) to have to hit a second key to unmute the mic.
//here is my register hot key code !
CoughKeyWnd := AllocateHwnd(CoughKeyWndProc);
CoughKey := GlobalAddAtom('CoughKey');
if CoughKey <> 0 then
RegisterHotKey(CoughKeyWnd, CoughKey, MOD_CONTROL, VK_OEM_3);
//the procedure
procedure TForm1.CoughKeyWndProc(var Message: TMessage);
begin
if Message.Msg = WM_HOTKEY then
begin // to prevent recalling mute
if CoughOn = FALSE then
begin
CoughOn := True;
CoughOff.SetMute(1,#GUID_NULL);
end;
Timer1.Enabled := FALSE;
Timer1.Enabled := True;
end
else
begin
Message.Result := DefWindowProc(CoughKeyWnd, Message.Msg, Message.WParam, Message.LParam);
end;
//and finally the ontimer !
procedure TForm1.JvTimer1Timer(Sender: TObject);
begin
CoughOff.SetMute(0,#GUID_NULL);
Timer1.Enabled := False;
CoughOn := False;
end;
You'll see that kind of behavior if your timer expires before the second WM_HOTKEY is retrieved but does not expire retrieving consecutive messages. The time frame between the first and second message is greater than the time frame between consecutive messages. This is because keyboard delay is greater (~250ms typical) than keyboard repeat interval.
To make your approach work, increase your timer interval, something like twice the keyboard delay for instance. You can use SystemParametersInfo to get an approximation for keyboard delay. Or employ a minimum time frame for the microphone to stay in muted state, and only after then start watching for repeated hotkey messages to re-enable your timer. Still, this method will be somewhat unreliable, hotkey messages might be delayed for whatever reason. Better use GetKeyState in your timer handler to test if the keys are still down.
You can install a keyboard hook or register for raw input when the hotkey is hit if you don't want to use the timer.
how can i pause and resume in application while working with loops
i can put some sleep(xxx) at begininsg of my loop to pause ,but i want pause when ever i want and resume when i ever i need
any ideas ?
thanks in advance
ok here is alittle more explanation
for i:=0 to 100 do
begin
if button1.clicked then pause //stop the operation and wait for resume button
if button2.clicked then resume //resume the operations
end;
edit2 :
ok guys i will tell an example well take any checker for suppose proxy checker ,i have a bunch of proxies loaded in my tlistviwe i am checking them all ,by using lop for i:=0 to listview.items.count do ......
i want to pause my checking operation when ever i want and resume when ever i need
am i clear or still i have to explain some ? :S
regards
You need a boolean flag that will indicate whether it's safe to continue looping. If something happens that makes it so you need to pause, it should set the variable to false. This means that, unless you're working with multiple threads, whatever sets this flag will have to be checked inside the loop somewhere. Then at the top (or the bottom) of your loop, check to see if this variable is true and pause otherwise.
Here's the basic idea, in the context of your explanation:
procedure TForm1.DoLoop;
begin
FCanContinue := true;
for i:=0 to 100 do
begin
//do whatever
Application.ProcessMessages; //allow the UI to respond to button clicks
if not FCanContinue then Pause;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FCanContinue := false;
end;
procedure TForm2.Button1Click(Sender: TObject);
begin
FCanContinue := true;
Resume;
end;
This is a very simplistic implementation. What you really should do if you have a long-running task that you need to be able to pause and resume on command is put this in a separate thread and take a look at the TEvent class in SyncObjs.
OK, I don't understand what you are trying to do, but here is some pseudo code:
for i:=0 to 100 do
begin
if button1.clicked then
begin
while not button2.clicked do
sleep(50);
end;
end;
Check if a key is pressed?