Looking Forward to Window Clippings 2.0: Add-In Development

Update: Window Clippings 2.0 is now available! Download it now from http://www.windowclippings.com/.

 

Previously I introduced the concept of add-ins for Window Clippings from the user’s perspective. Today I want to talk about what it takes to develop an add-in.

Arguably the concept of an add-in has been around for a very long time. In modern computing, applications can be considered add-ins to the operating system. Many well-established applications like Internet Explorer, Firefox, Microsoft Office, etc. all support add-ins of some kind. As far as platforms go, COM provides a great foundation for developing add-ins and more recently the upcoming release of the .NET Framework is finally introducing plumbing to simplify extensibility for managed applications.

There are many considerations when designing extensibility into a product. Naturally the nature and design of a given product plays a part in shaping what can be done with add-ins and how they might be exposed or integrated. Window Clippings is a native Windows application that does not rely on the .NET Framework so naturally supporting add-ins developed with native code is important. Equally obvious is support for add-ins developed using managed code since many users will want to be able to whip up a quick add-in using their favorite .NET-supporting compiler.

Based on these scenarios and constraints I devised an extensibility model that allows add-ins to be developed in either native C++ using COM or using purely managed code using your favorite .NET compiler. Both native and managed add-ins are first-class add-ins and both receive the same treatment from Window Clippings as far as integration and feature support goes. Window Clippings also starts as a native application but will load the CLR on demand in the event that any managed add-ins are being used. You can of course continue to use Window Clippings on platforms that may not have the .NET Framework installed such as Windows XP or Windows Server Core.

I will start by discussing native add-ins but if you’re only interested in managed add-ins then feel free to skip ahead as managed add-ins are quite a lot simpler since the WindowClippings.dll managed assembly takes care of all of the plumbing.

Writing add-ins using native C++

Add-ins are packaged as COM servers. You can include as many add-ins as you wish in a server. Typically this involves creating a DLL project in Visual C++, exporting DllGetClassObject and friends, and implementing one COM class for each add-in. You can of course use whatever language, compiler and packaging model as long as you fulfill the responsibilities of a COM server.

The WindowClippings.h header file defines the IAddIn interface as well as the three interfaces that derive from it and represent the three types of add-ins supported by Window Clippings namely Filter, Save As and Send To add-ins. IAddIn is defined as follows:

struct DECLSPEC_UUID("...") DECLSPEC_NOVTABLE
IAddIn : IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE get_Location(__out BSTR* location) = 0;
    virtual HRESULT STDMETHODCALLTYPE get_Name(__out BSTR* name) = 0;
    virtual HRESULT STDMETHODCALLTYPE get_HasSettings(__out BOOL* hasSettings) = 0;
    virtual HRESULT STDMETHODCALLTYPE LoadSettings(IStream* source) = 0;
    virtual HRESULT STDMETHODCALLTYPE SaveSettings(IStream* destination) = 0;
    virtual HRESULT STDMETHODCALLTYPE EditSettings(HWND parent) = 0;
};

get_Location must be implemented and provides the fully-qualified path for the file that contains the add-in. This is used by Window Clippings to allow the user to easily unregister a given add-in. You could use the GetModuleFileName function to implement this method.

get_Name must be implemented and provides the display name for the add-in.

get_HasSettings must be implemented and indicates whether the add-in has configurable settings. If get_HasSettings return false through its hasSettings parameter then the next three methods will not be called and need not be implemented.

LoadSettings is called by Window Clippings prior to using the add-in and allows the add-in to load any configuration settings that were previously saved. This method should return E_NOTIMPL if get_HasSettings returns false.

SaveSettings is called by Window Clippings after the EditSettings method is called and the user chose to apply his or her changes. This method should return E_NOTIMPL if get_HasSettings returns false.

EditSettings is called by Window Clippings when the user chooses to edit the add-in’s settings. The method must display a modal dialog box with configuration settings. It should return E_NOTIMPL if get_HasSettings returns false.

The following class template may be used to simplify developing add-ins that are not configurable:

template <typename T>
class NoSettingsAddIn :
    public T
{
private:

    STDMETHODIMP get_HasSettings(__out BOOL* hasSettings)
    {
        HR_(E_POINTER, 0 != hasSettings);

        *hasSettings = false;
        return S_OK;
    }

    STDMETHODIMP LoadSettings(IStream* /*source*/)
    {
        return E_NOTIMPL;
    }

    STDMETHODIMP SaveSettings(IStream* /*destination*/)
    {
        return E_NOTIMPL;
    }

    STDMETHODIMP EditSettings(HWND /*parent*/)
    {
        return E_NOTIMPL;
    }

};

Add-ins must also be registered to implement the WindowClippingsCategory category (also defined in WindowClippings.h).

Filter add-ins implement the IFilter interface which is defined as follows:

