March 2015

Sun Mon Tue Wed Thu Fri Sat
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        


« Taking a screenshot of a user-selected portion of a drawing using .NET | Main | Ask me questions on the AutoCAD Exchange! »

September 25, 2009

Clipboard Manager: October’s ADN Plugin of the Month, now live on Autodesk Labs

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).

The Clipboard Manager in action

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


      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


      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



  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()



    Dim ed As Editor = _

      Autodesk.AutoCAD.ApplicationServices.Application _


    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




    ' Register ourselves to handle clipboard modifications


    _nxtCbVwrHWnd = SetClipboardViewer(Handle)


  End Sub


  Private Sub AddDataToGrid()


    Dim currentClipboardData As DataObject = _



    ' 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



      ' Get the current time and place that in another cell


      Dim newTimeCell As New DataGridViewTextBoxCell

      newTimeCell.Value = Now.ToLongTimeString



      ' Add our row to the data grid and select it



      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


    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( _


    _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


      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


      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


      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



  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


    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( _


    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.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




    If Not _internalHold Then AddDataToGrid()


    SendMessage(_nxtCbVwrHWnd, m.Msg, m.WParam, m.LParam)


    ' Another clipboard viewer has removed itself




    If m.WParam = CType(_nxtCbVwrHWnd, IntPtr) Then

      _nxtCbVwrHWnd = m.LParam


      SendMessage(_nxtCbVwrHWnd, m.Msg, m.WParam, m.LParam)

    End If


    End Select



  End Sub


End Class


Public Class PaletteToolStrip


  Inherits ToolStrip


  Public Sub New()


  End Sub


  Public Sub New(ByVal ParamArray Items() As ToolStripItem)


  End Sub


  Protected Overrides Sub WndProc(ByRef m As Message)

    If m.Msg = &H21 AndAlso CanFocus AndAlso Not Focused Then


    End If


  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)



                locName = rm.GetString(lid)


              End Try

            End If


            ' Add the information to our data structures





            If cma.GroupName IsNot Nothing AndAlso _

              Not groups.Contains(cma.GroupName) Then


            End If

          End If






  ' 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()


    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 = _



      ' Open the main AutoCAD (or vertical) and "Applications" keys


      Dim ack As RegistryKey = _

        hive.OpenSubKey( _


      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


          Exit Sub

        End If



      ' 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), _



      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, _



      End If



    End Sub


    Private Shared Sub RemoveDemandLoadingEntries( _

      ByVal currentUser As Boolean)




        ' Choose a Registry hive based on the function input


        Dim hive As RegistryKey = _



        ' Open the main AutoCAD (or vertical) and "Applications" keys


        Dim ack As RegistryKey = _

          hive.OpenSubKey( _


        Dim appk As RegistryKey = _

          ack.OpenSubKey("Applications", True)


        ' Delete the key with the same name as this assembly


        appk.DeleteSubKeyTree( _





      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.

blog comments powered by Disqus


10 Random Posts