I spent a few hours a couple of days ago creating a plugin for Windows Live Writer that allows for easy screen captures. I've long been a huge fan of SnagIt from Techsmith and since SnagIt's capture functionality is available as a COM interface it's quite easy to expose the functionality in other applications. For example, I have SnagIt plugged into my Help Builder tool to provide for screen captures in the Help HTML content.
Blogging is kind of similar: image capturing is pretty vital and making the process directly integrated into Live Writer itself makes screen captures a real cinch. Combined with Live Writer's ability to send images directly to the Web site via HTTP image capturing and publishing has never been easier.
You can grab the plug in and code from:
http://www.west-wind.com/tools/snagitlivewriterplugin.asp
Here's what the plugin looks like inside of Windows Live Writer:
When you click the Insert SnagIt button this dialog pops up which exposes most of the SnagIt capture options. Clicking Capture then goes off and uses SnagIt's native image capture functionailty (you know the red box to capture whatever capture mode selection you've made) to capture the image to file. You can optionally pop up the image editor and edit the image the same way as you can with native SnagIt use.
Settings are not saved, unless you click the Save Settings link, which stores the settings for later reuse. The idea is that you typically have a standard set of capture settings and these are restored each time you capture. You can also opt to not save the image locally. Live Writer actually makes a copy of each image you embed into a post so there's no need to hang on to captured images. The plugin writes out the file and then delay deletes the file if you have the Don't save image file option set to reduce file clutter on the local drive.
Creating a Live Writer Plugin
One of the nice things about Live Writer is the fact that you can very easily create plugins for it. You can download a Windows Live Writer SDK which provides fairly complete documentation and a few simple examples of how to plug into Live Writer. There are a host of different types of plugins you can create, although it looks like all plugins only address the content in the editor, not the overall operation of Writer. For example, as far as I can tell you can't override the HTTP communication or the actual messages that Writer sends when communicating with the server which would be very cool!
In any case creating an add-in that inserts content into the active blog post at the cursor position is very simple to create. The concept of this plug in is pretty simple:
- Create a class library project
- Add a reference to the WindowsLive.Writer.Api assembly to your project
- Implement the PlugIn API and handle the CreateContent method
- Create the SnagItAutomation object
- Bring up the Configuration Dialog that configures the object
- Run the screen capture
- Retrieve the filename that the capture writes to disk
- Insert the image as an <img> tag into the blog post
At the highest level the implementation of Writer Content plugin is quite simple:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using WindowsLive.Writer.Api;
namespace SnagitScreenCapturePlugin
{
[WriterPlugin( "7E113E74-A693-4bcd-8CF8-4C732654699C",
"SnagIt Screen Capture",
ImagePath = "Images.snagit.png",
PublisherUrl = "http://www.west-wind.com/tools/SnagitLiveWriterPlugin.aspx",
Description = "Embeds a screen capture image from SnagIt.")]
[InsertableContentSource( "SnagIt Screen Capture" )]
public class SnagitScreenCapturePlugin : ContentSource
{
public SnagitScreenCapturePlugin()
{
}
public override DialogResult CreateContent(IWin32Window dialogOwner, ref string newContent)
{
DialogResult dr = DialogResult.OK;
// *** Result Output file captured
string OutputFile = null;
try
{
SnagItAutomation SnagIt = SnagItAutomation.Create();
SnagIt.ActiveForm = Form.ActiveForm;
SnagItConfigurationForm ConfigForm = new SnagItConfigurationForm(SnagIt);
if (ConfigForm.ShowDialog() == DialogResult.Cancel)
return DialogResult.Cancel;
OutputFile = SnagIt.CaptureImageToFile();
SnagIt.SaveSettings();
}
catch (Exception ex)
{
MessageBox.Show("Failed to capture image:\r\n\r\n" + ex.Message,
"SnagIt Capture Exception", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
return DialogResult.Cancel;
}
// *** Just embed the image
if (!string.IsNullOrEmpty(OutputFile))
newContent = @"<img src='file:///" + OutputFile + "'>\r\n";
return dr;
}
}
}
This is really all there's to the plugin portion of this particular plugin. All the work that happens for displaying the dialog and running the screen capture has really nothing to do with the plugin itself and is freestanding. The CreateContent method only cares about a result value that is the content HTML that is to be embedded into the page along with a DialogResult return value that indicates whether the operation was successful.
Automating SnagIt with COM through .NET
At the crux of this plugin is the automation of SnagIt. SnagIt has a COM interface that can be automated quite easily. Although I might have chosen to simply import the COM type library as a COM Interop assembly, I decided I didn't want to do this as to avoid having to distribute another assembly with the plugin. So rather than use a COM Interop assembly the code I use utilizes Reflection to get at the COM properties and methods.
The key method of the SnagItAutomation class demonstrates how to use the SnagIt COM objects for capture automation:
/// <summary>
/// Captures an image to file and returns the filename
/// </summary>
/// <returns></returns>
public string CaptureImageToFile()
{
FormWindowState OldState = this.ActiveForm.WindowState;
if (this.ActiveForm != null)
this.ActiveForm.WindowState = FormWindowState.Minimized;
Application.DoEvents();
/// *** Capture first access to check if SnagIt is installed
try
{
wwUtils.SetPropertyExCom(this.SnagItCom, "OutputImageFile.Directory", this.CapturePath);
}
catch
{
throw new Exception("SnagIt isn't installed - COM Access failed.\r\nPlease install SnagIt from Techsmith Corporation (www.techsmith.com\\snagit).");
}
wwUtils.SetPropertyCom(this.SnagItCom,"EnablePreviewWindow",this.ShowPreviewWindow);
wwUtils.SetPropertyExCom(this.SnagItCom,"OutputImageFile.Filename", "captured_Image.png");
wwUtils.SetPropertyExCom(this.SnagItCom, "Input", this.CaptureMode);
wwUtils.SetPropertyExCom(this.SnagItCom, "OutputImageFile.FileType", (int)this.OutputFileCaptureFormat);
wwUtils.SetPropertyExCom(this.SnagItCom,"Filters.ColorConversion.ColorDepth",this.ColorDepth);
wwUtils.SetPropertyExCom(this.SnagItCom, "OutputImageFile.ColorDepth", this.ColorDepth);
wwUtils.SetPropertyExCom(this.SnagItCom, "IncludeCursor", this.IncludeCursor);
if (this.DelayInSeconds > 0)
{
wwUtils.SetPropertyExCom(this.SnagItCom, "DelayOptions.EnableDelayedCapture", true);
wwUtils.SetPropertyExCom(this.SnagItCom, "DelayOptions.DelaySeconds", this.DelayInSeconds);
}
// *** Need to delay a little here so that the form has properly minimized first
// *** especially under Vista
for (int i = 0; i < 20; i++)
{
Application.DoEvents();
Thread.Sleep(5);
}
wwUtils.CallMethodCom(this.SnagItCom, "Capture");
try
{
bool TimedOut = true;
while (true)
{
if ((bool)wwUtils.GetPropertyCom(this.SnagItCom, "IsCaptureDone"))
{
TimedOut = false;
break;
}
Thread.Sleep(100);
Application.DoEvents();
}
}
// *** No catch let it throw
finally
{
this._OutputCaptureFile = wwUtils.GetPropertyCom(this.SnagItCom, "LastFileWritten") as string;
if (this.ActiveForm != null)
this.ActiveForm.WindowState = OldState;
Marshal.ReleaseComObject(this.SnagItCom);
}
// *** If deleting the file we'll fire on a new thread and then delay by
// *** a few seconds until Writer has picked up the image.
if ((this.DeleteImageFromDisk))
{
Thread thread = new Thread( new ParameterizedThreadStart(DeleteImage));
thread.Start(this.OutputCaptureFile);
}
return this.OutputCaptureFile;
}
The code basically uses a COM instance - SnagItCom - that is created like this:
/// <summary>
/// Snagit COM Instance
/// </summary>
public object SnagItCom
{
get
{
if (_SnagItCom != null)
return _SnagItCom;
try
{
Type loT = Type.GetTypeFromProgID(SNAGIT_PROGID);
this._SnagItCom = Activator.CreateInstance(loT);
}
catch
{
return null;
}
return _SnagItCom;
}
}
private object _SnagItCom = null;
The instance is just a raw COM object wrapper and then Reflection is used to access this COM instance to set properties and eventually call the Capture() method. Notice that code first minimizes Writer so that it doesn't get in the way of any captures you make.
The Capture method requires a little explanation - the method is asynchronous, so it returns immediately. So to determine when the capture is complete you need to continually check the IsCaptureDone property until it is true. The capture is done either when you cancelled the capture or when you saved the capture image to file.
At this point all that needs to happen is to retrieve the LastFileWritten property which returns the full path to the captured image file which is returned as part of the method call.
The Plugin's CreateContent method then picks up this file name builds the HTML image tag and returns this HTML back through the reference newContent parameter, which in turn injects the HTML into the writer document. The Image file link is simply created like this:
<img src="file:///c:\temp\testimage.png">
Live Writer then internally copies the file to its own temporary store and instead embeds a template reference into the document. If you were to check the HTML after an image was embedded you get:
<img src="$testimage.png">
Because Writer makes an internal copy, you can actually delete the captured image file from disk, and there's an option to delete the capture file on the options dialog. When this option is chosen, the plugin creates a new thread and fires up this thread and calls the DeleteImageFile() method of the class. This method then simply sleeps for 10 seconds and then deletes the image file specified.
Storing Settings
The plugin has a Save Settings option which persists all the settings that were made to the object in the registry. It uses Serialization to basically persist the state of the SnagItAutomation object into a binary buffer which is then stored in the registry:
/// <summary>
/// Saves the current settings of this object to the registry
/// </summary>
/// <returns></returns>
public bool SaveSettings()
{
SnagItAutomation Snag = this;
byte[] Buffer = null;
if (!wwUtils.SerializeObject(Snag,out Buffer))
return false;
RegistryKey SubKey = Registry.CurrentUser.OpenSubKey(REGISTRY_STORAGE_SUBKEY,true);
if (SubKey == null)
SubKey = Registry.CurrentUser.CreateSubKey(REGISTRY_STORAGE_SUBKEY);
if (SubKey == null)
return false;
SubKey.SetValue("ConfigData", Buffer, RegistryValueKind.Binary);
SubKey.Close();
return true;
}
/// <summary>
/// Factory method that creates teh SnagItAutomation object by trying to read last capture settings
/// from the registry.
/// </summary>
/// <returns></returns>
public static SnagItAutomation Create()
{
byte[] Buffer = null;
RegistryKey SubKey = Registry.CurrentUser.OpenSubKey(REGISTRY_STORAGE_SUBKEY);
if (SubKey != null)
Buffer = SubKey.GetValue("ConfigData",null,RegistryValueOptions.None) as byte[];
if (Buffer == null)
return new SnagItAutomation();
// *** Force Assembly resolving code to fire so we can load the assembly
AppDomain.CurrentDomain.AssemblyResolve +=
new ResolveEventHandler(CurrentDomain_AssemblyResolve);
SnagItAutomation SnagIt = wwUtils.DeSerializeObject(Buffer,typeof(SnagItAutomation)) as SnagItAutomation;
// *** Unhook the event handler for the rest of the application
AppDomain.CurrentDomain.AssemblyResolve -= new ResolveEventHandler(CurrentDomain_AssemblyResolve);
if (SnagIt == null)
return new SnagItAutomation();
return SnagIt;
}
/// <summary>
/// Handle custom loading of the current assembly if the assmebly won't
/// resolve with the name.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
/// <returns></returns>
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
Assembly assembly = System.Reflection.Assembly.Load(args.Name);
if (assembly != null)
return assembly;
}
catch { ;}
return Assembly.GetExecutingAssembly();
}
As I mentioned a couple of days ago there's an interesting problem that crops up with deserialization in that the plugin assembly can't be found by the deserializer in .NET because the plugin assembly actually lives in a not-known folder of the application which results in an Assembly load error. The work around for this is to manually hook the AssemblyResolve event on the AppDomain to ensure that the correct assembly is returned. In this case I was lucky enough to directly pass GetExecutingAssembly() back - in other situations Assembly.LoadFromFile() might do the trick by parsing the assembly filename out of the full assembly name and looking in a particular path.
So there you have it. Creating an add-in is pretty easy with Live Writer and it's one reason that I've started to get into Live Writer and have now weaned myself from Word. There are a few plugins that help in this respect including the set of plugins on CodePlex, which provide the vital capability to paste HTML and past VS.NET Code directly into Writer.
I meant this to be a quick little project and indeed getting the base capture functionality set up took only a couple of hours. But the devil's always in the details and cleaning up the UI for the dialog, dealing with the Save Settings issues I ran into and creating the install package all took a bit longer than it should have. Before I knew it the whole evening was gone <s>...
Anyway, I've posted the plug-in and the code, so you can play with it on your own. Hopefully you find it as useful as I do...
Other Posts you might also like