65.9K
CodeProject 正在变化。 阅读更多。
Home

始终置顶

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (12投票s)

2003年11月23日

3分钟阅读

viewsIcon

189130

downloadIcon

4224

一个 DLL,它创建了一个系统钩子来捕获 WM_INITMENUPOPUP 并将“始终置顶”选项添加到所有系统菜单中。

引言

在观看流媒体网络视频时,我喜欢同时进行多任务处理并使用其他应用程序。但是,与 Windows Media Player 不同的是,当视频嵌入 Internet Explorer 中时,没有“始终置顶”设置可以让我这样做。

Power Menu 是网络上提供的几种解决方案之一,可惜没有源代码 :(,但它有一些我不需要的附加选项。当然,我当时想“我自己也可以编写这个……”。

编码问题

此项目引发了许多编码问题。

  1. 隐藏基于对话框的应用程序
  2. 创建系统托盘图标
  3. 从 DLL 安装系统范围的钩子
  4. 挂接系统菜单事件并附加自定义项

尽管其中一些问题已在 Code Project 的其他地方处理过,但本文应能作为它们实际应用的有用示例。

隐藏基于对话框的应用程序

CDialog 类的默认行为使得隐藏基于对话框的 MFC 应用程序变得困难。为确保隐藏基于对话框的应用程序,需要执行以下步骤。

在 Visual Studio 资源编辑器中,将对话框的“可见”属性设置为 false

删除应用程序 InitInstance() 中对 DoModal 的引用

BOOL CAOTApp::InitInstance()
{
    CWinApp::InitInstance();
    m_pMainWnd = new CAOTDlg;
    return TRUE;
}

创建新的 CAOTDlg 时,手动创建对话框。

CAOTDlg::CAOTDlg(CWnd* pParent /*=NULL*/) : CDialog(CAOTDlg::IDD, pParent)
{ 
    m_hIcon = AfxGetApp()->
    LoadIcon(IDR_MAINFRAME);
    Create (IDD, pParent);
}

完成后,对话框需要自我销毁。我们可以做到这一点最晚的时间是在 PostNcDestroy() 中。

void CAOTDlg::PostNcDestroy()
{
    delete this; // delete hidden dialog
}

在某些情况下(这种情况就是其中之一),PostNcDestroy 不会自动调用,因此我们必须自己调用它。

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
}

系统托盘图标

Chris Maunder 的 CSystemTray 类 使添加系统托盘图标变得非常容易。

m_TrayIcon.Create (this,
        WM_ICON_NOTIFY, 
        "Always on Top", 
        m_hIcon, 
        IDR_TRAYMENU);

要实现右键单击菜单(带默认项),我们只需要使用与托盘图标相同的 ID(即 IDR_TRAYMENU)在资源编辑器中创建一个菜单。然后所有消息都将传递给 CSystemTray 类进行处理。

LRESULT CAOTDlg::OnTrayNotification(WPARAM wParam, LPARAM lParam)
{    
    // Delegate all the work back to the default
    // implementation in CSystemTray.
    return m_TrayIcon.OnTrayNotification(wParam, lParam);
}

从 DLL 安装系统范围的钩子

所有系统范围的钩子都必须从 DLL 运行,以便可以从任何其他进程访问它们。创建 DLL 的基本要点如下。

为确保过程名称正确导出,并且不被链接器进行名称混淆,它们应该以 extern "C" 开头。

extern "C"
{
    static LRESULT CALLBACK ShellProc(int nCode, 
        WPARAM wParam, LPARAM lParam);
    static LRESULT CALLBACK MenuProc(int nCode, 
        WPARAM wParam, LPARAM lParam);
}

需要由使用 DLL 的不同进程(例如主窗口的句柄)访问的任何数据都需要存储在共享数据段中,并给出适当的链接器指令。

#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")

DLL 的入口点是 DllMain 而不是 WinMain。如果链接器给出错误,提示 _dllMain 已定义,则应从链接器开关中删除 _USRDLL

挂接系统菜单并附加自定义项

通过挂接 WH_CALLWNDPROC 并处理 WM_INITMENUPOPUP 来添加新菜单项。如果 lParam 的高位字为 TRUE,则调用的菜单是系统菜单,我们可以添加我们的菜单项。

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");
            }
        }
    }

起初我没有 bother 在菜单消失时将其移除,但这显然会在主对话框关闭且钩子被移除后仍然保留菜单项。

要删除新菜单项,需要处理 WM_MENUSELECT 消息。当菜单消失时,lParam 将为 NULLwParam 的高位字将为 0xFFFF。如果存在,则删除 ID 为 AOT_AOT 的菜单项。

    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);
        }
    }

菜单项本身通过挂接 WH_GETMESSAGE 并处理 WM_SYSCOMMAND 来处理,其中 wParam 的低位字是我们自定义菜单消息 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);
        }
    }

Bug

有两个有时会出现的错误

  • “始终置顶”并不总是第一次调用时附加到系统菜单,尽管如果取消并再次调用,它就会在那里。 Winspector Spy 表明第一次调用 WM_INITPOPMENU 时,lParam 在这些情况下是不正确的。
  • 单击任务栏中活动应用程序的图标并不总是会将其最小化(直到您右键单击其图标),

欢迎提出任何想法!

© . All rights reserved.