Kean Walmsley


  • About the Author
    Kean on Google+

April 2014

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      







« 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

    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.

blog comments powered by Disqus

10 Random Posts