ClipSpy+






4.91/5 (33 votes)
A utility to uncover the mysteries of the Clipboard, now with a data injector that allows you to pass various data formats to the Clipboard.

Introduction
I had originally planned on calling this application ClipSpy but about midway through the project I thought I had better Google it and see if there was anything else associated with this name, lo and behold there was an excellent article right here on The Code Project written by Michael Dunn entitled ClipSpy. After reviewing what Michael had presented I decided to go ahead with my project since; his was written in 2001 and the Clipboard
API has changed since then. I was planning on presenting the data differently and going a little more in depth. This implementation is written in C#, and I wanted to learn about the Clipboard
and its inner workings.
So that how ClipSpy+ came to be and hopefully this article will introduce you to the Clipboard
in all its glory!
In the following sections, we will explore the inner workings of the Clipboard
and when we have a basic knowledge of that, I will explain how to use the ClipSpy+ application. So let's break the problem down into manageable chunks:
- Update
- Clipboard 101
- Clipboard Chaining and the Registering/Unregistering for activity notifications
- Basic API
- Clipboard 201
- DataFormats
- Advanced API
- Injecting Data
- Using ClipSpy+
- Using ClipInjector+
- References - A few links I picked up along the way
As always, I hope you get as much out of reading this article as I did writing it!
Update
There were some major problems that needed to be addressed to make the application more stable and get data that we weren't able to get in the first version.
I needed a way to test the new version to be sure that I could pass the different formats to the Clipboard
and have it accept any type of data, so I created ClipInjector+ to inject different formats. It worked out so well that I decided that it would be a useful addition to the article and a good teaching tool as well. The Custom data format option that I've added to the injector will show you what can be done with Custom formats and the way data is passed. I use a structure with the status of the format controls in the injector and the text in the RichTextBox
as a string
to the Clipboard
. I could have just as easily passed it as string[]
or MemoryStream
but it was easy to make a string
using StringBuilder
.
An addition to the main screen is the button in the upper right corner with the start image, this brings up the Injector. In the Data viewer, I have added a button to the upper left corner to play a wave file if encountered.
Clipboard 101
The Clipboard
class uses global memory to store and retrieve data during cut, copy and paste operations and when dragging and dropping files. It accomplishes this by storing data pertaining to the object in fragments with various formats to represent different aspects of the data being acted upon. We'll go more into the different data formats in the following sections.
Clipboard Chaining and the Registering/Unregistering for Activity Notifications
Windows provides a hook for anyone interested in intercepting data from the Clipboard
by allowing us to add ourself to a chain or linked list of listeners. The only thing we need to do here is to relay the data passed to us to the next listener in the chain. Don't break the chain or you'll be dancing on thin ice! i.e. unpredictable things can happen according to MSDN literature. I haven't been brave enough to try it, knowingly that is!
To register ourselves as a listener we will have to resort to some interop.
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern
IntPtr SetClipboardViewer(IntPtr hWnd);
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern
bool ChangeClipboardChain(
IntPtr hWndRemove,
//handle to window to remove
IntPtr hWndNewNext
//handle to next window
);
The SetClipboardViewer
call is used to Register
as listener and is used as follows:
private IntPtr ClipboardViewerNext;
/// <summary>
/// Add this control to the Clipboard chain to receive notification events|
/// </summary>
private void RegisterClipboardViewer()
{
ClipboardViewerNext = SetClipboardViewer(this.Handle);
}
The ClipboardViewNext
is a pointer to the next listener in the chain and we must reserve it for when we remove ourself from the chain as follows:
/// <summary>
/// Remove this form from the Clipboard Viewer list
/// </summary>
private void UnregisterClipboardViewer()
{
ChangeClipboardChain(this.Handle, ClipboardViewerNext);
}
Now to the heart of the matter!
So we've registered as a listener, now what? Well, we must override the WndProc
method:
/// <summary>
/// Process window messages
/// <remarks>
/// This code was not entirely written by me but has been modified from
/// compiled from examples
/// found while researching the subject!
/// </remarks>
/// </summary>
/// <param name="m"></param>
protected override void WndProc(ref Message m)
{
switch ((Msgs)m.Msg)
{
case Msgs.WM_DRAWCLIPBOARD: //0x308
//Retrieve the data object and Process it
IDataObject data = Clipboard.GetDataObject();
ProcessClip(data);
//
// Each window that receives the WM_DRAWCLIPBOARD message
// must call the SendMessage function to pass the message
// on to the next window in the clipboard viewer chain.
//
SendMessage(ClipboardViewerNext, m.Msg, m.WParam, m.LParam);
break;
//
// The WM_CHANGECBCHAIN message is sent to the first window
// in the clipboard viewer chain when a window is being
// removed from the chain.
//
case Msgs.WM_CHANGECBCHAIN: //0x30D
// When a clipboard viewer window receives the WM_CHANGECBCHAIN message,
// it should call the SendMessage function to pass the message to the
// next window in the chain, unless the next window is the window
// being removed. In this case, the clipboard viewer should save
// the handle specified by the lParam parameter
// as the next window in the chain.
//
// wParam is the Handle to the window being removed from
// the clipboard viewer chain
//Param is the Handle to the next window in the chain
//following the window being removed.
if (m.WParam == ClipboardViewerNext)
{
//
// If wParam is the next clipboard viewer then it
// is being removed so update pointer to the next
// window in the clipboard chain
//
ClipboardViewerNext = m.LParam;
}
else
{
SendMessage(ClipboardViewerNext, m.Msg, m.WParam, m.LParam);
}
break;
default:
//
// Just pass the message on.
//
base.WndProc(ref m);
break;
}
The code should explain itself.
Basic API
The Clipboard
class defines methods for checking, getting and setting specific types of data but I found them to be restrictive as far as what types of data it would handle. If you play by the rules and only work with data that it understands you're all right. Listed below are the methods available:
- For Audio -
ContainsAudio
/GetAudioStream
/SetAudio
- For generic data -
ContainsData
/GetData
/SetData
- For DropLists -
ContainsFileDropList
/GetFileDropList
/SetFileDropList
- For Images -
ContainsImage
/GetImage
/SetImage
- For text -
ContainsText
/GetText
/SetText
I won't go into a lot of detail about these as they are well documented in the help files and MSDN.
If you don't care about working with anything exotic, these will work just fine but I needed to view anything that was thrown at me and I wanted to present the data at a low level so I used the methods associated with the IDataObject
object. These will be discussed in detail in the next section.
Clipboard 201
The IDataObject
interface provides the following methods to work with the data contained within:
GetDataPresent
- Used to determine if a data object with the given format is available or can be converted to that formatGetFormats
- Returns a list of formats that this object contains or may be converted toGetData
-Get
s the data objectSetData
-Set
s the data object
The GetFormats
method returns a list of formats that are associated with the object. Most of these are uninteresting and there doesn't seem to be a specification available to determine what they are used for but the relevant ones are listed below:
- Text - Simple text
- Rich Text Format - Rich text
- HTML Format - HTML
FileNameW
- Full path and file name describing where to find the objectBitmap
- Bitmap dataDeviceIndependentBitmap
- Generic bitmap data
Note: As I continue my research, I will update this list if I find any more but for now these are the pertinent formats that I check for.
As an example, if we copy an RTF selection the following formats are involved:
System.String
- Contains the text in a .NETstring
format- Unicode Text - As Unicode Text
- Text - As just plain old text
- Rich Text Format - Here's the Rich Text
- Hidden Text Banner Format - Not used for anything we need, i.e. I don't know
- Locale - Not used for anything we need - I assume something to do with culture?
As you can see, the information is given in several formats to accommodate the application that will potentially consume the information.
When rendering the data to its raw form, I found that by going through the list of formats returned by the GetFormats
method, the data is stored in one of the following ways:
System.String
System.String[]
MemoryStream
System.Bitmap
(which is just aStream
)
The code given below is what I use to extract the raw data from the DataObject
.
Note: I've tried copying a lot of different types of data from several applications, etc. and these are the only ones I've found so far. If I stumble on any more formats, I will update.
public void ProcessRawData(IDataObject data, bool IsUnknowClip)
{
string[] strs = new string[20];
dataFormats = data.GetFormats(true);
try
{
int index = 0;
MemoryStream ms = null;
foreach (string s in dataFormats)
{
switch (data.GetData(s, true).GetType().ToString())
{
case "System.String[]":
strs = (string[])data.GetData(s, true);
rawDataBuffers[index++] = strs[0];
break;
case "System.IO.MemoryStream":
ms = (MemoryStream)data.GetData(s, true);
rawByteBuffers[index++] = ms.ToArray();
break;
case "System.String":
rawDataBuffers[index++] = (string)data.GetData(s, true);
break;
}
}
}
//The catchall, there was an error processing the data
catch
{
if (IsUnknowClip)|
title = "ERROR - Processing data";
}
}
The best way to learn about this is to use ClipSpy+ and check out what you have learned. This brings us to actually using the dang thing, so let's do that now!
Injecting Data
Injecting the data is the opposite of retrieving it. If we know how to retrieve data, and hopefully having read this far you do, we can visualize how we will pass the data to the Clipboard
. We see that there are four options available for us to use to structure our data: string
, string[]
, MemoryStream
or Bitmap
.
NOTE: Any data that you may pass has to be Serializable
. The code below is the class I use when passing Custom Format data.
[Serializable]
public class CustomFormatStruct : Object
{
public bool textActive = true;
public bool rtfActive = true;
public bool audioActive = true;
public bool imageActive = true;
public string data = string.Empty;
public string name = string.Empty;
public CustomFormatStruct(bool ta, bool ra, bool aa, bool ia, string d, string n)
{
textActive = ta;
rtfActive = ra;
audioActive = aa;
imageActive = ia;
data = d;
name = n;
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
sb.Append("This is an example of the kinds of data that can be passed
using the Clipboard\n\n");
sb.Append("<CustomFormatStruct>\n");
sb.Append("\t Text Data Sent: " + textActive.ToString() + "\n");
sb.Append("\t Rtf data sent: " + rtfActive.ToString() + "\n");
sb.Append("\tAudio data sent: " + audioActive.ToString() + "\n");
sb.Append("\tImage data sent: " + imageActive.ToString() + "\n");
sb.Append("</CustomFormatStruct>\n\n");
sb.Append("<" + name + ">\n");
sb.Append("\t" + data + "\n");
sb.Append("</" + name + ">");
return sb.ToString();
}
If you look at the code below, you will see how the different data is passed in various data formats depending on the type of data it is, i.e. If we are passing text we use the Text
format and so on. It seems a little daunting at first but it really is an easy concept to grasp once you start playing with it.
/// <summary>
/// Add data to the Clipboard
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
if (richTextBox1.Text == string.Empty)
richTextBox1.Text = "Tried to fool me didn't ya? Enter some text and try again!";
DataObject data = new DataObject();
//If we are going to send TExt set it here
if (optionTextActive)
data.SetData(DataFormats.Text, richTextBox1.Text);
//Struct for our custom data
CustomFormatStruct cfs = new CustomFormatStruct(
optionTextActive,
optionRtfActive,
optionAudioActive,
optionImageActive,
richTextBox1.Text,
textBox1.Text);
if (optionCustomActive)
{
data.SetData("ClipInjectorPlus", "This format was generated byt ClipInjector+");
data.SetData(textBox1.Text, cfs.ToString());
}
//Rtf data here
if (optionRtfActive)
data.SetData(DataFormats.Rtf, richTextBox1.Rtf);
//Image data here
if (optionImageActive)
data.SetData(DataFormats.Bitmap, image);
//Audio data here
if (optionAudioActive)
data.SetData(DataFormats.WaveAudio, audioData);
//Do the deed!
Clipboard.Clear();
Clipboard.SetDataObject(data);
}
Using ClipSpy+
As you may or may not know, I've been learning by myself GDI+ and graphics in general so the UI will illustrate some of what I've been learning along the way. I could have done more but then I wouldn't have any surprises for the next article.
So having patted myself on the back and taken a break to realign my arm, I'm now ready to show you how to use this marvellous utility.
Figure 1 and 2 show ClipSpy+ in the Full/Expanded mode and the Mini or Collapsed layouts respectively.

- A Toggles Sound On/Off
- B Enable/Disable ClipSpy+ from intercepting data
- C Filter bar, toggles category (Dims when not active)
- Note: If you click on the green filter button to the left of the bar, it turns all categories to active.
- D Current Clip object - Brief description and Icon indicating type
- E Display view toggle (Normal/Raw data)
- F History tree. Clips are inserted at the beginning of the appropriate category.
- G Form controls (left-to-right: Help, Minimize to tray, Minimize to Mini Mode, Exit)
- H View pane (Normal or Raw)
- In raw mode the
ComboBox
contains the various formats. Selecting a format shows corresponding data in viewer.
- In raw mode the

By default the application starts up with itself registered and the sound is on.
Using ClipInjector+
Using the injector is fairly straight forward. The available formats are in the upper pane and determine if the data will be set and what data will be set for the various data formats available. Once you have set the options that you are going to use, you click on the arrow button the lower left of the format area and this will set the clip to the Clipboard
! Once the data is sent, it is picked up by the ClipSpy+ application where the data may be viewed.

Well that's about it for this time, happy coding!
References
- CodeProject: ClipSpy - An excellent article by Michael Dunn
- MSDN: Clipboard - Contains links to other MSDN articles
- Clipboard Formats
- HTML Clipboard Format
- Other references: A simple explanation of the Clipboard
Revision History
- Version 1.0 released on 12/29.07 Saturday
Known problems: - After a while the sound drops out and no longer alerts user of activity. It doesn't seem to be unique to this application!
- Occasionally I run across an unrecognized data type. As I find them, I'll add them to this application and update, but there shouldn't be many more. In raw view if there's a message in the size field below the viewer pane that reads "Error: 0 bytes", it means I couldn't process the data with that format!