如何在 C # 中监视剪贴板的更改?

是否有一个剪贴板更改或更新的事件,我可以通过 C # 访问?

77040 次浏览

I think you'll have to use some p/invoke:

[DllImport("User32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer);

See this article on how to set up a clipboard monitor in c#

Basically you register your app as a clipboard viewer using

_ClipboardViewerNext = SetClipboardViewer(this.Handle);

and then you will recieve the WM_DRAWCLIPBOARD message, which you can handle by overriding WndProc:

protected override void WndProc(ref Message m)
{
switch ((Win32.Msgs)m.Msg)
{
case Win32.Msgs.WM_DRAWCLIPBOARD:
// Handle clipboard changed
break;
// ...
}
}

(There's more to be done; passing things along the clipboard chain and unregistering your view, but you can get that from the article)

For completeness, here's the control I'm using in production code. Just drag from the designer and double click to create the event handler.

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Drawing;


namespace ClipboardAssist {


// Must inherit Control, not Component, in order to have Handle
[DefaultEvent("ClipboardChanged")]
public partial class ClipboardMonitor : Control
{
IntPtr nextClipboardViewer;


public ClipboardMonitor()
{
this.BackColor = Color.Red;
this.Visible = false;


nextClipboardViewer = (IntPtr)SetClipboardViewer((int)this.Handle);
}


/// <summary>
/// Clipboard contents changed.
/// </summary>
public event EventHandler<ClipboardChangedEventArgs> ClipboardChanged;


protected override void Dispose(bool disposing)
{
ChangeClipboardChain(this.Handle, nextClipboardViewer);
}


[DllImport("User32.dll")]
protected static extern int SetClipboardViewer(int hWndNewViewer);


[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);


[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);


protected override void WndProc(ref System.Windows.Forms.Message m)
{
// defined in winuser.h
const int WM_DRAWCLIPBOARD = 0x308;
const int WM_CHANGECBCHAIN = 0x030D;


switch (m.Msg)
{
case WM_DRAWCLIPBOARD:
OnClipboardChanged();
SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;


case WM_CHANGECBCHAIN:
if (m.WParam == nextClipboardViewer)
nextClipboardViewer = m.LParam;
else
SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;


default:
base.WndProc(ref m);
break;
}
}


void OnClipboardChanged()
{
try
{
IDataObject iData = Clipboard.GetDataObject();
if (ClipboardChanged != null)
{
ClipboardChanged(this, new ClipboardChangedEventArgs(iData));
}


}
catch (Exception e)
{
// Swallow or pop-up, not sure
// Trace.Write(e.ToString());
MessageBox.Show(e.ToString());
}
}
}


public class ClipboardChangedEventArgs : EventArgs
{
public readonly IDataObject DataObject;


public ClipboardChangedEventArgs(IDataObject dataObject)
{
DataObject = dataObject;
}
}
}

I believe one of the earlier solutions doesn't check for a null on the dispose method:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Drawing;


namespace ClipboardAssist {


// Must inherit Control, not Component, in order to have Handle
[DefaultEvent("ClipboardChanged")]
public partial class ClipboardMonitor : Control
{
IntPtr nextClipboardViewer;


public ClipboardMonitor()
{
this.BackColor = Color.Red;
this.Visible = false;


nextClipboardViewer = (IntPtr)SetClipboardViewer((int)this.Handle);
}


/// <summary>
/// Clipboard contents changed.
/// </summary>
public event EventHandler<ClipboardChangedEventArgs> ClipboardChanged;


protected override void Dispose(bool disposing)
{
if(nextClipboardViewer != null)
ChangeClipboardChain(this.Handle, nextClipboardViewer);
}


[DllImport("User32.dll")]
protected static extern int SetClipboardViewer(int hWndNewViewer);


[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);


[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);


protected override void WndProc(ref System.Windows.Forms.Message m)
{
// defined in winuser.h
const int WM_DRAWCLIPBOARD = 0x308;
const int WM_CHANGECBCHAIN = 0x030D;


switch (m.Msg)
{
case WM_DRAWCLIPBOARD:
OnClipboardChanged();
SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;


case WM_CHANGECBCHAIN:
if (m.WParam == nextClipboardViewer)
nextClipboardViewer = m.LParam;
else
SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;


default:
base.WndProc(ref m);
break;
}
}


void OnClipboardChanged()
{
try
{
IDataObject iData = Clipboard.GetDataObject();
if (ClipboardChanged != null)
{
ClipboardChanged(this, new ClipboardChangedEventArgs(iData));
}


}
catch (Exception e)
{
// Swallow or pop-up, not sure
// Trace.Write(e.ToString());
MessageBox.Show(e.ToString());
}
}
}


public class ClipboardChangedEventArgs : EventArgs
{
public readonly IDataObject DataObject;


public ClipboardChangedEventArgs(IDataObject dataObject)
{
DataObject = dataObject;
}
}
}

Ok so this is an old post but we found a solution that seems very simple compared to the current set of answers. We are using WPF and we wanted to have our own custom Commands (in a ContextMenu) enable and disable if the Clipboard contains text. There is already an ApplicationCommands.Cut, Copy and Paste and these commands respond correctly to the clipboard changing. So we just added the following EventHandler.

ApplicationCommands.Paste.CanExecuteChanged += new EventHandler(Paste_CanExecuteChanged);


private void Paste_CanExecuteChanged(object sender, EventArgs e) {
ourVariable= Clipboard.ContainsText();
}

We actually are controlling the CanExecute on our own Command this way. Works for what we needed and maybe it will help others out there.

There are multiple ways of doing this but this is my favorite and works for me. I've created a class library so that others may add the project and include the DLL then simply call on it and use it wherever they want within their applications.

This answer was made with the help of this one.

  1. Create Class Library project and name it ClipboardHelper.
  2. Replace the Class1 name with ClipboardMonitor.
  3. Add the below code into it.
  4. Add System.Windows.Forms reference.

More steps under code.

using System;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.InteropServices;


namespace ClipboardHelper
{
public static class ClipboardMonitor
{
public delegate void OnClipboardChangeEventHandler(ClipboardFormat format, object data);
public static event OnClipboardChangeEventHandler OnClipboardChange;


public static void Start()
{
ClipboardWatcher.Start();
ClipboardWatcher.OnClipboardChange += (ClipboardFormat format, object data) =>
{
if (OnClipboardChange != null)
OnClipboardChange(format, data);
};
}


public static void Stop()
{
OnClipboardChange = null;
ClipboardWatcher.Stop();
}


class ClipboardWatcher : Form
{
// static instance of this form
private static ClipboardWatcher mInstance;


// needed to dispose this form
static IntPtr nextClipboardViewer;


public delegate void OnClipboardChangeEventHandler(ClipboardFormat format, object data);
public static event OnClipboardChangeEventHandler OnClipboardChange;


// start listening
public static void Start()
{
// we can only have one instance if this class
if (mInstance != null)
return;


var t = new Thread(new ParameterizedThreadStart(x => Application.Run(new ClipboardWatcher())));
t.SetApartmentState(ApartmentState.STA); // give the [STAThread] attribute
t.Start();
}


// stop listening (dispose form)
public static void Stop()
{
mInstance.Invoke(new MethodInvoker(() =>
{
ChangeClipboardChain(mInstance.Handle, nextClipboardViewer);
}));
mInstance.Invoke(new MethodInvoker(mInstance.Close));


mInstance.Dispose();


mInstance = null;
}


// on load: (hide this window)
protected override void SetVisibleCore(bool value)
{
CreateHandle();


mInstance = this;


nextClipboardViewer = SetClipboardViewer(mInstance.Handle);


base.SetVisibleCore(false);
}


[DllImport("User32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer);


[DllImport("User32.dll", CharSet = CharSet.Auto)]
private static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);


[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);


// defined in winuser.h
const int WM_DRAWCLIPBOARD = 0x308;
const int WM_CHANGECBCHAIN = 0x030D;


protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_DRAWCLIPBOARD:
ClipChanged();
SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;


case WM_CHANGECBCHAIN:
if (m.WParam == nextClipboardViewer)
nextClipboardViewer = m.LParam;
else
SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;


default:
base.WndProc(ref m);
break;
}
}


static readonly string[] formats = Enum.GetNames(typeof(ClipboardFormat));


private void ClipChanged()
{
IDataObject iData = Clipboard.GetDataObject();


ClipboardFormat? format = null;


foreach (var f in formats)
{
if (iData.GetDataPresent(f))
{
format = (ClipboardFormat)Enum.Parse(typeof(ClipboardFormat), f);
break;
}
}


object data = iData.GetData(format.ToString());


if (data == null || format == null)
return;


if (OnClipboardChange != null)
OnClipboardChange((ClipboardFormat)format, data);
}
}
}


public enum ClipboardFormat : byte
{
/// <summary>Specifies the standard ANSI text format. This static field is read-only.
/// </summary>
/// <filterpriority>1</filterpriority>
Text,
/// <summary>Specifies the standard Windows Unicode text format. This static field
/// is read-only.</summary>
/// <filterpriority>1</filterpriority>
UnicodeText,
/// <summary>Specifies the Windows device-independent bitmap (DIB) format. This static
/// field is read-only.</summary>
/// <filterpriority>1</filterpriority>
Dib,
/// <summary>Specifies a Windows bitmap format. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
Bitmap,
/// <summary>Specifies the Windows enhanced metafile format. This static field is
/// read-only.</summary>
/// <filterpriority>1</filterpriority>
EnhancedMetafile,
/// <summary>Specifies the Windows metafile format, which Windows Forms does not
/// directly use. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
MetafilePict,
/// <summary>Specifies the Windows symbolic link format, which Windows Forms does
/// not directly use. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
SymbolicLink,
/// <summary>Specifies the Windows Data Interchange Format (DIF), which Windows Forms
/// does not directly use. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
Dif,
/// <summary>Specifies the Tagged Image File Format (TIFF), which Windows Forms does
/// not directly use. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
Tiff,
/// <summary>Specifies the standard Windows original equipment manufacturer (OEM)
/// text format. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
OemText,
/// <summary>Specifies the Windows palette format. This static field is read-only.
/// </summary>
/// <filterpriority>1</filterpriority>
Palette,
/// <summary>Specifies the Windows pen data format, which consists of pen strokes
/// for handwriting software, Windows Forms does not use this format. This static
/// field is read-only.</summary>
/// <filterpriority>1</filterpriority>
PenData,
/// <summary>Specifies the Resource Interchange File Format (RIFF) audio format,
/// which Windows Forms does not directly use. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
Riff,
/// <summary>Specifies the wave audio format, which Windows Forms does not directly
/// use. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
WaveAudio,
/// <summary>Specifies the Windows file drop format, which Windows Forms does not
/// directly use. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
FileDrop,
/// <summary>Specifies the Windows culture format, which Windows Forms does not directly
/// use. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
Locale,
/// <summary>Specifies text consisting of HTML data. This static field is read-only.
/// </summary>
/// <filterpriority>1</filterpriority>
Html,
/// <summary>Specifies text consisting of Rich Text Format (RTF) data. This static
/// field is read-only.</summary>
/// <filterpriority>1</filterpriority>
Rtf,
/// <summary>Specifies a comma-separated value (CSV) format, which is a common interchange
/// format used by spreadsheets. This format is not used directly by Windows Forms.
/// This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
CommaSeparatedValue,
/// <summary>Specifies the Windows Forms string class format, which Windows Forms
/// uses to store string objects. This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
StringFormat,
/// <summary>Specifies a format that encapsulates any type of Windows Forms object.
/// This static field is read-only.</summary>
/// <filterpriority>1</filterpriority>
Serializable,
}
}
  1. In your other projects right click on solution and Add -> Exiting Project -> ClipboardHelper.csproj
  2. On your project go to and right click References -> Add Reference -> Solution -> Select ClipboardHelper.
  3. In your class file of the project type using ClipboardHelper.
  4. You may now type ClipboardMonitor.Start or .Stop or .OnClipboardChanged

    using ClipboardHelper;
    
    
    namespace Something.Something.DarkSide
    {
    public class MainWindow
    {
    
    
    public MainWindow()
    {
    InitializeComponent();
    
    
    Loaded += MainWindow_Loaded;
    }
    
    
    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
    ClipboardMonitor.OnClipboardChange += ClipboardMonitor_OnClipboardChange;
    ClipboardMonitor.Start();
    }
    
    
    private void ClipboardMonitor_OnClipboardChange(ClipboardFormat format, object data)
    {
    // Do Something...
    }
    }
    

I had this challenge in WPF and ended up using the approach described below. For windows forms there are excellent examples elsewhere in this answer, such as the ClipboardHelper control.

For WPF we cannot override WndProc, so we have to hook it explicitly with an HwndSource AddHook call using the Source from a window. The clipboard listener still uses the AddClipboardFormatListener native interop call.

Native methods:

internal static class NativeMethods
{
// See http://msdn.microsoft.com/en-us/library/ms649021%28v=vs.85%29.aspx
public const int WM_CLIPBOARDUPDATE = 0x031D;
public static IntPtr HWND_MESSAGE = new IntPtr(-3);


// See http://msdn.microsoft.com/en-us/library/ms632599%28VS.85%29.aspx#message_only
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool AddClipboardFormatListener(IntPtr hwnd);
}

Clipboard Manager class:

using System.Windows;
using System.Windows.Interop;


public class ClipboardManager
{
public event EventHandler ClipboardChanged;


public ClipboardManager(Window windowSource)
{
HwndSource source = PresentationSource.FromVisual(windowSource) as HwndSource;
if(source == null)
{
throw new ArgumentException(
"Window source MUST be initialized first, such as in the Window's OnSourceInitialized handler."
, nameof(windowSource));
}


source.AddHook(WndProc);


// get window handle for interop
IntPtr windowHandle = new WindowInteropHelper(windowSource).Handle;


// register for clipboard events
NativeMethods.AddClipboardFormatListener(windowHandle);
}


private void OnClipboardChanged()
{
ClipboardChanged?.Invoke(this, EventArgs.Empty);
}


private static readonly IntPtr WndProcSuccess = IntPtr.Zero;


private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == NativeMethods.WM_CLIPBOARDUPDATE)
{
OnClipboardChanged();
handled = true;
}


return WndProcSuccess;
}
}

