Reputation: 3879
I am developing an application which runs in the background, and when a specific event is happening, the music which is played by media players should be stopped.
The major music players like Spotify and Windows Media Player support being controlled via special keyboard keys "Stop", "Start", "Next track" etc.
So I would like to use this feature to control all running media players and tell them to stop playing.
Media players which do not implement these keys won't be targeted by my application; they will be incompatible.
Currently, I have following function, which works so far:
procedure StopMusic;
const
KEYEVENTF_KEYDOWN = 0;
KEYEVENTF_KEYUP = 2;
begin
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
end;
I don't know exactly how this mechanism works. The operating system messages all applications, no matter in which application I am currently working in, but only if keybd_event is called. (SendMessage WM_KEYDOWN won't work)
The problem I am having is that some applications like OwnCloud swallow these keys if their window is focused (also if pressed on a real keyboard). I will contact them to report this bug. But in the meantime I want to try to make a workaround for it.
My idea for a workaround is to quickly switch the focus to a different window, then perform the virtual key press, then switch back to the window which was active:
procedure StopMusic;
const
KEYEVENTF_KEYDOWN = 0;
KEYEVENTF_KEYUP = 2;
var
hBak: HWND;
begin
hBak := GetForegroundWindow();
try
SetForegroundWindow(GetDesktopWindow());
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
finally
SetForegroundWindow(hBak);
end;
end;
But this function does not work at all. It has no effect, no matter which Window is currently focused. I guess it is because the Desktop can't gain the focus.
Therefore my question is:
UPDATE: Solution 1 (Dirty)
The first workaround is to switch the focus to the taskbar. But obviously, this is a dirty solution, since it is an implementation detail of Windows.
procedure StopMusic;
const
KEYEVENTF_KEYDOWN = 0;
KEYEVENTF_KEYUP = 2;
var
hBak: HWND;
begin
hBak := GetForegroundWindow();
try
SetForegroundWindow(FindWindow('Shell_TrayWnd', nil));
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
finally
SetForegroundWindow(hBak);
end;
end;
UPDATE: Solution 2 (Dirty)
This solution also works. It is not a hardcoded string like Shell_TrayWnd, but still using an undocumented API call.
function TaskmanWindow: HWND;
type
TGetTaskmanWindow = function(): HWND; stdcall;
var
hUser32: THandle;
GetTaskmanWindow: TGetTaskmanWindow;
begin
Result := 0;
hUser32 := GetModuleHandle('user32.dll');
if (hUser32 > 0) then
begin
@GetTaskmanWindow := GetProcAddress(hUser32, 'GetTaskmanWindow'); // Undocumented
if Assigned(GetTaskmanWindow) then
begin
Result := GetTaskmanWindow;
end;
end;
end;
procedure StopMusic;
const
KEYEVENTF_KEYDOWN = 0;
KEYEVENTF_KEYUP = 2;
var
hBak: HWND;
begin
hBak := GetForegroundWindow();
try
SetForegroundWindow(TaskmanWindow);
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
finally
SetForegroundWindow(hBak);
end;
end;
UPDATE: Solution 3 (Good)
The clean solution of the problem is to send an AppCommand to all applications
procedure StopMusic;
begin
SendMessage(HWND_BROADCAST, WM_APPCOMMAND, 0, MAKELONG(0, APPCOMMAND_MEDIA_STOP));
end;
UPDATE: Bugreport
A friend with an github account posted the bugreport for me.
Upvotes: 2
Views: 406
Reputation: 54792
... keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0); keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0); ...I don't know exactly how this mechanism works...
This code simulates pressing the VK_MEDIA_STOP
key which generates a WM_APPCOMMAND
message with the APPCOMMAND_MEDIA_STOP
application command. It's possible to learn what happens then from the message's documentation:
If a child window does not process this message and instead calls DefWindowProc, DefWindowProc will send the message to its parent window. If a top level window does not process this message and instead calls DefWindowProc, DefWindowProc will call a shell hook with the hook code equal to HSHELL_APPCOMMAND.
So, when a window of an application is in the foreground when you press the key, or simulate the input, if the focused control or its parent chain does not halt the processing of the message and the top level window calls the default window procedure, then a shell event is generated. The well behaving media players you mention are the ones that have registered to receive shell hook messages. This mechanism is also explained in this MSDN blog post.
What OwnCloud does is to, obviously, not calling the default window procedure after it receives a WM_APPCOMMAND
. When it is in the foreground this happens to cause your problem; other media applications cannot respond to the message since no shell event is generated.
Hence, your workaround of trying to activate a non-interfering window and generating keyboard input to that window. This is pointless, your own application's window is as good as the taskbar's or any other application's window as long as you don't do anything with the message. When your application's window is in the foreground and you generate input, if Windows Media Player is not responding, there is something else and we'd need a reproduction case for that.
In any case, generating a shell event will not be helpful to OwnCloud or similar applications since they don't have a registered window for shell hook notifications. To these applications, you can send the WM_APPCOMMAND
yourself. The linked MSDN blog post has an example of doing that. Although not suggested, you can also broadcast the message if you have a possibility of running an indefinite number of media applications. For a number of considerations why this is not suggested, see this MSDN blog post. If you don't see any problem broadcasting the message, consider using SendMessageTimeout
instead of SendMessage
.
Upvotes: 2