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