This gets used in a WPF window by adding the event in OnSourceInitialized or later such as the Window.Loaded event or during operation. (when we have enough information to use the native hooks):

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}


protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);


// Initialize the clipboard now that we have a window soruce to use
var windowClipboardManager = new ClipboardManager(this);
windowClipboardManager.ClipboardChanged += ClipboardChanged;
}


private void ClipboardChanged(object sender, EventArgs e)
{
// Handle your clipboard update here, debug logging example:
if (Clipboard.ContainsText())
{
Debug.WriteLine(Clipboard.GetText());
}
}
}

I'm using this approach in a Path of Exile item analyzer project, as the game exposes item information via the clipboard when you hit Ctrl-C.

https://github.com/ColinDabritz/PoeItemAnalyzer

I hope this helps someone with WPF clipboard change handling!

        [DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer);
private IntPtr _ClipboardViewerNext;


private void Form1_Load(object sender, EventArgs e)
{
_ClipboardViewerNext = SetClipboardViewer(this.Handle);
}


protected override void WndProc(ref System.Windows.Forms.Message m)
{
const int WM_DRAWCLIPBOARD = 0x308;


switch (m.Msg)
{
case WM_DRAWCLIPBOARD:
//Clipboard is Change
//your code..............
break;
default:
base.WndProc(ref m);
break;
}
}

