As Scott is leaving on a well-deserved sabbatical, he has gone ahead and posted our next Plugin of the Month a few days ahead of schedule. Here’s a link to Scott’s post announcing the tool.
This is a very cool little application developed by Mark Dubbelaar from Australia. Mark has been drafting/designing with AutoCAD for the last 10+ years and, during this time, has used a variety of programming languages to customize AutoCAD: LISP, VBA and now VB.NET. Mark was inspired by the “clipboard ring” functionality that used to be in Microsoft Office (at least I say “used to be” because I haven’t found it in Office 2007), and decided to implement similar functionality in AutoCAD.
The implementation of the tool is quite straightforward but the functionality is really very compelling: after having NETLOADed the tool and run the CLIPBOARD command, as you use Ctrl-C to copy drawing objects from inside AutoCAD to the clipboard a custom palette gets populated with entries containing these sets of objects. Each entry contains a time-stamp and an automatically-generated name which you can then change to something more meaningful.
When you want to use these clipboard entries, you simply right-click on one and choose the appropriate paste option (which ultimately just calls through to the standard AutoCAD paste commands, PASTECLIP, PASTEBLOCK and PASTEORIG, reducing the complexity of the tool).
That’s really all there is to it: a simple yet really useful application. Thanks for providing such a great little tool, Mark! :-)
Under the hood, the code is quite straightforward. The main file, Clipboard.vb, sets up the application to create demand-loading entries when first loaded into AutoCAD and defines a couple of commands – CLIPBOARD and REMOVECB, which removes the demand-loading entries to “uninstall” the application. It also contains the PaletteSet that contains our CbPalette and gets displayed by the CLIPBOARD command.
Imports Autodesk.AutoCAD.Runtime
Imports Autodesk.AutoCAD.Windows
Imports Autodesk.AutoCAD.EditorInput
Public Class ClipBoard
Implements IExtensionApplication
<DebuggerBrowsable(DebuggerBrowsableState.Never)> _
Private _cp As CbPalette = Nothing
Public ReadOnly Property ClipboardPalette() As CbPalette
Get
If _cp Is Nothing Then
_cp = New CbPalette
End If
Return _cp
End Get
End Property
Private _ps As PaletteSet = Nothing
Public ReadOnly Property PaletteSet() As PaletteSet
Get
If _ps Is Nothing Then
_ps = New PaletteSet("Clipboard", _
New System.Guid("ED8CDB2B-3281-4177-99BE-E1A46C3841AD"))
_ps.Text = "Clipboard"
_ps.DockEnabled = DockSides.Left + _
DockSides.Right + DockSides.None
_ps.MinimumSize = New System.Drawing.Size(200, 300)
_ps.Size = New System.Drawing.Size(300, 500)
_ps.Add("Clipboard", ClipboardPalette)
End If
Return _ps
End Get
End Property
Private Sub Initialize() _
Implements IExtensionApplication.Initialize
DemandLoading.RegistryUpdate.RegisterForDemandLoading()
End Sub
Private Sub Terminate() _
Implements IExtensionApplication.Terminate
End Sub
<CommandMethod("ADNPLUGINS", "CLIPBOARD", CommandFlags.Modal)> _
Public Sub ShowClipboard()
PaletteSet.Visible = True
End Sub
<CommandMethod("ADNPLUGINS", "REMOVECB", CommandFlags.Modal)> _
Public Sub RemoveClipboard()
DemandLoading.RegistryUpdate.UnregisterForDemandLoading()
Dim ed As Editor = _
Autodesk.AutoCAD.ApplicationServices.Application _
.DocumentManager.MdiActiveDocument.Editor()
ed.WriteMessage(vbCr + _
"The Clipboard Manager will not be loaded" _
+ " automatically in future editing sessions.")
End Sub
End Class
It’s the Clipboard_Palette.vb file that contains the more interesting code, implementing the behaviour of the CbPalette object. The real “magic” is how it hooks into AutoCAD’s COPYCLIP by attaching itself as the default “clipboard viewer”.
Imports AcApp = Autodesk.AutoCAD.ApplicationServices.Application
Imports System.Windows.Forms
Public Class CbPalette
' Constants for Windows API calls
Private Const WM_DRAWCLIPBOARD As Integer = &H308
Private Const WM_CHANGECBCHAIN As Integer = &H30D
' Handle for next clipboard viewer
Private _nxtCbVwrHWnd As IntPtr
' Boolean to control access to clipboard data
Private _internalHold As Boolean = False
' Counter for our visible clipboard name
Private _clipboardCounter As Integer = 0
' Windows API declarations
Declare Auto Function SetClipboardViewer Lib "user32" _
(ByVal HWnd As IntPtr) As IntPtr
Declare Auto Function SendMessage Lib "User32" _
(ByVal HWnd As IntPtr, ByVal Msg As Integer, _
ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Long
' Class constructor
Public Sub New()
' This call is required by the Windows Form Designer
InitializeComponent()
' Register ourselves to handle clipboard modifications
_nxtCbVwrHWnd = SetClipboardViewer(Handle)
End Sub
Private Sub AddDataToGrid()
Dim currentClipboardData As DataObject = _
My.Computer.Clipboard.GetDataObject
' If the clipboard contents are AutoCAD-related
If IsAutoCAD(currentClipboardData.GetFormats) Then
' Create a new row for our grid and add our clipboard
' data stored in the "tag"
Dim newRow As New DataGridViewRow()
newRow.Tag = currentClipboardData
' Increment our counter
_clipboardCounter += 1
' Create and add a cell for the name, using our counter
Dim newNameCell As New DataGridViewTextBoxCell
newNameCell.Value = "Clipboard " & _clipboardCounter
newRow.Cells.Add(newNameCell)
' Get the current time and place that in another cell
Dim newTimeCell As New DataGridViewTextBoxCell
newTimeCell.Value = Now.ToLongTimeString
newRow.Cells.Add(newTimeCell)
' Add our row to the data grid and select it
clipboardDataGridView.Rows.Add(newRow)
clipboardDataGridView.FirstDisplayedScrollingRowIndex = _
clipboardDataGridView.Rows.Count - 1
newRow.Selected = True
End If
End Sub
' Move the selected item's data into the clipboard
' Check whether the clipboard data was created by AutoCAD
Private Function IsAutoCAD(ByVal Formats As String()) As Boolean
For Each item As String In Formats
If item.Contains("AutoCAD") Then Return True
Next
Return False
End Function
Private Sub PasteToClipboard()
' Use a variable to make sure we don't edit the
' clipboard contents at the wrong time
_internalHold = True
My.Computer.Clipboard.SetDataObject( _
clipboardDataGridView.SelectedRows.Item(0).Tag)
_internalHold = False
End Sub
' Send a command to AutoCAD
Private Sub SendAutoCADCommand(ByVal cmd As String)
AcApp.DocumentManager.MdiActiveDocument.SendStringToExecute( _
cmd, True, False, True)
End Sub
' Our context-menu command handlers
Private Sub PasteToolStripButton_Click( _
ByVal sender As Object, ByVal e As EventArgs) _
Handles PasteToolStripMenuItem.Click
' Swap the data from the selected item in the grid into the
' clipboard and use the internal AutoCAD command to paste it
If clipboardDataGridView.SelectedRows.Count = 1 Then
PasteToClipboard()
SendAutoCADCommand("_pasteclip ")
End If
End Sub
Private Sub PasteAsBlockToolStripMenuItem_Click( _
ByVal sender As Object, ByVal e As EventArgs) _
Handles PasteAsBlockToolStripMenuItem.Click
' Swap the data from the selected item in the grid into the
' clipboard and use the internal AutoCAD command to paste it
' as a block
If clipboardDataGridView.SelectedRows.Count = 1 Then
PasteToClipboard()
SendAutoCADCommand("_pasteblock ")
End If
End Sub
Private Sub PasteToOriginalCoordinatesToolStripMenuItem_Click( _
ByVal sender As Object, ByVal e As EventArgs) _
Handles PasteToOriginalCoordinatesToolStripMenuItem.Click
' Swap the data from the selected item in the grid into the
' clipboard and use the internal AutoCAD command to paste it
' at the original location
If clipboardDataGridView.SelectedRows.Count = 1 Then
PasteToClipboard()
SendAutoCADCommand("_pasteorig ")
End If
End Sub
Private Sub RemoveAllToolStripButton_Click( _
ByVal sender As Object, ByVal e As EventArgs) _
Handles RemoveAllToolStripButton.Click
' Remove all the items in the grid
clipboardDataGridView.Rows.Clear()
End Sub
Private Sub RenameToolStripMenuItem_Click( _
ByVal sender As Object, ByVal e As EventArgs) _
Handles RenameToolStripMenuItem.Click
' Rename the selected row by editing the name cell
If clipboardDataGridView.SelectedRows.Count = 1 Then
clipboardDataGridView.BeginEdit(True)
End If
End Sub
Private Sub RemoveToolStripMenuItem_Click( _
ByVal sender As Object, ByVal e As EventArgs) _
Handles RemoveToolStripMenuItem.Click
' Remove the selected grid item
If clipboardDataGridView.SelectedRows.Count = 1 Then
clipboardDataGridView.Rows.Remove( _
clipboardDataGridView.SelectedRows.Item(0))
End If
End Sub
' Our grid view event handlers
Private Sub ClipboardDataGridView_CellMouseDown( _
ByVal sender As Object, _
ByVal e As DataGridViewCellMouseEventArgs) _
Handles clipboardDataGridView.CellMouseDown
' Responding to this event allows us to make sure the
' correct row is properly selected on right-click
If e.Button = Windows.Forms.MouseButtons.Right Then
clipboardDataGridView.CurrentCell = _
clipboardDataGridView.Item(e.ColumnIndex, e.RowIndex)
End If
End Sub
Private Sub ClipboardDataGridView_MouseDown( _
ByVal sender As System.Object, ByVal e As MouseEventArgs) _
Handles clipboardDataGridView.MouseDown
' On right-click display the row as selected and show
' the context menu at the location of the cursor
If e.Button = Windows.Forms.MouseButtons.Right Then
Dim hti As DataGridView.HitTestInfo = _
clipboardDataGridView.HitTest(e.X, e.Y)
If hti.Type = DataGridViewHitTestType.Cell Then
clipboardDataGridView.ClearSelection()
clipboardDataGridView.Rows(hti.RowIndex).Selected = True
ContextMenuStrip.Show(clipboardDataGridView, e.Location)
End If
End If
End Sub
' Override WndProc to get messages
Protected Overrides Sub WndProc(ByRef m As Message)
Select Case m.Msg
' The clipboard has changed
Case Is = WM_DRAWCLIPBOARD
If Not _internalHold Then AddDataToGrid()
SendMessage(_nxtCbVwrHWnd, m.Msg, m.WParam, m.LParam)
' Another clipboard viewer has removed itself
Case Is = WM_CHANGECBCHAIN
If m.WParam = CType(_nxtCbVwrHWnd, IntPtr) Then
_nxtCbVwrHWnd = m.LParam
Else
SendMessage(_nxtCbVwrHWnd, m.Msg, m.WParam, m.LParam)
End If
End Select
MyBase.WndProc(m)
End Sub
End Class
Public Class PaletteToolStrip
Inherits ToolStrip
Public Sub New()
MyBase.New()
End Sub
Public Sub New(ByVal ParamArray Items() As ToolStripItem)
MyBase.New(Items)
End Sub
Protected Overrides Sub WndProc(ByRef m As Message)
If m.Msg = &H21 AndAlso CanFocus AndAlso Not Focused Then
Focus()
End If
MyBase.WndProc(m)
End Sub
End Class
I also added a VB.NET version of the C# code that automatically registers an AutoCAD .NET application for demand-loading based on the commands it defines:
Imports System.Collections.Generic
Imports System.Reflection
Imports System.Resources
Imports System
Imports Microsoft.Win32
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.Runtime
Namespace DemandLoading
Public Class RegistryUpdate
Public Shared Sub RegisterForDemandLoading()
' Get the assembly, its name and location
Dim assem As Assembly = Assembly.GetExecutingAssembly()
Dim name As String = assem.GetName().Name
Dim path As String = assem.Location
' We'll collect information on the commands
' (we could have used a map or a more complex
' container for the global and localized names
' - the assumption is we will have an equal
' number of each with possibly fewer groups)
Dim globCmds As New List(Of String)()
Dim locCmds As New List(Of String)()
Dim groups As New List(Of String)()
' Iterate through the modules in the assembly
Dim mods As [Module]() = assem.GetModules(True)
For Each [mod] As [Module] In mods
' Within each module, iterate through the types
Dim types As Type() = [mod].GetTypes()
For Each type As Type In types
' We may need to get a type's resources
Dim rm As New ResourceManager(type.FullName, assem)
rm.IgnoreCase = True
' Get each method on a type
Dim meths As MethodInfo() = type.GetMethods()
For Each meth As MethodInfo In meths
' Get the methods custom command attribute(s)
Dim attbs As Object() = _
meth.GetCustomAttributes( _
GetType(CommandMethodAttribute), True)
For Each attb As Object In attbs
Dim cma As CommandMethodAttribute = _
TryCast(attb, CommandMethodAttribute)
If cma IsNot Nothing Then
' And we can finally harvest the information
' about each command
Dim globName As String = cma.GlobalName
Dim locName As String = globName
Dim lid As String = cma.LocalizedNameId
' If we have a localized command ID,
' let's look it up in our resources
If lid IsNot Nothing Then
' Let's put a try-catch block around this
' Failure just means we use the global
' name twice (the default)
Try
locName = rm.GetString(lid)
Catch
End Try
End If
' Add the information to our data structures
globCmds.Add(globName)
locCmds.Add(locName)
If cma.GroupName IsNot Nothing AndAlso _
Not groups.Contains(cma.GroupName) Then
groups.Add(cma.GroupName)
End If
End If
Next
Next
Next
Next
' Let's register the application to load on demand (12)
' if it contains commands, otherwise we will have it
' load on AutoCAD startup (2)
Dim flags As Integer = (If(globCmds.Count > 0, 12, 2))
' By default let's create the commands in HKCU
' (pass false if we want to create in HKLM)
CreateDemandLoadingEntries(name, path, globCmds, locCmds, _
groups, flags, True)
End Sub
Public Shared Sub UnregisterForDemandLoading()
RemoveDemandLoadingEntries(True)
End Sub
' Helper functions
Private Shared Sub CreateDemandLoadingEntries( _
ByVal name As String, ByVal path As String, _
ByVal globCmds As List(Of String), _
ByVal locCmds As List(Of String), _
ByVal groups As List(Of String), _
ByVal flags As Integer, _
ByVal currentUser As Boolean)
' Choose a Registry hive based on the function input
Dim hive As RegistryKey = _
If(currentUser,Registry.CurrentUser,Registry.LocalMachine)
' Open the main AutoCAD (or vertical) and "Applications" keys
Dim ack As RegistryKey = _
hive.OpenSubKey( _
HostApplicationServices.Current.RegistryProductRootKey)
Dim appk As RegistryKey = ack.OpenSubKey("Applications", True)
' Already registered? Just return
Dim subKeys As String() = appk.GetSubKeyNames()
For Each subKey As String In subKeys
If subKey.Equals(name) Then
appk.Close()
Exit Sub
End If
Next
' Create the our application's root key and its values
Dim rk As RegistryKey = appk.CreateSubKey(name)
rk.SetValue("DESCRIPTION", name, RegistryValueKind.[String])
rk.SetValue("LOADCTRLS", flags, RegistryValueKind.DWord)
rk.SetValue("LOADER", path, RegistryValueKind.[String])
rk.SetValue("MANAGED", 1, RegistryValueKind.DWord)
' Create a subkey if there are any commands...
If (globCmds.Count = locCmds.Count) _
AndAlso globCmds.Count > 0 Then
Dim ck As RegistryKey = rk.CreateSubKey("Commands")
For i As Integer = 0 To globCmds.Count - 1
ck.SetValue(globCmds(i), locCmds(i), _
RegistryValueKind.[String])
Next
End If
' And the command groups, if there are any
If groups.Count > 0 Then
Dim gk As RegistryKey = rk.CreateSubKey("Groups")
For Each grpName As String In groups
gk.SetValue(grpName, grpName, _
RegistryValueKind.[String])
Next
End If
appk.Close()
End Sub
Private Shared Sub RemoveDemandLoadingEntries( _
ByVal currentUser As Boolean)
Try
' Choose a Registry hive based on the function input
Dim hive As RegistryKey = _
If(currentUser,Registry.CurrentUser,Registry.LocalMachine)
' Open the main AutoCAD (or vertical) and "Applications" keys
Dim ack As RegistryKey = _
hive.OpenSubKey( _
HostApplicationServices.Current.RegistryProductRootKey)
Dim appk As RegistryKey = _
ack.OpenSubKey("Applications", True)
' Delete the key with the same name as this assembly
appk.DeleteSubKeyTree( _
Assembly.GetExecutingAssembly().GetName().Name)
appk.Close()
Catch
End Try
End Sub
End Class
End Namespace
That’s really all there is to it. If you have any feedback regarding the behaviour of the tool, please do send us an email.