How to get rid of windows sounds when capturing CTRL + S? - delphi

In my application, when I press CTRL + S, my form (with Key Preview enabled) captures this and saves the document. But when the focus is in, for example, an edit control, I get an annoying "Ding" sound, or in general, windows sounds. How do I avoid this sound?
Here's my form's capture of this key event...
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
C: String;
begin
if not fChanging then
Modified;
if ssCtrl in Shift then begin
C:= LowerCase(Char(Key));
if C = 's' then begin
DoSave;
Key:= 0; //Tried this but didn't work
end else
if C = 'c' then begin
//Copy selected item(s)
end;
end;
end;
PS - Is there a more standard way of capturing these events? Because I'm sure I'm doing something wrong, and I'm sure there's another way I should be getting these key events without sounds.

A couple of things:
Try putting your code into FormKeyPress instead of FormKeyDown. This will make the Key := 0; code work... You will need to handle the CTRL checking manually though, by using something like GetKeyState() (I originally had GetAsyncKeyState() here, but as Rob Kennedy points out, GetKeyState() is a much better option).
Use an Action instead. Plop a TActionList on your form, double click on it, add an action and set it's hot key to CTRL-S. Add your save code to it's OnExecute event handler. This is the "proper" way to do it I believe.
Hope this helps.

Why don't you use Actions? That is the best way to handle shortcuts.

Here is some Delphi code to disable the system beep.

Related

Problem when using return key to move to TDBGrid from TEdit

In a search form I have two components, a TEdit and and TDBGrid. The user enters text into the TEditand in the TEdit.OnChange event I search for matching items in a table using a query. These are displayed in a TDGrid. The event only triggers when the user has entered more than three characters in the TEdit.Textto cut down the overhead.
The problem occurs when the search has returned all possible records and the user wants to select one of the records displayed in the TDBGrid. While I told the users to use the Tab key (or the mouse) to switch from the TEdit to the TDBGrid, they use the return key to do this in another application and are insisting that they should be able to do the same thing in the application I'm altering/adding to. I can understand this, but the problem is that if I use
if (key = VK_RETURN) then key := VK_TAB;
in the TEdit.OnKeyUp event, the original value of the last key pressed in the TEdit is "remembered" as VK_RETURN and is passed to the TDBGrid's OnKeyUp event. Since I have other actions triggered in that event, I get undesired things happening as soon the the TDBGrid gains focus because they also want to select the correct row in the grid by pressing the return key again.
Therefore what I want to do is to "cancel" the key value passed to the TDBGrid from the TEdit. I tried using if (Sender = TEdit) then Key := VK_CANCEL; in the DGBrid's OnKeyUp event but I get a compiler "Incompatible types" error and I can't find anything to tell me how I should use it in this situation.
Is this possible? Or am I going about this the wrong way?
Thanks in advance!
I believe the following approach satisfies your needs and I suspect it to be the shortest such approach:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if Key = #13 then
Key := #0;
end;
procedure TForm1.Edit1KeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
if Key = VK_RETURN then
begin
PostMessage(Handle, WM_NEXTDLGCTL, 0, 0);
Key := 0;
end;
end;
This will make Enter, in Edit1, move focus to the next control. This is accomplished by posting the form a WM_NEXTDLGCTL message. The OnKeyPress handler is needed to suppress the "invalid input" message beep.
For bonus points, do
PostMessage(Handle, WM_NEXTDLGCTL, Ord(ssShift in Shift), 0);
instead to have Shift+Enter correspond to Shift+Tab.
I'm not quite sure I like this entire idea, though. I think I prefer to have Enter act on key down instead. The target control shoudn't care about this key up message.
Just to add to Andreas answer, I have used the same approach. There are 3 events you can play around with, onkeydown, onkeypress, onkeyup. At some point you will probably have to assign the key to null to avoid the "beep". The other thing to watch out for are unintended side effects, like moving 2 fields instead of one as has happened to me in the past.

Deleting a dynamically created button a runtime

