使用C#检测活动窗口变化而无需轮询

38

如何在当前活动窗口更改时调用回调函数。我已经了解到使用CBTProc可以实现这一点。然而,使用托管代码并不容易钩入全局事件。我有兴趣找到一种不需要轮询的方法。我希望采用事件驱动的方法。

谢谢


2
根据http://support.microsoft.com/kb/318804,你无法在.NET中执行全局钩子,而且由于全局钩子是监视这些类型事件的方法,因此看起来您唯一的其他选择是轮询或编写一些非托管代码(例如C ++),以提供您可以从C#访问的接口。 - Jim Mischel
Jim,我认为你的答案是正确的。 - Joe
3
吉姆其实只是部分正确。他可能只是粗略地浏览了那篇文章。键盘和鼠标钩子是全局的(如果你读到最后就会发现),你可以用 C# 来做这些,但是你不能用 C# 来做你正在尝试做的事情。 - Ben Lesh
3个回答

71

创建一个新的Windows窗体项目,添加一个文本框,使其多行显示,并将文本框的Dock属性设置为填充,命名为Log,然后粘贴以下代码(您需要将System.Runtime.InteropServices添加到您的usings中)...

    WinEventDelegate dele = null;

    public Form1()
    {
        InitializeComponent();
        dele = new WinEventDelegate(WinEventProc);
        IntPtr m_hhook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
    }

    delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

    [DllImport("user32.dll")]
    static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);

    private const uint WINEVENT_OUTOFCONTEXT = 0;
    private const uint EVENT_SYSTEM_FOREGROUND = 3;

    [DllImport("user32.dll")]
    static extern IntPtr GetForegroundWindow();

    [DllImport("user32.dll")]
    static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);

    private string GetActiveWindowTitle()
    {
        const int nChars = 256;
        IntPtr handle = IntPtr.Zero;
        StringBuilder Buff = new StringBuilder(nChars);
        handle = GetForegroundWindow();

        if (GetWindowText(handle, Buff, nChars) > 0)
        {
            return Buff.ToString();
        }
        return null;
    }

    public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
    {
        Log.Text += GetActiveWindowTitle() + "\r\n";
    } 

3
根据Savvas Sopiadis的回答,需要扩大dele的范围以防止委托被处理。更多信息请参见文档页面上的评论。 - Kenneth K.
这似乎不能在控制台应用程序中工作,有人知道为什么吗?我删除了InitializeComponent()。 - Anthony Raimondo
1
@AnthonyRaimondo 显然,如果您有一个 Console.ReadLine 处于活动状态,则它不会触发。请参见 https://dev59.com/fGox5IYBdhLWcg3w_ZR0#11943387 - public wireless
2
最后还应该调用UnhookWinEvent - NtFreX
很遗憾,当重新排列窗口(从最小化状态)时,这个功能无法正常工作。 - IneedHelp
显示剩余2条评论

21

我知道这个帖子已经有点旧了,但考虑到将来的使用,请注意:当运行代码一段时间后,你会注意到程序崩溃了。这是由于Form构造函数中的那一行代码引起的:

public Form1()
    {
        InitializeComponent();
        WinEventDelegate dele = new WinEventDelegate(WinEventProc);//<-causing ERROR
        IntPtr m_hhook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
    }

不要像上面那样,做出以下修改:

public Form1()
        {
            InitializeComponent();
            dele = new WinEventDelegate(WinEventProc); 
            IntPtr m_hhook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
        }
WinEventDelegate dele = null;

现在按预期工作!


它崩溃是因为当WinEventDelegate deleForm1构造函数内作用域时,它是一个候选的垃圾收集对象,因为引用在构造函数结束后就丢失了。将其作用域提升至类级别将会保持该引用活动直到类实例被释放。 - Walter Stabosz

7
你可以使用SetWinEventHook并监听EVENT_SYSTEM_FOREGROUND事件。使用WINEVENT_OUTOFCONTEXT标志以避免全局挂钩问题。

请您举几个例子,谢谢。 - Ehsan Zargar Ershadi
4
你可以搜索 Stack Overflow (https://dev59.com/PG855IYBdhLWcg3wViot)。 - Raymond Chen
2
附加信息:您的上下文函数必须快速。 - public wireless

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接