I’ve been meaning to attack this one since we first published the Clipboard Manager as a Plugin of the Month: working out how to display a preview image of the clipboard contents inside the Clipboard Manager palette. And then I happened to receive a request by email, yesterday, suggesting a couple of enhancements to the tool. A nice reminder. :-)
1. Have the clipboard include image previews, similar to that of the wblock command (instead of needing to immediately rename the item when fast copying multiple items).
2. Have the clipboard store items in memory for use in between autocad sessions (this would be useful on projects that are not completed in one day/session, or really useful when/if autocad encounters a fatal error).
To start with I thought that both of these requests would have a common solution (at their core, at least): understanding how AutoCAD places its data on the clipboard would hopefully allow us to load it into a Database and then create a thumbnail from it. And we’d also be able to save off a copy of the data for future reuse.
After asking some colleagues for their opinions (and many thanks, Davis, Lee, Markus, Murali and Jack for the input! :-) it turns out the format AutoCAD uses to write to the clipboard is defined in an ObjectARX SDK header, clipdata.h. I think I once knew this, but once again I’m a victim of my own over-efficient garbage collection :-). Here’s the relevant structure, edited to fit the width of the blog:
typedef struct tagClipboardInfo {
ACHAR szTempFile[260]; // block temp file name
ACHAR szSourceFile[260]; // file name of drawing from which
// selection was made
ACHAR szSignature[4]; // szClipSignature
int nFlags; // kbDragGeometry: dragging
// geometry from AutoCAD?
AcGePoint3d dptInsert; // original world coordinate of
// insertion point
RECT rectGDI; // GDI coord bounding rectangle of
// sset
void* mpView; // Used to verify that this object
// was created in this view (HWND*)
DWORD m_dwThreadId; // AutoCAD thread that created this
// DataObject
int nLen; // Length of next segment of data,
// if any, starting with chData
int nType; // Type of data, if any
// (eExpandedClipDataTypes)
ACHAR chData[1]; // Start of data, if any.
} ClipboardInfo;
A number of the people I asked pointed out that AutoCAD actually WBLOCKs out the selected objects into a complete, standalone drawing file, which is referenced from this structure. The problem I suspect I’m going to have, at some point, is to map this structure to .NET, although as I know the temporary file is going to be in the first 260 characters, I could probably get away with just pulling out those characters and ignoring the rest. Although reusing them in a later session may then actually involve calling INSERT rather than placing them back into the clipboard and calling PASTECLIP, as in any case it feels as though there are some fields that might be tricky to recreate (thread ID has me nervous, for instance). But anyway – that’s for another day.
Another of the pieces of input I received had me thinking this was probably over-engineering the solution to the first request. We actually store a bunch of different formats to the clipboard – including a bitmap of the selected contents – which means we probably don’t need to care about the AutoCAD-specific format to address that request.
Rather than extending the existing VB.NET code to accomplish this, I decided to start with a simple C# app showing a palette that only displays the contents of the clipboard via an embedded PictureBox. I chose to go back to C# as I thought, at the time, I’d be mapping the above C++ structure – which would be easier – but anyway. Now that I have some working C# code I’ll be taking a look at extending the existing Clipboard Manager plugin, in due course.
Here’s the C# test code I put together. I placed everything in a single file – the UI is created by the code, rather than being in the Visual Studio designer, to keep it all simple. Oh, and I’m sorry for the lack of comments – if you’re interested in the implementation details but they’re not obvious, please leave a comment on this post.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Drawing;
using System;
namespace ClipboardViewing
{
public enum Msgs
{
WM_DRAWCLIPBOARD = 0x0308,
WM_CHANGECBCHAIN = 0x030D
}
public class ClipboardView : UserControl
{
[DllImport("user32.dll")]
public static extern IntPtr SetClipboardViewer(
IntPtr hWndNewViewer
);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(
IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam
);
IntPtr _nxtVwr;
PictureBox _img;
PaletteSet _ps;
public ClipboardView(PaletteSet ps)
{
_img = new PictureBox();
_img.Anchor =
(AnchorStyles)(AnchorStyles.Top |
AnchorStyles.Bottom |
AnchorStyles.Left |
AnchorStyles.Right);
_img.Location = new Point(0, 0);
_img.Size = this.Size;
_img.SizeMode = PictureBoxSizeMode.StretchImage;
Controls.Add(_img);
_nxtVwr = SetClipboardViewer(this.Handle);
_ps = ps;
}
private void ExtractImage()
{
IDataObject iData;
try
{
iData = Clipboard.GetDataObject();
}
catch (System.Exception ex)
{
MessageBox.Show(ex.ToString());
return;
}
if (iData.GetDataPresent("Bitmap"))
{
object o = iData.GetData("Bitmap");
Bitmap b = o as Bitmap;
if (b != null)
{
_img.Image = b;
if (_ps != null)
{
_ps.Size =
new Size(b.Size.Width / 3, b.Size.Height / 3);
}
}
}
}
protected override void WndProc(ref Message m)
{
switch ((Msgs)m.Msg)
{
case Msgs.WM_DRAWCLIPBOARD:
ExtractImage();
SendMessage(_nxtVwr, m.Msg, m.WParam, m.LParam);
break;
case Msgs.WM_CHANGECBCHAIN:
if (m.WParam == _nxtVwr)
_nxtVwr = m.LParam;
else
SendMessage(_nxtVwr, m.Msg, m.WParam, m.LParam);
break;
default:
base.WndProc(ref m);
break;
}
}
}
public class Commands
{
PaletteSet _ps = null;
ClipboardView _cv = null;
[CommandMethod("CBS")]
public void ShowClipboard()
{
if (_ps == null)
{
_ps = new PaletteSet(
"CBS",
new System.Guid("DB716FC9-2BD8-49ca-B3DF-6F2523C9B8E5")
);
if (_cv == null)
_cv = new ClipboardView(_ps);
_ps.Text = "Clipboard";
_ps.DockEnabled =
DockSides.Left | DockSides.Right | DockSides.None;
_ps.Size = new System.Drawing.Size(300, 500);
_ps.Add("ClipboardView", _cv);
}
_ps.Visible = true;
}
}
}
When we run the CBS command, it displays a palette which updates when the clipboard is modified to show its contents. The palette resizes to be a third of the size of the bitmap, so the aspect ratio is maintained. You may recognise the drawing used from the last post.
So that happily ended up being much easier than expected. Now I’ll be taking a look at how best to integrate this into the Clipboard Manager. And in due course how best to handle the persistence and reuse of the data between sessions.