I need the user to be able to right click the button and it deletes itself but the following code isn't working
procedure TForm1.Button1Click(Sender: TObject); ////////Creates a new object
var
ExampleButton : TButton;
Begin
ExampleButton := TButton.Create(self); //Creates an object the same as its self
ExampleButton.Parent := self;
//Button properties go here
//Procedures called here
ExampleButton.OnMouseDown := DragOrDelete;
end;
Above creates the button, below I try to delete it
procedure TForm1.DragOrDelete(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
CursorPosition := Mouse.CursorPos; ////Location of mouse
ExampleButtonStartingLocation := TPoint.Create((Sender as Tbutton).Left, (Sender as Tbutton).Top);
if Button = mbRight then
FreeAndNil(TButton);
end;
The error I get is constant object cannot be passed as a var parameter.
Is it because I create numerous TButtons but the program doesn't know which one to refer one.
Well,
FreeAndNil(TButton);
should be
(Sender as TButton).Free; // thanks to DH
But this is not good. The RTL routines that call the event handler will still have a reference to the button, and need to continue accessing it after the event handler exits, so freeing it may cause further problems (also, Sender is not a var parameter, so setting it to nil will have no effect in the caller).
A better option might be to do something like creating a custom message with Sender as the wParam and posting it to the main form.
Edit
To do this you would create a user message, e.g.
const
WM_DELETE_CONTROL = WM_USER +1;
and replace the offending line with
PostMessage( FormMain.WindowHandle, WM_DELETE_CONTROL, WPARAM( Sender ), 0 );
Then create a procedure in your main form to handle the message, e.g.
procedure DestroyButton( var Msg : TMessage); message WM_DELETE_CONTROL;
with a definition like
procedure TForm1.DestroyButton( var Msg : TMessage);
begin
// RemoveControl( TButton( Msg.LParam ));
// correction - thanks to Remy Lebeau
TButton( Msg.WParam ).Free;
end;
You should make two changes there.
1) you should remember which object you created into a variable, living long enough that both procedures can access it.
2) you should destroy that object by the variable, mentioned above
Right now you are trying to destroy just some any random button. But that is hardly what you need! You probably want to destroy exactly the button you was creating, not some another one.
So:
1) FTableButton should be moved out of procedure TForm1.Button1Click and promoted into a variable of TForm1 class.
2) procedure TForm1.Button1Click should check if the button was already created and not create the second, third, forth... buttons.
procedure TForm1.Button1Click(Sender: TObject);
var
TableString : String;
Begin
if nil <> Self.FTableButton then
raise Exception.Create('Dynamic button already exists!!!');
TableString := IntToStr(TableNumber);
Self.FTableButton := TButton.Create(self);
....
Alternatively, you might choose to delete already existing button (if any) before creating a new one. Usually that is not a good idea, but in some specific scenarios it might make sense (when you need to "reset" the button object, to drop old customized object and create a new one with non-customized default properties).
procedure TForm1.Button1Click(Sender: TObject);
var
TableString : String;
Begin
Self.FTableButton.Free; // if there already was a dynamic button - destroy it
TableString := IntToStr(TableNumber);
Self.FTableButton := TButton.Create(self);
....
3) Now that you have that specific button remembered inside form's FTableButton variable you can use it to delete that very specific object.
procedure TForm1.ButtonMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
...
if Button = mbRight then
FreeAndNil( Self.FTableButton );
end;
Now there is another potential issue here - the "cleanup after suicide" issue.
David Heffernan in comments states that you can not delete the TButton from its own event handler. David states that after you exit the TForm1.ButtonMouseDown procedure the just deleted button would (or at least potentially may) do some more after-event actions with itself without recognizing it was already deleted, leading to any potential error, most probably Access Violation of nil dereference kind.
I can agree with half of that claim.
1) using TForm and TButton from VCL library for Windows is safe, because VCL (at least in Delphi XE2 version) was carefully designed to avoid this trap. The button's internal event calling sequence is designed to never directly address itself after the TForm1.ButtonMouseDown exited. So, at least in that narrow case David is not correct and that is safe thing to do.
2) That is called "relying over implementation detail" however. It is bad style because it is fragile. It might work in some specific case but suddenly break from many other reasons.
As soon as you would switch from the standard VCL TButton to some other fancy buttons from any other library ( Dev Express, LMD, TMS, anything ) that code my suddenly broke. Or you might switch form VCL to FMX. Or switch from FMX/Win32 to FMX/Android.
Or maybe just upgrading Delphi to some newer version would have VCL broken in that regard (that is highly unlikely but possible nonetheless).
So, to play safe you have to decouple those two actions: the button's event handling and the process of its deletion. There are many possible ways to do it, but they all demand more or less extra work and some understanding more issues. Those ways I see split into two avenues.
1) Timing would not change, killing the button would be immediate. But it should not be button deleting itself, it should be some other component.
That is the approach I like the most. I think deleting button on right-click is not the good idea. User might click it randomly, by mistake, by sudden twitch of the hand. Doing something as extreme as suddenly deleting button would be way too harsh change for a random erratic action.
I think you should go traditional way here. Right click should open the context menu, and in the menu there should be the command to delete the button. That way, the menu would be deleting the button, not the button itself.
You would need to add the TPopupMenu onto the form having one single element - deleting the button.
object mnu1: TPopupMenu
object mniFreeBM: TMenuItem
Caption = 'Free the button'
OnClick = mniFreeBMClick
end
end
procedure TForm18.mniFreeBMClick(Sender: TObject);
begin
FreeAndNil( Self.btnMenu );
end;
Then you would have to connect that menu to the button.
.....
Self.FTableButton := TButton.Create(self);
// FTableButton.OnMouseUp := ButtonMouseUp; -- no more doing it! bad style!
Self.PopupMenu := mnu1;
mnu1.AutoPopup := True;
.....
Here we go. When user would R-click the button - there would come the menu, asking him if he wants to delete that button. If he does - then menu, not the button itself, would be freeing it. If users cancels the menu - then it was his random action and he want the button to be kept alive.
That is the better approach as I see.
2) other approaches revolve around idea of time delay, they assure that button only asks Delphi to be deleted someday later, but there would be no immediate kill.
Delphi when realizing it was asked to do it, would then delete the button some later time, after OnMouseUp procedure (and probably few other event-handling procedures too) are long executed and exited.
If the application is not heavy loaded with looong heaaavy computations, then that "later" is very very short actually. Most probably the user would never be able to see the difference. For playing safe, the button might also make itself invisible until someone would delete it.
There can be many approaches to do it, but I would list two.
2.1) DSM in his answer outlines Post_Message-centered approach. That is a good solid "old school" code. It is fast. It is well-understood. But it also has some limitations.
a) it only works on Windows. Forget Android, iOS and others. Well, if you only intend to work on Windows then you can just use standard VCL buttons which can suicide safely, at least in Delphi XE2.
b) it needs you to make a lot of boiler plate, like declaring extra procedures and constants.
c) it is unsafe - you have to make hard unchecked typecasts between integers and button pointers. Easy to make mistake typing/refactoring.
d) you need to understand Windows implementation: message loop, VCL place inside that message loop, difference between PostMessage, SendMessage and Perform, etc.
That is not a rocket science. For any "old school" desktop programmer it is easy and well known. Well, if you were one you would not ask such a question.
Another approach would use multi-threading. From performance perfectionism point of view that is abomination. Creating a new thread (quite the expensive operation!) just to call back and ask the button to be deleted - is very inefficient. But - that way you have much less code to write. You can use standard Delphi features that would most probably work with every operating system and every forms/buttons library, in current and future Delphi versions.
The code is like that.
procedure TForm18.btnThreadMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
......
if mbRight = Button then
TThread.CreateAnonymousThread( procedure
begin
TThread.CurrentThread.Synchronize( nil, procedure
begin
FreeAndNil( btnThread );
end
);
end
).Start;
end;
This way when you exit the OnMouseButtonUp the button is not being deleted, there only is incoming request from the temporary thread to delete it. When the form would work that request out may differ, but anyway it would be another event that happens after you safely exited the button's event handler. Unless you used another abomination ProcessMessages but you did not and hopefully you never ever would.