struct DECLSPEC_UUID("...") DECLSPEC_NOVTABLE
IFilter : IAddIn
{
    virtual HRESULT STDMETHODCALLTYPE Process(Gdiplus::BitmapData* bitmapData) = 0;
};

So in addition to implementing IAddIn, you need to implement the Process method. The single BitmapData parameter is borrowed from GDI+ and provides the attributes of the image that Window Clippings has captured. The Process method may manipulate the bitmap directly before returning.

Save As add-ins implement the ISaveAs interface which is defined as follows:

struct DECLSPEC_UUID("...") DECLSPEC_NOVTABLE
ISaveAs : IAddIn
{
    virtual HRESULT STDMETHODCALLTYPE get_Extension(__out BSTR* extension) = 0;

    virtual HRESULT STDMETHODCALLTYPE Save(Gdiplus::BitmapData* bitmapData,
                                           COLORREF backColor,
                                           IStream* destination) = 0;
};

Save As add-ins are called by the built-in “Save to disk” add-in to save the image in the user’s chosen format.

get_Extension provides the file extension for the particular format that the add-in provides. This is called by Window Clippings when automatically generating a file name. It is also used to populate the filter combo box, along with the get_Name method, in the Save As dialog box if the user prefers to provide a file name directly.

The Save method is where you do the actual work of formatting the bitmap in your particular format and write the results to the stream. The BitmapData parameter provides a working copy of the image that Window Clippings has captured. A copy is provided so that you can freely manipulate it prior to formatting and saving the image. This can be useful depending on your image format’s capabilities. For example, the bitmap has an alpha channel but some formats don’t support that and you may need to “flatten” the image first. The COLORREF parameter indicates the user’s chosen background color. This should be used if you need to remove the alpha channel and replace it with the background color. Finally, the IStream is where you save the image.

Send To add-ins implement the ISendTo interface which is defined as follows:

struct DECLSPEC_UUID("...") DECLSPEC_NOVTABLE
ISendTo : IAddIn
{
    virtual HRESULT STDMETHODCALLTYPE Send(Gdiplus::BitmapData* bitmapData,
                                           BSTR title,
                                           COLORREF backColor,
                                           ISaveAs* saveAs) = 0;
};

The Send method is called by Window Clippings while executing the user’s action sequence. The BitmapData parameter provides a working copy of the image captured by Window Clippings. The image may have already been altered by any previous Filter add-ins. The BSTR parameter provides the title of the selected window, if any. This may be used if the destination of the Send To add-in can use or display it somehow. The COLORREF parameter indicates the user’s chosen background color. This should be used if the destination of the Send To add-in does not support an alpha channel. Finally, the ISaveAs parameter provides the Save As add-in that should be used in the event that the Send To add-in saves the image to a file. This might be useful if you’re writing an add-in to send to an FTP server for example.

The Window Clippings website will include developer information and complete working samples for each type of add-in when version 2.0 is released.

Writing add-ins using managed code

Welcome back Daniel.  :) 

Window Clippings 2.0 includes the WindowClippings.dll assembly that makes developing add-ins a breeze. Here’s what it looks like in .NET Reflector:

As you can see, it only has four public types. A bunch of internal types take care of all the hard work of making COM interop seamless, taking care of laying out the managed interfaces so that the vtables line up just right and the add-ins register themselves correctly. Not only that, but WindowClippings.dll is 100% MSIL without a hint of native code, making it platform agnostic. This means that any managed add-ins you develop will work with both 32-bit and 64-bit versions of Window Clippings (assuming your add-in assembly is not platform specific).

AddInRegistrar is for internal use only and should not be used. It is used by the (native) Window Clippings application to register managed add-ins.

Filter is an abstract class that you must derive from to implement a Filter add-in. Here’s an example of a filter add-in:

[Guid("...")]
public class FilterSample : Filter
{
    public FilterSample()
    {
        Name = "Filter sample";
    }

    protected override void Process(Bitmap bitmap)
    {
        bitmap.SetPixel(0, 0, Color.Red);
    }
}

Remember that every add-in class needs a unique GUID that you set using the Guid attribute. In this example the constructor sets the display name of the add-in and the abstract Process method is implemented with a simple modification of the bitmap. Now let’s make it configurable. First you need to set HasSettings to true in the constructor. You then need to override the protected virtual LoadSettings, SaveSettings and EditSettings methods. Here’s the updated example:

[Guid("...")]
public class FilterSample : Filter
{
    public FilterSample()
    {
        HasSettings = true;
        m_color = Color.Red; // default

        UpdateName();
    }

    protected override void Process(Bitmap bitmap)
    {
        bitmap.SetPixel(0, 0, m_color);
    }

    protected override void LoadSettings(BinaryReader reader)
    {
        m_color = Color.FromArgb(reader.ReadInt32());
        UpdateName();
    }

    protected override void SaveSettings(BinaryWriter writer)
    {
        writer.Write(m_color.ToArgb());
    }

