Warm tip: This article is reproduced from serverfault.com, please click

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

发布于 2020-12-04 21:40:41

I am creating a clipboard manager in C# and from time to time I experience that the clipboard is being set empty by some application.

This happens in e.g. Excel when deselecting what is just copied, so I need to figure out if the clipboard is empty but how can I get the application name that updates the clipboard?

I hope that I somehow can get the HWnd handle of the application that updates the clipboard so I can then lookup the process behind it with this code:

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

I would have hoped that I could use the HWnd from the Message object, but this seems to be my own application - probably to notify the application with this process ID:

This what I get from the Quick View in C# when the clipboard is changed

If I can get it any other way then this would of course be fully okay also but I would appreciate any insights on this :-)


Solution

Based on @Jimi's answer then this is very simple. I can just add the below 3 lines to my original code:

// 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();
Questioner
Beauvais
Viewed
0
Jimi 2021-02-04 19:18:56

You can call GetClipboardOwner() to get the handle of the Window that last set or cleared the Clipboard (the operation that triggered the notification).

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

There are special cases when a Process passes a null handle to OpenClipboard(): read the Remarks section of this function and the EmptyClipboard function.

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.


► Here I'm using a 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) method, to create an invisible Window.
Then override WndProc of the initialized NativeWindow, to receive WM_CLIPBOARDUPDATE notifications.

The AddClipboardFormatListener function is used to place the Window in the system Clipboard listeners chain.

► The ClipboardUpdateMonitor class generates an event when a Clipboard notification is received. The custom ClipboardChangedEventArgs object passed in the event contains the Handle of the Clipboard Owner, as returned by GetClipboardOwner(), the ThreadId and ProcessId returned by GetWindowThreadProcessId() and the Process name, identified by Process.GetProcessById().

You can setup a ClipboardUpdateMonitor object like this:
This class can also be initialized in 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;
            }
        }
    }
}

Custom EventArgs object used to carry the information collected about the Clipboard Owner:

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