How do I add the key binding Shift+Ctrl+H X to the Delphi IDE using the ToolsApi?

Adding a new ShortCut to the Delphi IDE is not too difficult because the Open Tools API provides a service for this. I am trying something apparently more complex: Add a Wordstar like additional ShortCut:
I want something to happen when the user presses
Shift+Ctrl+H followed by the single key X
where X should work regardless of the state of the Shift key.
This is my code:
procedure TGxKeyboardBinding.BindKeyboard(const BindingServices: IOTAKeyBindingServices);
const
DefaultKeyBindingsFlag = kfImplicitShift + kfImplicitModifier + kfImplicitKeypad;
var
GExpertsShortcut: Byte;
ShiftState: TShiftState;
FirstShortCut: TShortCut;
SecondShortCut: TShortCut;
begin
GExpertsShortcut := Ord('H');
ShiftState := [ssShift, ssCtrl];
FirstShortCut := ShortCut(GExpertsShortcut, ShiftState);
SecondShortCut := ShortCut(Ord('X'), []);
BindingServices.AddKeyBinding([FirstShortCut, SecondShortCut],
TwoKeyBindingHandler, nil,
DefaultKeyBindingsFlag, '', '');
end;
So, if I set ShiftState := [ssCtrl] pressing
Ctrl+H X
calls my TwoKeyBindingHandler method.
But with ShiftState := [ssShift, ssCtrl] pressing
Shift+Ctrl+H X
does nothing.
Oddly enough, when specifying ShiftState := [ssShift, ssCtrl] (which should only affect the first key) pressing
Shift+Ctrl+H Shift+X
calls my TwoKeyBindingHandler method, even though the second ShortCut is added without a modifier key.
Any idea? Is this maybe a known limitation/bug of the Delphi IDE/Open Tools API? Is there a known workaround?
I tried it in Delphi 2007 and Delphi 10 Seattle, no difference.
You should be able to do it using the GetKeyState function.
The program has two operations - Think of it as opening a drop down menu item. When ctr-shift-h is pressed your programme will need to flag that the 'Menu' is now open and that subsequent keypresses will either activate an option or close the 'menu' if an invalid key is presses.
function IsKeyDown(const VK: integer): boolean;
begin
IsKeyDown := GetKeyState(VK) and $8000 <> 0;
end;
procedure Form1.OnkeyDown(...)
begin
if Not H_MenuOpen then
if IsKeyDown(vk_Control) and IskeyDown(vk_Shift) and IsKeyDown(vk_H) then
begin
//Some Boolean in the form
H_MenuOpen:=True;
//Will probably need to invalidate some parameters here so that
//no control tries to process the key
exit;
end;
if H_MenuOpen then
begin
if key=vk_X then
begin
//x has been pressed
*Your code here*
//possibly invalidate some of the params again
exit;
end;
//Nothing valid
H_MenuOpen:=False;
end;
end;
OK, since apparently nobody has found an answer, here is what I ended up doing:
I had already planned to show a hint window listing all possible characters for the second key (actually that code was already working fine, using the approach suggested by Helen Fairgrieve in her answer to this question). Instead, I now register only a one-key shortcut:
BindingServices.AddKeyBinding([FirstShortCut],
TwoKeyBindingHandler, nil,
DefaultKeyBindingsFlag, '', '');
And in the TwoKeyBindingHandler method, I show a popup menu which contains those characters as the shortcuts. The IDE/VCL/Windows then handles the rest for me.
This is what it looks like:
It's not an answer to the actual question but it solves my problem. Sorry if you got here expecting something more.

