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
- 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,
IntPtr hWndNewNext
);
The SetClipboardViewer
call is used to Register
as listener and is used as follows:
private IntPtr ClipboardViewerNext;
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:
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:
protected override void WndProc(ref Message m)
{
switch ((Msgs)m.Msg)
{
case Msgs.WM_DRAWCLIPBOARD:
IDataObject data = Clipboard.GetDataObject();
ProcessClip(data);
SendMessage(ClipboardViewerNext, m.Msg, m.WParam, m.LParam);
break;
case Msgs.WM_CHANGECBCHAIN:
if (m.WParam == ClipboardViewerNext)
{
ClipboardViewerNext = m.LParam;
}
else
{
SendMessage(ClipboardViewerNext, m.Msg, m.WParam, m.LParam);
}
break;
default:
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 format GetFormats
- Returns a list of formats that this object contains or may be converted to GetData
- Get
s the data object SetData
- 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 object Bitmap
- Bitmap data DeviceIndependentBitmap
- 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 .NET string
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 a Stream
)
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;
}
}
}
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.
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 (optionTextActive)
data.SetData(DataFormats.Text, richTextBox1.Text);
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());
}
if (optionRtfActive)
data.SetData(DataFormats.Rtf, richTextBox1.Rtf);
if (optionImageActive)
data.SetData(DataFormats.Bitmap, image);
if (optionAudioActive)
data.SetData(DataFormats.WaveAudio, audioData);
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.
Figure 1. ClipSpy+ Layout - Full Mode
- 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.
Figure 2. ClipSpy+ Mini Mode
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
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!