Detect key(s) held down in another process - delphi

I have a Delphi 2007 Win32 executable which sends keystrokes to other applications. My app is invoked from within these target applications by a hotkey like F11 or Shift+F11.
I want users to be able to hold down a key to abort the keystroke sending (say, if they realize they invoked my app in the wrong location). I had thought Shift, Ctrl, and Alt were good candidates because, alone, those key presses aren't likely to disrupt anything in the target application. (Escape, for instance, is a bad choice, as it might cause the target application to close one or more windows.)
I wrote the function farther below and call it periodically as follows, while sending keystrokes with the intent of detecting keys held down.
if wsAnyKeysDownInWindow( TgtWindow, [VK_Escape, VK_Menu{Alt}, VK_Control, VK_Shift] ) then
Abort;
Problem is, my app sends keystroke combinations like Shift+Tab and Ctrl+Home, which (I think) makes this approach fail--it always detects a down state for Shift and/or Ctrl. (I also tried a similar function which called SetKeyboardState just prior to beginning to send keystrokes, to set the key states' high-order (down) bit but that didn't help.)
Anyone think of a workable approach, short of hooking the keyboard?
function wsAnyKeysDownInWindow(Handle: HWnd; VKeys: array of byte): boolean;
{ Checks whether each of the VKeys set of virtual keys is down in Handle,
a window created by another process. }
var
OtherThreadID : integer;
State: TKeyboardState;
AKey: byte;
begin
Result := False;
if not IsWindow(Handle) then
exit;
OtherThreadID := GetWindowThreadProcessID( Handle, nil);
if AttachThreadInput( GetCurrentThreadID, OtherThreadID, True ) then try
GetKeyboardState(State);
for AKey in VKeys do
if (State[AKey] and 128) <> 0 then begin //If high-order bit is set, key is down
Result := True;
exit;
end;
finally
AttachThreadInput( GetCurrentThreadID, OtherThreadID, False );
end;
end;

Consider using Scroll Lock for this. It is rarely used (only of Excel comes to my mind), and you will have even visual indicator if keys are being sent or not.
BTW, Alt is not a good choice for another reason - it invokes the main menu in an application (if there is one, of course).

Related

How to avoid repeating execution of TAction.Shortcut?

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.

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.

Determine if SHIFT is pressed insde keyboard Hook Proc

inside my hook proc, how can i determine whether a used is pressing SHIFT (without releasing it) and then a char key (like A) ?
for example, if i press Shift+A, i want to know that it will be an uppercase a instead of getting it like Shift, a
so it will be A, if a user presses and releases Shift, it will capture Shift only.
the instaleld hook is WH_KEYBOARD (Global)
function KeyHookProc(Code: Integer; wVirtKey: WPARAM; lKeyStroke: LPARAM)
: LRESULT; stdcall;
type
TTransitionState = (tsPressed, tsReleased);
PKeystrokeData = ^TKeystrokeData;
TKeystrokeData = record
VirtualKey: WPARAM;
KeyStroke: LPARAM;
KeyState: TKeyboardState;
end;
var
Transition: TTransitionState;
KeystrokeDataPtr: PKeystrokeData;
begin
Result := CallNextHookEx(hKeyHook, Code, wVirtKey, lKeyStroke);
Transition := TTransitionState((lKeyStroke shr 31) and 1);
if (Code = HC_ACTION) and (Transition = tsPressed) then
begin
New(KeystrokeDataPtr);
try
KeystrokeDataPtr^.VirtualKey := wVirtKey;
KeystrokeDataPtr^.KeyStroke := lKeyStroke;
GetKeyboardState(KeystrokeDataPtr^.KeyState);
SendMessage(hConsole, WM_NULL, 0, LPARAM(KeystrokeDataPtr));
finally
Dispose(KeystrokeDataPtr);
end;
end;
end;
Here's the code we use in normal day-to-day use to detect the shift key. I've never used it in a hooked context, so I don't know if it would work there, or if something is different in that context that would prevent it.
function ShiftIsDown : Boolean;
var
State: TKeyboardState;
begin
WINDOWS.GetKeyboardState(State);
Result := ((State[vk_Shift] and 128) <> 0);
end;
You detect the pressing of the SHIFT key when your hook proc is called with wParam equal to VK_SHIFT.
When your hook proc is called corresponding to the A key being pressed, the wParam and lParam values are identical whether or not the SHIFT key is down. Since you are not calling TranslateMessage and DispatchMessage as would happen in a normal message loop, you are going to have to translate the raw key down/up events into actual key presses.
It's probably going to be easiest for you to use GetKeyState(VK_SHIFT)<0 to detect whether or not the SHIFT key is down. That depends on exactly what you are trying to do. It looks a little like you are making a fully functioned keylogger. In which case ad-hoc calls to GetKeyState(VK_SHIFT)<0 may not meet your needs, and proper processing of the individual key down/up messages would be better.
Some other comments:
Why are you heap allocating your TKeystrokeData record? You can perfectly well use a stack allocated record.
I hope that hConsole is a window in the same process as your hook. If not it won't receive any useful information because the pointer you send it is only meaningful in the process that defines it. If you want to send information cross-process them WM_COPYDATA is your guy.

