如何获取更新剪贴板的应用程序的进程ID或名称?

How to get the process ID or name of the application that has updated the clipboard?

我正在用 C# 创建一个剪贴板管理器,我时常遇到某些应用程序将剪贴板设置为空的情况。

这发生在例如Excel取消选择刚刚复制的内容时,所以我需要弄清楚剪贴板是否为空但如何获取更新剪贴板的应用程序名称?

我希望我能以某种方式获得更新剪贴板的应用程序的 HWnd 句柄,这样我就可以使用以下代码查找其背后的进程:

[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
...

protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case WM_CLIPBOARDUPDATE:
            // How to get the "handle" HWnd?
            IntPtr handle = ??? <============= HOW TO GET THIS ONE ???

            // Get the process ID from the HWnd
            uint processId = 0;
            GetWindowThreadProcessId(handle, out processId);

            // Get the process name from the process ID
            string processName = Process.GetProcessById((int)processId).ProcessName;

            Console.WriteLine("Clipboard UPDATE event from [" + processName + "]");
            break;
        }
        default:
            base.WndProc(ref m);
            break;
    }
}

我本来希望我可以使用 Message 对象中的 HWnd,但这似乎是我自己的应用程序 - 可能是为了通知具有此进程 ID 的应用程序:

如果我能以任何其他方式获得它,那么这当然也完全没问题,但我将不胜感激任何对此的见解:-)


解决方案

根据@Jimi 的回答,这很简单。我可以将以下 3 行添加到我的原始代码中:

// Import the "GetClipboardOwner" function from the User32 library
[DllImport("user32.dll")]
public static extern IntPtr GetClipboardOwner();
...

// Replace the original line with "HOW TO GET THIS ONE" with this line below - this will give the HWnd handle for the application that has changed the clipboard:
IntPtr handle = GetClipboardOwner();

您可以调用 GetClipboardOwner() 获取上次设置或清除剪贴板(触发通知的操作)的 Window 的句柄。

[...] In general, the clipboard owner is the window that last placed data in the Clipboard.
The EmptyClipboard function assigns Clipboard ownership.

进程将空句柄传递给 OpenClipboard(): read the Remarks section of this function and the EmptyClipboard 函数时存在特殊情况。

Before calling EmptyClipboard, an application must open the Clipboard by using the OpenClipboard function. If the application specifies a NULL window handle when opening the clipboard, EmptyClipboard succeeds but sets the clipboard owner to NULL. Note that this causes SetClipboardData to fail.


▶ 我在这里使用 NativeWindow derived class to setup a Clipboard listener. The Window that process the Clipboard update messages is created initializing a CreateParams object and passing this parameter to the NativeWindow.CreateHandle(CreateParams) 方法来创建一个 invisible Window.
然后覆盖已初始化的 NativeWindow 的 WndProc,以接收 WM_CLIPBOARDUPDATE 通知。

AddClipboardFormatListener 函数用于将 Window 放置在系统剪贴板侦听器链中。

ClipboardUpdateMonitor class 在收到剪贴板通知时生成一个事件。事件中传递的自定义 ClipboardChangedEventArgs 对象包含 GetClipboardOwner() 返回的剪贴板所有者的句柄,GetWindowThreadProcessId() and the Process name, identified by Process.GetProcessById() 返回的 ThreadIdProcessId

您可以像这样设置一个 ClipboardUpdateMonitor 对象:
这个class也可以在Program.cs

中初始化
private ClipboardUpdateMonitor clipboardMonitor = null;
// [...]

clipboardMonitor = new ClipboardUpdateMonitor();
clipboardMonitor.ClipboardChangedNotify += this.ClipboardChanged;
// [...]

private void ClipboardChanged(object sender, ClipboardChangedEventArgs e)
{
    Console.WriteLine(e.ProcessId);
    Console.WriteLine(e.ProcessName);
    Console.WriteLine(e.ThreadId);
}

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Windows.Forms;

public sealed class ClipboardUpdateMonitor : IDisposable
{
    private bool isDisposed = false;
    private static ClipboardWindow window = null;
    public event EventHandler<ClipboardChangedEventArgs> ClipboardChangedNotify;

    public ClipboardUpdateMonitor()
    {
        window = new ClipboardWindow();
        if (!NativeMethods.AddClipboardFormatListener(window.Handle)) {
            throw new TypeInitializationException(nameof(ClipboardWindow), 
                new Exception("ClipboardFormatListener could not be initialized"));
        }
        window.ClipboardChanged += ClipboardChangedEvent;
    }

    private void ClipboardChangedEvent(object sender, ClipboardChangedEventArgs e) 
        => ClipboardChangedNotify?.Invoke(this, e);

    public void Dispose()
    {
        if (!isDisposed) {
            // Cannot allow to throw exceptions here: add more checks to verify that 
            // the NativeWindow still exists and its handle is a valid handle
            NativeMethods.RemoveClipboardFormatListener(window.Handle);
            window?.DestroyHandle();
            isDisposed = true;
        }
    }

    ~ClipboardUpdateMonitor() => Dispose();

    private class ClipboardWindow : NativeWindow
    {
        public event EventHandler<ClipboardChangedEventArgs> ClipboardChanged;
        public ClipboardWindow() {
            new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand();
            var cp = new CreateParams();

            cp.Caption = "ClipboardWindow";
            cp.Height = 100;
            cp.Width = 100;

            cp.Parent = IntPtr.Zero;
            cp.Style = NativeMethods.WS_CLIPCHILDREN;
            cp.ExStyle = NativeMethods.WS_EX_CONTROLPARENT | NativeMethods.WS_EX_TOOLWINDOW;
            this.CreateHandle(cp);
        }
        protected override void WndProc(ref Message m)
        {
            switch (m.Msg) {
                case NativeMethods.WM_CLIPBOARDUPDATE:
                    IntPtr owner = NativeMethods.GetClipboardOwner();
                    var threadId = NativeMethods.GetWindowThreadProcessId(owner, out uint processId);
                    string processName = string.Empty;
                    if (processId != 0) {
                        using (var proc = Process.GetProcessById((int)processId)) { 
                            processName = proc?.ProcessName;
                        }
                    }
                    ClipboardChanged?.Invoke(null, new ClipboardChangedEventArgs(processId, processName, threadId));
                    m.Result = IntPtr.Zero;
                    break;
                default:
                    base.WndProc(ref m);
                    break;
            }
        }
    }
}

自定义 EventArgs 对象,用于携带收集的有关剪贴板所有者的信息:

public class ClipboardChangedEventArgs : EventArgs
{
    public ClipboardChangedEventArgs(uint processId, string processName, uint threadId)
    {
        this.ProcessId = processId;
        this.ProcessName = processName;
        this.ThreadId = threadId;
    }
    public uint ProcessId { get; }
    public string ProcessName { get; }
    public uint ThreadId { get; }
}

NativeMethods class:

internal static class NativeMethods
{
    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool AddClipboardFormatListener(IntPtr hwnd);

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool RemoveClipboardFormatListener(IntPtr hwnd);

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

    [DllImport("user32.dll", SetLastError = true)]
    internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

    internal const int WM_CLIPBOARDUPDATE = 0x031D;

    internal const int WS_CLIPCHILDREN = 0x02000000;
    internal const int WS_EX_TOOLWINDOW = 0x00000080;
    internal const int WS_EX_CONTROLPARENT = 0x00010000;
}