Delphi: Convert keys to letters, ignoring shiftsate

I'm trying to make a search feature where the user can hold in the control key and type some text to search.
I'm using the OnKeyDown/OnKeyUp to trap the control key.
Is there a easy way to check if the key parameter given to the onKeyUp/Down event is a litteral?
Or is it possible to convert the char given to OnKeyPressed while holding down the control key to the char it would have been if the control key where not pressed?
Edit:
I need a solution that can handle letters beyond the simple a..z range, like æ, ø å.
It looks like Delphi 2009 got a couple of useful methods in the TCharachter-class; e.g. function IsLetterOrDigit(C: Char): Boolean;
I'm stuck with delphi 2007, though...
The OnKeyDown & OnKeyPress Events have a number of Limitations.
Rather use a TApplicationEvents Component.
In it's OnShortCut Event hander use the following code
procedure TFormA.ApplicationEvents1ShortCut(var Msg: TWMKey; var Handled: Boolean);
begin
if (Msg.CharCode = Ord('B')) and (HiWord(Msg.KeyData) and KF_ALTDOWN <> 0) then
begin
Handled := True;
// Do what needs to be done;
end;
end;
This will trap ALT-B
have a look at Delphi - Using the TApplicationEvents OnShortCut event to detect Alt+C key presses for more info
You can convert the Key(Char) parameter from OnKeyPress event to it's ordinal value using Ord(Key) however, in the OnKeyDown event you can do the following
if CharInSet(Char(Key), ['A'..'Z']) then
do something
you might also want to play with ShiftState to check if ALT, CTRL and/or SHIFT key is down...
Here you have all the info:
Virtual Key Codes
Understanding and Processing Keyboard events in Delphi