SharpClipboard as a library could be of more benefit as it encapsulates the same features into one fine component library. You can then access its ClipboardChanged event and detect various data-formats when they're cut/copied.

You can choose the various data-formats you want to monitor:

var clipboard = new SharpClipboard();


clipboard.ObservableFormats.Texts = true;
clipboard.ObservableFormats.Files = true;
clipboard.ObservableFormats.Images = true;
clipboard.ObservableFormats.Others = true;

Here's an example using its ClipboardChanged event:

private void ClipboardChanged(Object sender, ClipboardChangedEventArgs e)
{
// Is the content copied of text type?
if (e.ContentType == SharpClipboard.ContentTypes.Text)
{
// Get the cut/copied text.
Debug.WriteLine(clipboard.ClipboardText);
}


// Is the content copied of image type?
else if (e.ContentType == SharpClipboard.ContentTypes.Image)
{
// Get the cut/copied image.
Image img = clipboard.ClipboardImage;
}


// Is the content copied of file type?
else if (e.ContentType == SharpClipboard.ContentTypes.Files)
{
// Get the cut/copied file/files.
Debug.WriteLine(clipboard.ClipboardFiles.ToArray());


// ...or use 'ClipboardFile' to get a single copied file.
Debug.WriteLine(clipboard.ClipboardFile);
}


// If the cut/copied content is complex, use 'Other'.
else if (e.ContentType == SharpClipboard.ContentTypes.Other)
{
// Do something with 'e.Content' here...
}
}

