问题描述
我正在用 C# 创建一个剪贴板管理器,但我不时遇到某些应用程序将剪贴板设置为空的情况.
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.
这发生在例如Excel 取消选择刚刚复制的内容时,所以我需要弄清楚剪贴板是否为空,但是如何获取更新剪贴板的应用程序名称?
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?
我希望我能以某种方式获得更新剪贴板的应用程序的 HWnd
句柄,以便我可以使用以下代码查找其背后的进程:
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;
}
}
我本来希望我可以使用 Message
对象中的 HWnd
,但这似乎是我自己的应用程序 - 可能用这个进程 ID 通知应用程序:
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 :-)
解决方案
根据@Jimi 的回答,这很简单.我可以将以下 3 行添加到我的原始代码中:
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();
推荐答案
您可以调用 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).
[...] 一般而言,剪贴板所有者是上次在剪贴板中放置数据的窗口.
EmptyClipboard 函数分配剪贴板所有权.
在某些特殊情况下,进程将空句柄传递给 OpenClipboard():阅读此函数的备注部分和 EmptyClipboard 功能.
There are special cases when a Process passes a null handle to OpenClipboard(): read the Remarks section of this function and the EmptyClipboard function.
在调用 EmptyClipboard 之前,应用程序必须打开剪贴板通过使用 OpenClipboard 函数.如果应用程序指定了一个打开剪贴板时的 NULL 窗口句柄,EmptyClipboard 成功但将剪贴板所有者设置为 NULL.请注意,这会导致将ClipboardData 设置为失败.
► 这里我使用的是 NativeWindow 派生类来设置剪贴板侦听器.创建处理剪贴板更新消息的窗口以初始化 CreateParams 对象并将此参数传递给 NativeWindow.CreateHandle(CreateParams) 方法,创建一个不可见窗口.
然后覆盖初始化的 NativeWindow 的 WndProc
,以接收 WM_CLIPBOARDUPDATE
通知.
► 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.
AddClipboardFormatListener 函数用于将 Window 放置在系统剪贴板侦听器链中.
The AddClipboardFormatListener function is used to place the Window in the system Clipboard listeners chain.
► ClipboardUpdateMonitor
类在收到剪贴板通知时生成事件.事件中传递的自定义 ClipboardChangedEventArgs
对象包含剪贴板所有者的句柄,由 GetClipboardOwner()
、ThreadId
和 返回ProcessId
由 GetWindowThreadProcessId() 返回 和进程名称,由 Process 标识.GetProcessById().
► 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().
您可以像这样设置 ClipboardUpdateMonitor
对象:
You can setup a ClipboardUpdateMonitor
object like this:
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
对象,用于携带收集的有关剪贴板所有者的信息:
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
类:
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;
}
这篇关于如何获取已更新剪贴板的应用程序的进程 ID 或名称?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!