Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

ClipSpy+

4.91/5 (35 votes)
5 Jan 2008CPOL9 min read 1   7.7K  
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.
Main_Screen.png

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.

C#
[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:

C#
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:

C#
/// <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:

C#
/// <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 format
  • GetFormats - Returns a list of formats that this object contains or may be converted to
  • GetData - Gets the data object
  • SetData - Sets 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.

C#
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.

C#
[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.

C#
/// <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.

Main_Screen_II.gif
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.
MiniScreen.png
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.

Injector_Screen.png

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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)