    protected override void EditSettings(IWin32Window parentWindow)
    {
        using (ColorDialog dialog = new ColorDialog())
        {
            dialog.FullOpen = true;
            dialog.Color = m_color;

            if (DialogResult.OK == dialog.ShowDialog(parentWindow))
            {
                m_color = dialog.Color;
                UpdateName();
            }
        }
    }

    private void UpdateName()
    {
        Name = string.Format("Filter sample ({0})",
                             m_color);
    }

    private Color m_color;
}

And here’s what it looks like when you click the Settings button for this add-in:

If you look closely you’ll see a blue pixel in the top-left corner of the image thanks to this add-in.

Notice that I update the add-in’s Name property whenever the color is updated. Window Clippings will query the Name property after the user changes the add-in’s settings so this allows you to customize the name based on how the add-in is configured.

The Name and HasSettings properties as well as the LoadSettings, SaveSettings and EditSettings methods work identically for the other types of add-ins so I’m not going to discuss them again. Rest assured that Save As and Send To add-ins can provide configurable settings in just the same way.

SaveAs is an abstract class that you must derive from to implement a Save As add-in. Here’s an example of such an add-in:

[Guid("...")]
public class SaveAsGif : SaveAs
{
    public SaveAsGif()
    {
        Name = "GIF image";
        Extension = "gif";
    }

    public override void Save(Bitmap bitmap,
                              Color backColor,
                              Stream destination)
    {
        bitmap.Save(destination,
                    ImageFormat.Gif);
    }
}

You must set the Extension property to the file extension of the particular format your add-in provides. You must also override the Save method to take care of saving the bitmap to the destination stream. This example simply uses the Bitmap’s Save method to save the image using the GIF image format. Keep in mind that Bitmap will always be a 32-bit image including an alpha channel. Given an image format, such as GIF (GIF uses a color mask for transparency), which cannot handle the alpha channel, you need to remove any transparency from the image during or prior to formatting it. That’s where the Color parameter comes in. This is the background color chosen by the user and needs to be blended into the image to remove the alpha channel if your image format does not support it. Here’s a simple example:

public override void Save(Bitmap bitmap,
                          Color backColor,
                          Stream destination)
{
    FlattenBitmap(bitmap,
                  backColor);

    bitmap.Save(destination,
                ImageFormat.Gif);
}

private void FlattenBitmap(Bitmap bitmap,
                           Color backColor)
{
    for (int x = 0; x < bitmap.Width; ++x)
    {
        for (int y = 0; y < bitmap.Height; ++y)
        {
            Color color = bitmap.GetPixel(x, y);

            color = Color.FromArgb(color.R * color.A / 255 + backColor.R * (255 - color.A) / 255,
                                   color.G * color.A / 255 + backColor.G * (255 - color.A) / 255,
                                   color.B * color.A / 255 + backColor.B * (255 - color.A) / 255);

            bitmap.SetPixel(x, y, color);
        }
    }
}

SendTo is an abstract class that you must derive from to implement a Send To add-in. Here’s an example of such an add-in:

[Guid("...")]
public class SendToSample : SendTo
{
    public SendToSample()
    {
        Name = "Send to sample";
    }

    protected override void Send(Bitmap bitmap,
                                 string title,
                                 Color backColor,
                                 SaveAs saveAs)
    {
        // TODO: send image to destination
    }
}

You must override the Send method to take care of sending the image to its destination. This destination can be anything you can imagine such as a web service, database or application. The string parameter provides the original title of the window. The Color parameter provides the background color chosen by the user in the event that you need to flatten the image. The SaveAs parameter provides the add-in chosen by the user to handle image formatting in the event that your destination can handle any format.

That’s it for today. I hope this article provided you with a good idea of how to develop add-ins for Window Clippings.

Stay tuned for more highlights from the upcoming Window Clippings 2.0!

© 2007 Kenny Kerr

4 Comments

  • Do you need to register the native plugins..err add-in in HKCU/HKLM like a normal COM dll or are you rolling your own COM like handling?

    Will .net add-ins run into the same problem as explorer shell exstensions written in .net ( Only one version of .net runtime in a process )

  • ac: good questions.

    Native add-ins need to provide the regular DllRegisterServer and DllUnregisterServer exports but Window Clippings will call them directly when the user clicks the “Register Add-In” and “Unregister Add-In” buttons so there’s no need for you to call regsvr32 or the like.

    The same issues apply to managed add-ins as with shell extensions. I’m considering pre-loading a specific version of the CLR so that it is more deterministic. That way a user can configure WC to use .NET 3.5 and load both .NET 2.0 and 3.5 add-ins predictably.

  • How do you load managed assemblies? I understand that your appplication is a native app and not managed.

  • gyurisc: Window Clippings takes care of registering the assembly for COM interop. So when it comes to loading a managed add-in, Window Clippings doesn&rsquo;t know the difference and doesn&rsquo;t care.

Comments have been disabled for this content.