How to avoid the ding sound when Escape is pressed while a TEdit is focused?

In code I have developed some years ago I have been using this a lot to close the current form on pressing the Escape key at any moment:
procedure TSomeForm.FormKeyPress(Sender: TObject; var Key: Char);
begin
if key = #27 then close;
end;
This behaviour is defined for the TForm. The form's KeyPreview property is be set to True to let the form react to key presses before any other components. It all works perfectly well for the best part of the program, however, when the Escape key is pressed while a TEdit component is focused a sound (a ding sound used by Windows to signify invalid operation) is issued. It still works fine but I have never quite managed to get rid of the sound.
What's the problem with this?
Steps to recreate:
new VCL Forms application, set the form's KeyPreview to true
on the Events tab double-click the onKeyPress event and enter dummy code:
if key=#27 then ;
add a TListBox, TCheckBox, TEdit to the form and run the application
in the application try pressing Esc and NOTHING happens, as specified by the dummy code
focus the TEdit and press Esc. Nothing happens but the sound is played.
You get the ding because you left the ESC in the input. See how Key is a var? Set it to #0 and you eliminate the ding. That removes it from further processing.
procedure TSomeForm.FormKeyPress(Sender: TObject; var Key: Char);
begin
if key = #27 then
begin
key := #0;
close;
end;
end;
KeyPreview is just that, a preview of what will be passed to the controls unless you stop it.
Starting from Jim's answer (thanks Jim) I had to make it work for me. What I needed was to make a dropped down combobox close keeping the selected item and move to the next/previous control when TAB/shift+TAB was pressed. Everytime I did press TAB the annoying sound filled the room. My work arroud was using onKeyDown event to catch the shiftstate, declaring var aShift: boolean; in form's interface and use the following code:
procedure TForm2.StComboKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
if ssShift in Shift then aShift := true else aShift := false;
end;
procedure TForm2.StComboKeyPress(Sender: TObject; var Key: Char);
begin
if Key=char(VK_TAB) then
begin
Key := #0;
StCombo.DroppedDown := false;
if aShift
then previousControl.SetFocus
else nextControl.SetFocus;
end;
end;
Using the menu items and setting them to invisible, and using the shortcut, is a quick workaround that I've just stumbled across, but won't work if you need a shortcut that uses a character that is used in the first letter of an existing shortcut: For example for Alt+ENTER, you need to add something like this to the form create procedure:
MainMenu1.Items[0].ShortCut:=TextToShortCut('Alt+e');
However it's probably easier to use TActionList instead, and even though something like Alt+E is not listed you can add it.
It's an old thread... but anyway, here's a far better one: catching Alt-C!
Unlike ESC, Alt-C isn't serviced by KeyPress, so setting Key to #0 in KeyPress doesn't work, and the horrendous "ding!" is issued every time.
After hours of trying, here's the workaround I found:
- create a main menu option to service the request
- set its ShortCut to Alt+C - yes indeed, that is NOT one of the available ShortCut choices(!!)... but it does work anyway!
- do the processing in that menu option's OnClick
- you may even make in "in the background": you may set the menu option's Visible to false - as long as its Enabled stays true, it will be activated by Alt-C even though it will not be visible in the menu.
Hope that may help! And if you have something more elegant, please advise.

Resources