Introduction
When watching streaming internet video I like to multitask and use other
applications at the same time. However, unlike Windows Media Player, when the
video is embedded in Internet Explorer there is not an "Always on Top" setting
which will allow me to do this.
Power
Menu is one of several solutions the web offers without source code :(, but
has a number of extraneous options which I don't need. And of course I thought
"I could code that myself ...".
Coding issues
There were a number of coding issues raised by this project.
- Hiding a dialog-based application
- Creating a system tray icon
- Installing a system wide hook from a dll
- Hooking the system menu events and appending a custom item
Although some of these have been dealt with elsewhere on The Code Project,
this article should hopefully act as a useful example of their practical
application.
Hiding a dialog-based application
The default behaviour of the CDialog
class makes it difficult to
hide an MFC dialog-based application. The following steps are necessary to
ensure that a dialog-based application is hidden.
In the Visual Studio resource editor, set the Visible property of the dialog
to false
Remove references to DoModal
in the application's
InitInstance()
BOOL CAOTApp::InitInstance()
{
CWinApp::InitInstance();
m_pMainWnd = new CAOTDlg;
return TRUE;
}
When the new CAOTDlg is constructed, create the dialog box manually.
CAOTDlg::CAOTDlg(CWnd* pParent ) : CDialog(CAOTDlg::IDD, pParent)
{
m_hIcon = AfxGetApp()->
LoadIcon(IDR_MAINFRAME);
Create (IDD, pParent);
}
When we are finished with it, the dialog needs to destroy itself. The last
possible time we can do this is in PostNcDestroy()
.
void CAOTDlg::PostNcDestroy()
{
delete this;
}
In some cases (and this is one of them) PostNcDestroy
does not
get called automatically, so we have to call it ourselves.
void CAOTDlg::OnDestroy()
{
CDialog::OnDestroy();
ClearHook (m_hWnd);
PostNcDestroy ();
}
System tray icon
Chris Maunder's
CSystemTray class makes it very easy to add a system tray icon.
m_TrayIcon.Create (this,
WM_ICON_NOTIFY,
"Always on Top",
m_hIcon,
IDR_TRAYMENU);
To implement a right click menu (with a default item), we only need create a
menu in the resource editor with the same ID as the tray icon ie
IDR_TRAYMENU
. All messages are then passed to the CSystemTray class
to deal with.
LRESULT CAOTDlg::OnTrayNotification(WPARAM wParam, LPARAM lParam)
{
return m_TrayIcon.OnTrayNotification(wParam, lParam);
}
Installing a system wide hook from a dll
All system wide hooks have to run from a dll so that they can be accessed
from any other process. The basic points of creating a DLL are as follows.
To ensure procedure names are exported correctly, and not mangled by the
linker, they should be prefaced with extern "C".
extern "C"
{
static LRESULT CALLBACK ShellProc(int nCode,
WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK MenuProc(int nCode,
WPARAM wParam, LPARAM lParam);
}
Any data which needs to be accessed across different processes which use the
dll (eg the handle to the main window) needs to be held in a shared data
segment, and the appropriate linker directive given.
#pragma data_seg(".SHARDATA")
HWND g_hwndMain = NULL;
HHOOK g_hookShell = NULL;
HHOOK g_hookMenu = NULL;
HINSTANCE g_hInstance = NULL;
UINT AOT_POPUP;
UINT AOT_AOT;
#pragma data_seg()
#pragma comment(linker, "/section:.SHARDATA,rws")
The entry point for a dll is DllMain
rather than
WinMain
. If the linker gives an error that _dllMain
is
already defined, _USRDLL
should be removed from the linker
switches.
Hooking the system menu and appending a custom item
The new menu item is appended by hooking WH_CALLWNDPROC
and
handling WM_INITMENUPOPUP
. If the high order word of
lParam
is TRUE
, the menu called is the system menu and
we can add our menu item.
g_hookShell = SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)ShellProc,
g_hInstance, 0);
.
.
.
CWPSTRUCT *wps = (CWPSTRUCT*)lParam;
HWND hWnd = (HWND)(wps->hwnd);
if(wps->message == WM_INITMENUPOPUP)
{
HMENU hMenu = (HMENU)wps->wParam;
if ((IsMenu (hMenu) & (HIWORD(wps->lParam) == TRUE)))
{
if (GetWindowLong(hWnd, GWL_EXSTYLE) & WS_EX_TOPMOST)
{
AppendMenu (GetSystemMenu (hWnd, FALSE),
MF_CHECKED | MF_BYPOSITION | MF_STRING,
AOT_AOT,
"Always on top");
}
else
{
AppendMenu (GetSystemMenu (hWnd, FALSE),
MF_BYPOSITION | MF_STRING,
AOT_AOT,
"Always on top");
}
}
}
Originally I did not bother removing the item from the menu when it was
dismissed, but that obviously kept the menu item in existence even after the
main dialog had been closed and the hook removed.
To remove the new menu item, the WM_MENUSELECT
message is
handled. When a menu is dismissed, lParam
will be NULL
and the high order word of wParam
will be 0xFFFF
. If
it exists, the menu item with ID AOT_AOT
is removed.
CWPSTRUCT *wps = (CWPSTRUCT*)lParam;
HWND hWnd = (HWND)(wps->hwnd);
if (wps->message == WM_MENUSELECT)
{
if((wps->lParam == NULL) && (HIWORD(wps->wParam) == 0xFFFF))
{
RemoveMenu (GetSystemMenu (hWnd, FALSE),
AOT_AOT,
MF_BYCOMMAND);
}
}
The menu item itself is handled by hooking WH_GETMESSAGE
and
handling WM_SYSCOMMAND
where the low order word of
wParam
is our custom menu message, AOT_AOT
.
MSG *msg = (MSG *)lParam;
if ((msg->message == WM_SYSCOMMAND) && (LOWORD(msg->wParam) == AOT_AOT))
{
if (GetWindowLong(HWND(msg->hwnd), GWL_EXSTYLE) & WS_EX_TOPMOST)
{
SetWindowPos(HWND(msg->hwnd),
HWND_NOTOPMOST,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
}
else
{
SetWindowPos(HWND(msg->hwnd),
HWND_TOPMOST,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
}
}
Bugs
There are two bugs which sometimes occur:
- The "Always on Top" is not always appended to the system menu the first time
it is called, although if dismissed and then recalled it is there. Winspector Spy
indicates that the first time
WM_INITPOPMENU
is called, the
lParam
is incorrect in these cases.
- Clicking an active application's icon in the taskbar does not always
minimize it (until you have right clicked on its icon),
Any thoughts gratefully received!