You can also find out the application that the cut/copy event occurred on together with its details:

private void ClipboardChanged(Object sender, SharpClipboard.ClipboardChangedEventArgs e)
{
// Gets the application's executable name.
Debug.WriteLine(e.SourceApplication.Name);
// Gets the application's window title.
Debug.WriteLine(e.SourceApplication.Title);
// Gets the application's process ID.
Debug.WriteLine(e.SourceApplication.ID.ToString());
// Gets the application's executable path.
Debug.WriteLine(e.SourceApplication.Path);
}

There are also other events such as the MonitorChanged event which listens whenever clipboard-monitoring is disabled, meaning that you can enable or disable monitoring the clipboard at runtime.

In addition to all this, since it's a component, you can use it in Designer View by dragging-and-dropping it to a Windows Form, making it super easy for anyone to customize its options and work with its inbuilt events.

SharpClipboard seems to be the very best option for clipboard-monitoring scenarios in .NET.

One more issue you can face with clipboard viewer: it stops receiving WM_DRAWCLIPBOARD messages after some period of time (seems like the clipboard chain is broken somehow). The only solution I've found is to reregister the clipboard viewer if broken chain was found.

For my needs I created nuget package https://github.com/magicmanam/windows-clipboard-viewer which wraps handling for all needed Windows messages and provides method to refresh your clipboard viewer. The package's description contains sample of usage

There's one more answer (in 2022 I know!):

If you are in a Console app, you need to use [STAThread] be able to monitor the Clipboard (and import System.Windows.Forms for the Clipboard class).

SharpClipboard specifically does not work, even in an [STAThread] Environment.

using System;
using System.Windows.Forms;
class Main {
[STAThread]
static void Main() {
if (Clipboard.ContainsText()) {
string text = Clipboard.GetText();
Console.Writeline(text);
}
}
}

EDIT:

It may also be possible to hook the DLL functions instead to access the clipboard in this mode without System.Windows.Forms being imported.