Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Always on Top

0.00/5 (No votes)
26 Nov 2003 2  
A DLL which creates a system hook to trap WM_INITMENUPOPUP and append an "Always on Top" option to all system menus.

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.

  1. Hiding a dialog-based application
  2. Creating a system tray icon
  3. Installing a system wide hook from a dll
  4. 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 /*=NULL*/) : 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; // delete hidden dialog

}

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();    // call default proc

    ClearHook (m_hWnd);    // get rid of all of our hooks in the DLL

    PostNcDestroy ();        // to ensure that hidden dialog is deleted

}

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)
{    
    // Delegate all the work back to the default

    // implementation in CSystemTray.

    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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here