Monday, January 21, 2008

Clipboard and WPF as hooks and ImageSources

Challenge: create visual clipboard watcher, that displays and saves Bitmaps right after they arrives into the system clipboard by using WPF. Let's start.

First of all, we have to set hooks to detect clipboard changed events. .NET does not provide events, and does not listen to the clipboard changes, so, we have to deep into Win32 in order to archive the requirement. Let's see, once something arrives into the clipboard, we can see WM_DRAWCLIPBOARD message. It's not necessarily our data, so we have to change clipboard chain first, so, we should listen to WM_CHANGECBCHAIN and pass it into our application. But nothing will happen, if we will not use SetClipboardViewer(IntPtr) first. We already know, how to emulate WinProc in WPF. So, let's code it.

IntPtr viewerHandle = IntPtr.Zero;
IntPtr installedHandle = IntPtr.Zero;

const int WM_DRAWCLIPBOARD = 0x308;
const int WM_CHANGECBCHAIN = 0x30D;

[DllImport("user32.dll")]
private extern static IntPtr SetClipboardViewer(IntPtr hWnd);

[DllImport("user32.dll")]
private extern static int ChangeClipboardChain(IntPtr hWnd, IntPtr hWndNext);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private extern static int SendMessage(IntPtr hWnd,int wMsg,IntPtr wParam,IntPtr lParam);

Hook into WinProc

protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);
    HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource;
    if (hwndSource != null)
    {
        installedHandle = hwndSource.Handle;
        viewerHandle = SetClipboardViewer(installedHandle);
        hwndSource.AddHook(new HwndSourceHook(this.hwndSourceHook));
    }
}

And unhook after

protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
    ChangeClipboardChain(this.installedHandle, this.viewerHandle);
    int error = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
    e.Cancel = error != 0;

    base.OnClosing(e);
}
protected override void OnClosed(EventArgs e)
{
    this.viewerHandle = IntPtr.Zero;
    this.installedHandle = IntPtr.Zero;
    base.OnClosed(e);
}

Then choose and treat messages

IntPtr hwndSourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case WM_CHANGECBCHAIN:
            this.viewerHandle = lParam;
            if (this.viewerHandle != IntPtr.Zero)
            {
                SendMessage(this.viewerHandle, msg, wParam, lParam);
            }

            break;

        case WM_DRAWCLIPBOARD:
            EventArgs clipChange = new EventArgs();
            OnClipboardChanged(clipChange);

            if (this.viewerHandle != IntPtr.Zero)
            {
                SendMessage(this.viewerHandle, msg, wParam, lParam);
            }

            break;

    }
    return IntPtr.Zero;
}

Now, we should receive the Bitmap from Clipboard and convert it into BitmapSource. How to do it? Simple, really. Thank's to WPF Imaging team

ImageSource = (System.Windows.Interop.InteropBitmap)Clipboard.GetData(DataFormats.Bitmap);

We have a nasty Clipboard bug, so let's make it nicer a bit.

try
{
  if (Clipboard.ContainsData(DataFormats.Bitmap))
   ImageSource = (System.Windows.Interop.InteropBitmap)Clipboard.GetData(DataFormats.Bitmap);
}
catch{}

Now, all we have to do is to save the result by using regular encoder.

using (FileStream fs = new FileStream(f.FileName, FileMode.Create, FileAccess.Write))
                {
                    JpegBitmapEncoder enc = new JpegBitmapEncoder();
                    enc.Frames.Add(BitmapFrame.Create(ImageSource));
                    enc.Save(fs);
                    fs.Close();
                    fs.Dispose();
                }

That's all, folks. Now, upon the application start, we begin listening to clipboard and if we'll detect bitmap inside it, we'll enable ApplicationCommands.SaveAs command and save it into the user's hard drive.

<StackPanel>
        <StackPanel.CommandBindings>
            <CommandBinding Command="ApplicationCommands.SaveAs" Executed="CommandBinding_Executed" CanExecute="CommandBinding_CanExecute"/>
        </StackPanel.CommandBindings>
        <Button Command="ApplicationCommands.SaveAs" CommandTarget="{Binding Path=ImageSource}" Content="{Binding Path=Command.Text,RelativeSource={RelativeSource Self} }"/>
        <Image Name="image" Source="{Binding Path=ImageSource}"/>
    </StackPanel>

Have a nice day and be good people.

Source code for this article.

2 comments:

Tim said...

The Application does not show the preview image, is the InteropBitmap converted? Because with Debug.writeLine it prints as an InterOp type even after the line which should convert the image.

Bernat said...

I'm having a similar problem as Tim. I get the InteropBitmap object from the clipboard, but I'm unable to show it in an Image control. The Image control gets updated with the width and height of the image on the clipboard, but the image is empty.