Delphi Pascal Problem when WMDeviceChange function calls other functions/procedures

SOLVED
I am using delphi 2009. My program listens for usb drives being connected and remove. Ive used a very similar code in 10 apps over the past year. It has always worked perfectly. When i migrated i had to give up using thddinfo to get the drive model. This has been replaced by using WMI. The WMI query requires the physical disk number and i happen to already have a function in the app for doing just that.
As i test I put this in a button and ran it and it successfully determines the psp is physical drive 4 and returns the model (all checked in the debugger and in another example using show message):
function IsPSP(Drive: String):Boolean;
var
Model: String;
DriveNum: Byte;
begin
Result := False;
Delete(Drive, 2, MaxInt);
DriveNum := GetPhysicalDiskNumber(Drive[1]);
Model := (MagWmiGetDiskModel(DriveNum));
if Pos('PSP',Model) > 0 then Result := True;
end;
procedure TfrmMain.Button1Click(Sender: TObject);
var DriveNum: Byte;
begin
IsPSP('I');
end;
It works perfectly that is until i allow the WMDeviceChange that ive been using for a year to call up the getphysicaldisknumber and the wmi query statement. Ive tried them by themselves theyre both a problem. GetPhysicalDiskNumber freezes real bad when its doing a CloseHandle on the logical disk but does return the number eventually. The WMI query fails with no error just returns '' debugger points into the wbemscripting_tlb where the connection just never happened. Keep in mind the only thing thats changed in a year is what im calling to get the model i was using an api call and now im using something else.
Below is the rest of the code involved at this time sans the ispsp that is displayed above:
procedure TfrmMain.WMDeviceChange(var Msg: TMessage);
var Drive: String;
begin
case Msg.wParam of
DBT_DeviceArrival: if PDevBroadcastHdr(Msg.lParam)^.dbcd_devicetype = DBT_DevTyp_Volume then
begin
Drive := GetDrive(PDevBroadcastVolume(Msg.lParam)) + '\';
OnDeviceInsert(Drive);
end;
DBT_DeviceRemoveComplete: if PDevBroadcastHdr(Msg.lParam)^.dbcd_devicetype = DBT_DevTyp_Volume then
begin
Drive := GetDrive(PDevBroadcastVolume(Msg.lParam)) + '\';
OnDeviceRemove(Drive);
end;
end;
end;
Procedure TfrmMain.OnDeviceInsert(Drive: String);
var PreviousIndex: Integer;
begin
if (getdrivetype(Pchar(Drive))=DRIVE_REMOVABLE) then
begin
PreviousIndex := cbxDriveList.Items.IndexOf(cbxDriveList.Text);
cbxDriveList.Items.Append(Drive);
if PreviousIndex = -1 then //If there was no drive to begin with then set index to 0
begin
PreviousIndex := 0;
cbxDriveList.ItemIndex := 0;
end;
if isPSP(Drive) then
begin
if MessageDlg('A PSP was detect # ' + Drive + #10#13 + 'Would you like to select this drive?',mtWarning,[mbYes,mbNo], 0) = mrYes then
cbxDriveList.ItemIndex := cbxDriveList.Items.IndexOf(Drive)
else cbxDriveList.ItemIndex := PreviousIndex;
end
else if MessageDlg('USB Drive ' + Drive + ' Detected' + #10#13 + 'Is this your target drive?',mtWarning,[mbYes,mbNo], 0) = mrYes then
cbxDriveList.ItemIndex := cbxDriveList.Items.IndexOf(Drive)
else cbxDriveList.ItemIndex := PreviousIndex;
end;
end;
Procedure TfrmMain.OnDeviceRemove(Drive: String);
begin
if not (getdrivetype(Pchar(Drive)) = DRIVE_CDROM) then
begin
if cbxDriveList.Text = (Drive) then ShowMessage('The selected drive (' + Drive + ') has been removed');
cbxDriveList.Items.Delete(cbxDriveList.Items.IndexOf(Drive));
if cbxDriveList.Text = '' then cbxDriveList.ItemIndex := 0;
if Drive = PSPDrive then //Check Detect PSP and remove reference if its been removed
begin
PSPDrive := '';
end;
end;
end;
Rob has said something below about im not calling the inherited message handler, ive read the document i see a couple of things i can return... but im not really sure i understand but i will look into it. Im not a very good pascal programmer but ive been learning alot. The transition to 2009 has had some rough patches as well.
The USB drive detection and all that works perfectly. If i remove the two things from is psp the user is greeted right away with wis this your whatever and adds I:\ to the list. Its just the two new things that have changed in the app that fail when called by wmdevicechange and as said before they work on their own.
EDIT - SOLVED
Alright well im using a timer as suggested and the problem seems to be solved. One note is that when called by the timer very shortly after the wmdevicechange getting the physical disk number still seems to be slow. I attribute this to the device still being attached to the system.
On that note im using a P2 450 on the regular. I hooked the PSP and app to a 1.8Ghz Dual Core Laptop and the program detected the psp and notified the user very fast. So the app wont freeze unless there on a very very slow computer and on this slow onw its only for a matter of seconds and doesnt affect the operation of the program though isnt very cool. But i feel that all modern computers will run the detection fast especially because they can attach the device alot faster.
It's possible that the information you're querying becomes available only after the WMDeviceChange message handler runs. If the very same code works when called from a button, try this:
Refactor your WMDeviceChange handler code into one or more separate methods.
In the WMDeviceChange handler, activate a precreated timer and have it fire one second later, or something like that.
Call the former WMDeviceChange handler code from the timer handler code.
You haven't indicated what "statement 1" is in your code.
I have a few comments about parts of the code, which may or may not be related to the problem you're having.
First, you assign a value to DriveNum in IsPSP, but you don't use it. The compiler should have issued a hint about that; don't ignore hints and warnings. You also pass the magic number 4 into MagWmiGetDiskModel; was that supposed to be DriveNum instead?
You aren't calling the inherited message handler, and you aren't returning a result in your message handler. The documentation tells what values you're supposed to return. To return a value from a Delphi message handler, assign a value to the Msg.Result field. For the cases that your message handler doesn't handle, make sure you call inherited so that the next handler up the chain can take care of them. If there is no next handler, then Delphi will call DefWindowProc to get the operating system's default behavior.
The change you've illustrated is called refactoring, and it will do nothing to affect how your code runs. It makes the code easier to read, though, so please keep the second version. As for finding the problem, my best advice is to use the debugger to step through the code to identify the point where things stat to go wrong and the parts that run slower than you'd like. You can also try removing portions of the code to confirm that the other parts work correctly in isolation.

Delphi - Gracefully Closing Created Process in Service. (using tprocess / createProcess)

I have a Windows Service written in Delphi which runs a number of programs.
On Stopping the service, I want to also close these programs. When the service was originally written, this worked fine, but I think I've updated the tProcess component and now - The subordinate programs are not being closed.
in tProcess - Here's the code which starts the new processes.
if CreateProcess( nil , PChar( FProcess.Command ) , nil , nil , False ,
NORMAL_PRIORITY_CLASS , nil , Directory ,
StartupInfo , ProcessInfo ) then
begin
if FProcess.Wait then
begin
WaitForSingleObject( ProcessInfo.hProcess , Infinite );
GetExitCodeProcess( ProcessInfo.hProcess , ExitCode );
if Assigned( FProcess.FOnFinished ) then
FProcess.FOnFinished( FProcess , ExitCode );
end;
CloseHandle( ProcessInfo.hProcess );
CloseHandle( ProcessInfo.hThread );
end;
Each of the executables called by this are Windows GUI Programs (With a close button at the top).
When I stop the service, I also want to stop (not kill) the programs I've started up via the createProcess procedure.
How would you do this?
I'd use TJvCreateProcess component of JVCL which wraps about any process related functionality of win32 in a graceful way. This answer comes from Dont-touch-winapi-unless-really-required department :-)
You want to enumerate open windows that match your launched ProcessId and tell those windows to close. Here's some sample code for that:
uses Windows;
interface
function MyTerminateAppEnum(hHwnd:HWND; dwData:LPARAM):Boolean; stdcall;
implementation
function MyTerminateAppEnum(hHwnd:HWND; dwData:LPARAM):Boolean;
var
vID:DWORD;
begin
GetWindowThreadProcessID(hHwnd, #vID);
if vID = dwData then
begin
PostMessage(hHwnd, WM_CLOSE, 0, 0); //tell window to close gracefully
Result := False; //can stop enumerating
end
else
begin
Result := TRUE; //keep enumerating until you find your id
end;
end;
Then you'll want to utilize this in your code when you want to shut down the launched applications:
Procedure TerminateMe(YourSavedProcessInfo:TProcessInformation);
var
vExitCode:UINT;
begin
GetExitCodeProcess(YourSavedProcessInfo.hProcess, vExitCode);
if (vExitCode = STILL_ACTIVE) then //launched app still running..
begin
//tell it to close
EnumWindows(#MyTerminateAppEnum, YourSavedProcessInfo.dwProcessId);
if WaitForSingleObject(YourSavedProcessInfo.hProcess, TIMEOUT_VAL) <> WAIT_OBJECT_0 then
begin
if not TerminateProcess(YourSavedProcessInfo.hProcess, 0) then //didn't close, try to terminate
begin
//uh-oh Didn't close, didn't terminate..
end;
end;
end;
CloseHandle(YourSavedProcessInfo.hProcess);
CloseHandle(YourSavedProcessInfo.hThread);
end;
The only generic way to stop a process is to use TerminateProcess. But that's as far from graceful as you can get. To gracefully close a process, you need to tell the process that you'd like it to stop, and then hope it obeys. There is no way to do that in general, though.
For a GUI program, the usual way to tell it that you want it to stop running is to close its main window. There's no formal idea of "main window," though. A program can have zero or more windows, and there's no way to know externally which one you're supposed to close in order to make the program stop working.
You could use EnumWindows to cycle through all the windows and select the ones that belong to your process. (They'd be the ones for which GetWindowThreadProcessId gives the same process ID that CreateProcess gave you.)
Closing a window might not be enough. The program might display a dialog box (prompting for confirmation, or asking to save changes, etc.). You would need to know in advance how to dismiss that dialog box.
Non-GUI programs can have similar problems. It might be enough to simulate a Ctrl+C keystroke. It might catch and handle that keystroke differently, though. It might have a menu system that expects you to type "Q" to quit the program.
In short, you cannot gracefully close a program unless you know in advance how that program expects to be closed.

Resources