Viru took the codebase he developed for the DWF/PDF Batch Publish tool and created ScriptPro 2.0, written from the ground up in C# and developed without any dependency on a specific AutoCAD version. Viru’s approach uses the equivalent of late binding to call into AutoCAD through COM and so doesn’t require a specific AutoCAD Type Library. Which means the tool should also work for future versions of AutoCAD: there’s no longer any need to wait for an update to the tool. And one advantage of being written in a .NET language is that the application works as well on 64-bit platforms as it does on 32-bit ones (a historical frustration of ScriptPro users).

ScriptPro 2.0 has a new project format – with the file extension .BPL (for Batch Process List). While it can load old ScriptPro projects (.SCP), it doesn’t yet have perfect feature parity – there are some ScriptPro-specific keywords that have not yet been implemented in the 2.0 release, but (providing we hear it’s important to do so) we’ll get there.

The tool has a ribbon UI – built using WPF – but the bulk of the functionality is packaged up in a WinForms User Control and hosted by the WPF app (me may yet move the whole thing to WPF, in time). Here’s the new UI:

And the best thing is that – as a Plugin of the Month – it comes with the complete source code for you to dig into and extend. Be sure to take a look at this very exciting new tool and let us know what you think.

October 13, 2010

I had a very nice surprise in my inbox, this morning. Thorsten Meinecke, from GTB in Berlin, decided to convert the VB.NET code contained in the last post into an F# script and to share it with this blog’s readership. Thanks, Thorsten! :-)

One thing about it being an F# script (typically stored in a .fsx file) is that it can be loaded and executed directly from the “F# Interactive” (FSI) component in Visual Studio without the need to build it into a project creating an executable. What’s also very nice is that the XAML defining the WPF dialog is embedded directly into the script, making it a simple matter to copy & paste the application into Visual Studio.

Here’s the F# code:

#I@"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"

#r"PresentationCore.dll"

#r"PresentationFramework.dll"

#r"WindowsBase.dll"

#r"WindowsFormsIntegration.dll"

open System.IO

open System.Windows.Forms

open System.Windows

open System.Windows.Controls

open System.Windows.Markup

open System.Windows.Media

let xaml =

"<Window

xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'

xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'

Title='Sort On Drawing Version' Height='327' Width='511'

WindowStartupLocation='CenterScreen'>

<Grid Height='Auto'>

<Grid.ColumnDefinitions>

<ColumnDefinition Width='270*' />

<ColumnDefinition Width='270*' />

</Grid.ColumnDefinitions>

<Label Grid.ColumnSpan='2' Height='25' HorizontalAlignment='Left'

Name='Label1' VerticalAlignment='Top'

Width='77'>Root folder:</Label>

<TextBox Grid.ColumnSpan='2' Height='26' Margin='73,-1,166,0'

Name='FolderBox' VerticalAlignment='Top' />

<Button Height='29' Margin='5,0,0,33' Name='CopyButton'

VerticalAlignment='Bottom' IsEnabled='False'>Copy</Button>

<Button Grid.ColumnSpan='2' Height='28' Margin='0,0,85,0'

Name='BrowseButton' VerticalAlignment='Top' Width='75'

HorizontalAlignment='Right'>Browse...</Button>

<ListView Grid.ColumnSpan='2' Margin='5,30,6,68'

Name='FileList'/>

<Button Height='28' HorizontalAlignment='Right' Margin='0,0,6,0'

Name='ListButton' VerticalAlignment='Top' Width='73'

IsEnabled='False' Grid.Column='1'>List</Button>

<ProgressBar Grid.ColumnSpan='2' Height='28' Margin='5,0,6,2'

Name='SortProgress' VerticalAlignment='Bottom' />

<Button Grid.Column='1' Height='29' Margin='0,0,6,33'

Name='MoveButton' VerticalAlignment='Bottom'

IsEnabled='False'>Move</Button>

</Grid>

</Window>"

let (?) (this : Control) (prop: string) : 'T =

this.FindName prop :?> 'T

let (+=) e f = Observable.add f e

let window = XamlReader.Parse xaml :?> Window

let label1: Label = window?Label1

let folderBox: TextBox = window?FolderBox

let copyButton: Button = window?CopyButton

let browseButton: Button = window?BrowseButton

let fileList: ListView = window?FileList

let listButton: Button = window?ListButton

let sortProgress: ProgressBar = window?SortProgress

let moveButton: Button = window?MoveButton

let getVersion fn =

let (|StartsWith|_|) arg (s: string) =

if s.StartsWith arg then Some() else None

let data = Array.create 6 0uy

use fs = File.OpenRead fn

try

if fs.Read(data, 0, 6) = 6 then

match (new System.Text.ASCIIEncoding()).GetString data with

| StartsWith "MC0.0"->"R1.0"

| StartsWith "AC1.2"->"R1.2"

| StartsWith "AC1.40"->"R1.4"

| StartsWith "AC1.50"->"R2.05"

| StartsWith "AC2.10"->"R2.10"

| StartsWith "AC2.21"->"R2.21"

| StartsWith "AC2.22"->"R2.22"

| StartsWith "AC1001"->"R2.22"

| StartsWith "AC1002"->"R2.5"

| StartsWith "AC1003"->"R2.6"

| StartsWith "AC1004"->"R9"

| StartsWith "AC1006"->"R10"

| StartsWith "AC1009"->"R11"

| StartsWith "AC1012"->"R13"

| StartsWith "AC1014"->"R14"

| StartsWith "AC1015"->"2000"

| StartsWith "AC1018"->"2004"

| StartsWith "AC1021"->"2007"

| StartsWith "AC1024"->"2010"

| _ ->"Unknown"

else""

finally

fs.Close()

let messageBox (s: string) =

MessageBox.Show(s, "Sort On Drawing Version" ) |> ignore

let sortFiles move =

let numSorted = ref 0

let numSkipped = ref 0

try

if fileList.Items.Count = 0 then

"Nothing to sort!" |> messageBox

else

sortProgress.Minimum <- 0.

sortProgress.Maximum <- fileList.Items.Count - 1 |> float

sortProgress.Value <- 0.

for fn in Seq.cast fileList.Items do

let ver = getVersion fn

if not(System.String.IsNullOrEmpty ver) then

let loc = Path.Combine(folderBox.Text, ver)

if not(Directory.Exists loc) then

Directory.CreateDirectory loc |> ignore

let dest = Path.Combine(loc, Path.GetFileName fn)

if not(File.Exists dest) then

if move then

File.Move(fn, dest)

else

File.Copy(fn, dest)

incr numSorted

else

incr numSkipped

sortProgress.Value <- sortProgress.Value + 1.

System.String.Format(

"{0} file{1} {2}, {3} (already existing) file{4} skipped.",

!numSorted,

(if !numSorted = 1 then""else"s"),

(if move then"moved"else"copied"),

!numSkipped,

(if !numSkipped = 1 then""else"s" ) )

|> messageBox

sortProgress.Value <- 0.

fileList.ItemsSource <- null

with ex ->

"A problem was found while sorting files: " + ex.Message

|> messageBox

browseButton.Click +=

fun _ ->

let fbd =

new FolderBrowserDialog(

Description =

"Select the root folder for the DWG version sort:" )

if Directory.Exists folderBox.Text then

fbd.SelectedPath <- folderBox.Text

let dr = fbd.ShowDialog()

if dr = DialogResult.OK then

folderBox.Text <- fbd.SelectedPath

folderBox.TextChanged +=

fun e ->

let tb = e.Source :?> TextBox

listButton.IsEnabled <- Directory.Exists tb.Text

listButton.Click +=

fun _ ->

try

fileList.ItemsSource <-

Directory.GetFiles(

folderBox.Text, "*.dwg", SearchOption.AllDirectories )

with _ ->

"A problem was found accessing sub-folders in this " +

"location: will simply get the drawings in the root " +

"folder."

|> messageBox

fileList.ItemsSource <-

Directory.GetFiles(folderBox.Text, "*.dwg")

copyButton.IsEnabled <- true

moveButton.IsEnabled <- true

moveButton.Click += fun _ -> sortFiles true

copyButton.Click += fun _ -> sortFiles false

#if COMPILED

[<System.STAThread>]

[<EntryPoint>]

let main _ = (new Application()).Run window

#else

(new Application()).Run window

#endif

The simple way to run this code, for those unfamiliar with using F# in Visual Studio, is to paste it into a file (usually a .fsx file), select the contents and use Alt-Enter to load it into FSI. That should cause the dialog shown in the previous post to display, and to work in an identical manner.

October 11, 2010

A friend in our Product Support team in Neuchâtel asked me to help develop a little application to sort drawing files on disk. He has some old Clipper scripts that he used during a previous life as a CAD Manager to sort drawing files into different folders based on their version (held in the first six bytes of the DWG file, as mentioned in this previous post and this post of Shaan’s). We decided I’d implement it in VB.NET, to make it easier for him to understand and extend.

The application is simple enough, but could make a mess of your file system: we started with just a “Copy" function but decided to extend it to cover “Move”. These functions place files in sub-folders of the selected root folder named after the version numbers of the drawings encountered:

DWGs folder

|_ R11

|_ R14

|_ 2000

|_ 2004

|_ 2007

|_ 2010

The “List” process parses sub-folders until it encounters an exception, at which point it defaults to listing only the drawings in the root folder. The DWGs from the various folders all then get either copied or moved into one of the version folders under the root.

Because of the potential for harm to folder hierarchies brought by introducing “Move”, I’ve decided to leave the code in source form only (and here it is). Which means someone needs to build it to make use of it, and assuming they can do that then they can presumably take responsibility for whatever the code does to their (or their users’) file system(s). ;-)

Here’s the main VB.NET file:

Imports System.Windows.Forms

Imports System.IO

Imports System.Text

Class SortOnVersionWindow

PrivateSub BrowseButton_Click( _

ByVal sender As System.Object, _

ByVal e As System.Windows.RoutedEventArgs) _

Handles BrowseButton.Click

Dim fbd As FolderBrowserDialog = New FolderBrowserDialog()

fbd.Description = _

"Select the root folder for the DWG version sort:"

If Directory.Exists(FolderBox.Text) Then

fbd.SelectedPath = FolderBox.Text

EndIf

Dim dr As DialogResult = fbd.ShowDialog()

If dr = Forms.DialogResult.OK Then

FolderBox.Text = fbd.SelectedPath

EndIf

EndSub

PrivateSub FolderBox_TextChanged( _

ByVal sender As System.Object, _

ByVal e As System.Windows.Controls.TextChangedEventArgs) _

Handles FolderBox.TextChanged

Dim tb As System.Windows.Controls.TextBox = _

DirectCast(sender, System.Windows.Controls.TextBox)

ListButton.IsEnabled = Directory.Exists(tb.Text)

EndSub

PrivateSub ListButton_Click( _

ByVal sender As System.Object, _

ByVal e As System.Windows.RoutedEventArgs) _

Handles ListButton.Click

Try

FileList.ItemsSource = _

Directory.GetFiles( _

FolderBox.Text, "*.dwg", SearchOption.AllDirectories)

Catch ex As Exception

MessageBox.Show( _

"A problem was found accessing sub-folders in this " + _

"location: will simply get the drawings in the root " + _

"folder.", _

"Sort On Drawing Version")

FileList.ItemsSource = _

Directory.GetFiles(FolderBox.Text, "*.dwg")

EndTry

CopyButton.IsEnabled = True

MoveButton.IsEnabled = True

EndSub

PrivateSub MoveButton_Click( _

ByVal sender As System.Object, _

ByVal e As System.Windows.RoutedEventArgs) _

Handles MoveButton.Click

SortFiles(True)

EndSub

PrivateSub CopyButton_Click( _

ByVal sender As System.Object, _

ByVal e As System.Windows.RoutedEventArgs) _

Handles CopyButton.Click

SortFiles(False)

EndSub

PrivateSub SortFiles(ByVal move AsBoolean)

Dim numSorted AsInteger = 0

Dim numSkipped AsInteger = 0

Try

If FileList.Items.Count = 0 Then

MessageBox.Show( _

"Nothing to sort!", _

"Sort On Drawing Version")

Return

EndIf

SortProgress.Minimum = 0

SortProgress.Maximum = FileList.Items.Count - 1

SortProgress.Value = 0

ForEach fn In FileList.Items

Dim ver AsString = GetVersion(fn)

IfNotString.IsNullOrEmpty(ver) Then

Dim loc AsString = FolderBox.Text + "\" + ver

IfNot Directory.Exists(loc) Then

Directory.CreateDirectory(loc)

EndIf

Dim dest AsString = loc + "\" + Path.GetFileName(fn)

IfNot File.Exists(dest) Then

If move Then

File.Move(fn, dest)

Else

File.Copy(fn, dest)

EndIf

numSorted += 1

Else

numSkipped += 1

EndIf

SortProgress.Value += 1

EndIf

Next

MessageBox.Show( _

String.Format( _

"{0} file{1} {2}, {3} (already existing) file{4} skipped.", _

numSorted, IIf(numSorted = 1, "", "s"), _

IIf(move, "moved", "copied"), numSkipped, _

IIf(numSkipped = 1, "", "s")), _

"Sort On Drawing Version")

SortProgress.Value = 0

FileList.ItemsSource = Nothing

Catch ex As Exception

MessageBox.Show( _

"A problem was found while sorting files: " + _

ex.Message, _

"Sort On Drawing Version")

EndTry

EndSub

PrivateFunction GetVersion(ByVal fn AsString) AsString

Dim data(5) AsByte

Dim res AsInteger

Dim s AsString = String.Empty

Dim fs As FileStream = File.OpenRead(fn)

Using (fs)

res = fs.Read(data, 0, 6)

If res = 6 Then

Dim enc As ASCIIEncoding = New ASCIIEncoding()

s = enc.GetString(data)

Dim sh AsString = s.Substring(0, 5)

If sh = "MC0.0"Then

Return"R1.0"

ElseIf sh = "AC1.2"Then

Return"R1.2"

ElseIf s = "AC1.40"Then

Return"R1.4"

ElseIf s = "AC1.50"Then

Return"R2.05"

ElseIf s = "AC2.10"Then

Return"R2.10"

ElseIf s = "AC2.21"Then

Return"R2.21"

ElseIf s = "AC2.22"Then

Return"R2.22"

ElseIf s = "AC1001"Then

Return"R2.22"

ElseIf s = "AC1002"Then

Return"R2.5"

ElseIf s = "AC1003"Then

Return"R2.6"

ElseIf s = "AC1004"Then

Return"R9"

ElseIf s = "AC1006"Then

Return"R10"

ElseIf s = "AC1009"Then

Return"R11"

ElseIf s = "AC1012"Then

Return"R13"

ElseIf s = "AC1014"Then

Return"R14"

ElseIf s = "AC1015"Then

Return"2000"

ElseIf s = "AC1018"Then

Return"2004"

ElseIf s = "AC1021"Then

Return"2007"

ElseIf s = "AC1024"Then

Return"2010"

Else

Return"Unknown"

EndIf

EndIf

fs.Close()

EndUsing

Return s

EndFunction

EndClass

Here’s the main dialog, implemented using WPF, to give you an idea of how it works:

This tool may end up being a Plugin of the Month, at some point, but we’ll see. I worry about the damage that can be done by “Move” (unless we maintain some kind of undo stack or display prominent health warnings).

On the subject of our Plugins of the Month… Viru Aithal, who manages our DevTech team in India, is beavering away on a replacement for ScriptPro (whether we use the ScriptPro name or simply call it “Batch Processor” remains to be seen). He’s reimplemented – and in many ways extended – the popular ScriptPro tool using .NET, which also means it will work well on 64-bit systems (a limitation of the current VB6-implemented ScriptPro). If anyone is interested in Beta-testing this tool prior to its release, please drop me an email.

March 01, 2010

I’m very pleased to announce the availability of this really interesting Plugin of the Month over on Autodesk Labs: Batch Publish for AutoCAD. Here’s an excerpt from the ReadMe (which I happen to have written, but anyway):

This plugin can be used with AutoCAD to simplify the process of publishing sets of drawings to DWF and/or PDF. It runs as a command within AutoCAD – as opposed to a separate executable – and uses a separate executable to monitor AutoCAD's health and restart it, as needed. The status of the batch publishing operation is stored to disk, allowing it to pick up from where it left off and also for any failed documents to be retried without starting from scratch. A setting is available to only publish drawings that have been modified since they were last published, making it easier to publish sets of documents on a regular basis.

Varadan Krishnan and Viru Aithal from our DevTech India team have been working hard on this tool for some time. Viru and Varadan have added some very interesting enhancements over the last few months, as the plugin has evolved, and I think it’s going to prove a very useful tool for anyone needing to publish sets of drawings to DWF or PDF.

Typical batch publishing applications often take the form of standalone applications which call through to AutoCAD using COM to drive the publishing of drawings. This application takes a slightly different approach: it runs in-process to AutoCAD, as a standard .NET plugin defining a BATCHPUBLISH command, and this command will load and publish the various documents you care about, one after the other. So you have access to your publishing task right there in the AutoCAD application: no need to launch anything external. That said, as you’re launching a batch processing operation – which may be trying to read problematic source drawings – it’s highly recommended that you save and close any drawings you’re working on. As we’ll see below, AutoCAD may get restarted forcefully during the operation, so it’s highly recommended to launch it from a fresh instance of AutoCAD (or at least one with no unsaved drawings open in the editor).

The application uses a couple of very interesting techniques which may be of interest to people looking at this from a development perspective. Firstly, it has a separate little “regulator” application which monitors the health of AutoCAD: if AutoCAD hangs on loading a corrupt drawing, or the publish operation is taking suspiciously long to complete (relative to a user-defined timeout value), the regulator kills the AutoCAD session and restarts it, making sure the batch publish operation starts back up just after the problematic drawing. Once the other drawings have completed, the user can then choose to retry publishing of any unpublished drawings, specifying a different timeout, as appropriate.

The other technique that I found interesting is the way the application calls through to AutoCAD via COM: it uses Document.SendCommand() to drive AutoCAD’s PUBLISH command synchronously, but – rather than creating a dependency on AutoCAD’s type library, which adds some complexity in terms of multiple version support – the code uses Type.InvokeMember() to dynamically call SendCommand() without needing the type library. The same effect can be achieved in VB quite easily using late binding and now in C# 4.0 we have the dynamic keyword or can embed specific types from a type library rather than needing the whole thing – but for older versions of C# this is a viable and interesting technique if you’re only interested in a small portion of an application’s COM object model.

The application makes heavy use of an XML file to persist information about the status of the batch publishing operation. This has a number of benefits, especially related to resuming the operation in case AutoCAD needs restarting, as well as restoring some temporarily-overridden system variables when everything’s finished. The approach used to read and write the XML is probably slightly lower-level than might have been used – especially as .NET provides some very interesting object-level XML serialization capabilities – but the need to persist a variety of different pieces of data at various times during the batch publishing operation led to this approach being adopted, for better or worse. The current implement works well, but this is potentially one section of code that might be streamlined in a future release.

Now let’s take a look at the BATCHPUBLISH command in action…

The first thing to do is to copy both the program files provided - ADNPlugin-BatchPublish.dll and its companion ADNPlugin-BatchPublishRegulator.exe – into the same folder on your local hard-drive (preferably AutoCAD’s main Program Files folder).

Then we can NETLOAD the DLL into AutoCAD – which will create demand-loading entries for subsequent loads – and launch the BATCHPUBLISH command:

Once we’ve filled out the information in this main dialog…

… hitting configure will allow us to do additional fine-tuning of the application settings. We can choose whether to look for both DWG and DXF files in the source directory and we as having the option to exclude specific files or layouts. We can also change the timeout after which AutoCAD will be killed and restarted, as well as being able to choose whether to publish only the drawings that have been modified since the last time they were published (the alternative being to force a re-publish of everything).

Once we’ve finished, we can select “Done” and then launch the batch publish via the “Publish” button on the main form. You’ll now see AutoCAD publishing the various drawings from the specified source folder, along with a small dialog displayed by the regulator application. This will count down from our timeout threshold and will restart AutoCAD when it gets to zero.

30 seconds isn’t a very long time to publish both a DWF and PDF for complex drawings. If it’s clear that the timeout isn’t giving enough time for AutoCAD to complete both and the publish seems to be happening correctly, then you can either close the regulator (which will lead the rest of the batch publish to not be monitored, but will otherwise have no ill effects on the operation) or you can let AutoCAD be restarted and then try again later with a larger timeout value. In our case we’re going to do that.

Once AutoCAD has been killed we’ll see this message from the regulator:

The application waits for 10 seconds after killing AutoCAD before restarting it…

… and the batch publish operation should continue until all drawings have either been published or have failed to do so:

From here we can use “Retry” to attempt to publish any failed drawings again (possibly with a longer timeout specified in the top right of the dialog):

We should now find our output folders filled with DWFs…

… and with PDFs:

You’ll see two log files in each folder, as it took us two batch publish attempts to get the whole set of drawings published.

Please give this tool a try and let us know if you have any feedback. Batch processing tools are often quite hard to get right, so please don’t be discouraged if it doesn’t work perfectly on some of your drawings: please let us know and we’ll do what we can to fix the issue.

June 24, 2009

In this recent post we looked at an approach combining AutoLISP with a script generated on-the-fly to get around the fact that (command "_.OPEN" …) does nothing when SDI == 0. As mentioned in a comment in the post, I realised that the approach of using a single master script to do this is more prone to failure: a number of commands can cause scripts to stop executing, for instance, so it would be better practice to minimise the operations contained in a particular script to increase the application’s fault tolerance.

This modified approach was suggested by a member of our Engineering team in a recent thread (one that I came across after Monday’s post). It uses a data file to store a list of the drawings to process and only creates a script to load – and launch processing on – the next drawing in that list:

(defun C:BATCH(/ dwgs lsp-name data-name)

(setq dwgs '("C:/A.DWG" "C:/B.DWG" "C:/C.DWG" "C:/D.DWG")

lsp-name "c:/tmp.lsp"

data-name "c:/dwgs.tmp"

)

(create-drawing-list data-name dwgs)

(process-next-drawing data-name lsp-name "(create-circle)"T T)

(princ)

)

(defun create-circle()

(command"_.CIRCLE" "10,10,0" "5")

)

(defun create-drawing-list(data dwgs / f dwg)

;; Create data file containing the DWG names

(setq f (open data-name "w"))

(foreach dwg dwgs

(write-line dwg f)

)

(close f)

)

;; Get the first drawing from the list, removing it

(defun get-next-drawing(data / dwg dwgs f)

;; Read in the whole list of DWGs

(setq f (open data "r")

dwgs '()

)

(while (setq dwg (read-line f))

(setq dwgs (cons dwg dwgs))

)

(close f)

;; Reverse the list, take the head and write

;; back the remainder to the file

(if(>(length dwgs) 0)

(progn

(setq dwgs (reverse dwgs)

dwg (car dwgs)

dwgs (cdr dwgs)

)

(setq f (open data "w"))

(foreach dwg dwgs

(write-line dwg f)

)

(close f)

)

)

dwg

)

;; Process the current drawing and use a script to open

;; the next one in the list

(defun process-next-drawing(data lsp func save first / scr)

(setq scr "c:/tmp.scr")

;; We only want to run the function if not the first

;; time called... the same for save

(if(not first)

(progn

(eval(read func))

(if save

(command"_.QSAVE")

)

)

)

;; Get the next DWG name from the file

(setq dwg (get-next-drawing data))

;; If there is one, create a script to open it, reload

;; the application and process our function

(if dwg

(progn

(create-script scr data dwg lsp save first)

(command"_.SCRIPT" scr)

)

;; For the last drawing we simply close it and

;; delete the now-empty data file

(progn

(vl-file-delete data)

(vl-file-delete scr)

(command"_.CLOSE")

)

)

)

;; Create a script to close the current drawing and

;; open the next, calling back to our process function

;; (after having loaded the file that defines it)

(defun create-script(scr data dwg lsp save first / f)

(setq f (open scr "w"))

(if(not first)

(write-line"_.CLOSE" f)

)

(write-line

(strcat"_.OPEN \"" dwg "\"") f

)

(write-line

(strcat"(load \"" lsp "\")") f

)

(write-line

(strcat

"(process-next-drawing \""

data "\" \"" lsp "\" \"" func "\" "

(if save "T" "nil")" nil)"

)

f

)

(close f)

(princ)

)

I hope this is useful to people – as mentioned before, please do provide feedback on how/whether this works for you…

June 22, 2009

As mentioned in this previous post, there has been some discussion internally around the future of SDI. Given the change this will bring to applications, SDI is going to be around until we next deliberately choose to break binary application compatibility (something we just did with AutoCAD 2010 and typically try to do only every three releases).

That said, SDI is very likely to go away at some point, so it does seem worth drilling further into the reasons for using it and trying to determine an appropriate way to remove current dependencies on it.

Thanks to all of you who responded to my previous post and provided input on your use of SDI. The most common theme was around the use of SDI to batch-process sets of drawings: opening each one, performing an operation and (optionally) saving before opening the next.

My understanding is that SDI makes life easier, in this situation, because the closing of one drawing is taken care of automatically when opening the next and so LISP applications can operate more easily across multiple drawings.

I did some tests, to see how it helps, but the LISP part of my brain has unfortunately atrophied, over the years: I wasn’t able to get a simple SDI-only, batch processing application to work, as it always stopped once a new drawing was open. It may be that some use of script files is needed – and this certainly can make life easier, as we’ll see below – but it would be good if someone could help me out by posting a comment or dropping me an email. I’m sure I’m missing something very simple.

Anyway, irrespective of whether I was able to use SDI successfully, or not, I was able to get something working from an MDI environment that I hope to be equivalent, functionality-wise.

Thanks to guidance from Wayne Brill, a member of DevTech Americas, I was able to put together some LISP code that makes use of a temporary script file to handle the opening, processing, (saving) and closing of drawings.

Here’s the LISP application:

(defun C:BATCH(/ dwgs scr-name lsp-name)

(setq dwgs '("C:/A.DWG" "C:/B.DWG" "C:/C.DWG" "C:/D.DWG")

scr-name "c:/tmp.scr"

lsp-name "c:/batch.lsp"

)

(create-script scr-name dwgs lsp-name "(CreateCircle)"T)

(command"_.SCRIPT" scr-name)

(vl-file-delete scr-name)

(princ)

)

(defun CreateCircle()

(command"_.CIRCLE" "0,0,0" "30")

)

(defun create-script(scr dwgs lsp cmd save / f dwg)

(setq f (open scr "w"))

(foreach dwg dwgs

(progn

(write-line

(strcat"_.OPEN \"" dwg "\"") f

)

(write-line

(strcat"(load \"" lsp "\")") f

)

(write-line cmd f)

(if save

(write-line"_.QSAVE" f)

)

(write-line"_.CLOSE" f)

)

)

(close f)

(princ)

)

The script handles the opening of each drawing, reloading the LISP file (which I have saved in c:/tmp.lsp – this file is pointed at by the lsp-name variable in the C:BATCH function) inside each one and running the specified command/function before saving & closing. In this case we’re running a simple function that uses a command to create a circle – if we were doing something that didn’t require the drawing to be saved (if we were just querying data, for instance) we could pass nil instead of T into the (create-script) function.

To take a look at the script being used behind the scenes, you can simply comment out the call to (vl-file-delete) and open up the contents in your favourite text editor:

_.OPEN "C:/A.DWG"

(load "c:/tmp.lsp")

(CreateCircle)

_.QSAVE

_.CLOSE

_.OPEN "C:/B.DWG"

(load "c:/tmp.lsp")

(CreateCircle)

_.QSAVE

_.CLOSE

_.OPEN "C:/C.DWG"

(load "c:/tmp.lsp")

(CreateCircle)

_.QSAVE

_.CLOSE

_.OPEN "C:/D.DWG"

(load "c:/tmp.lsp")

(CreateCircle)

_.QSAVE

_.CLOSE

I hope this approach goes some way towards helping people batch-process drawings from LISP without having to move to a different language. I’d really appreciate your feedback on this subject, as it would be good to get a more definitive approach nailed down before this change (eventually) becomes a requirement.

May 25, 2009

Thanks to all of your interest in this recent post, which looked at a way to interface an out-of-process .NET application with an assembly running in-process to AutoCAD. After some obvious functionality gaps were raised, Renze de Waal, one of our ADN members, pointed out a DevNote on the ADN website covering – and more completely addressing – this topic. Shame on me for not checking there before writing the post. Anyway, onwards and upwards…

The information in the DevNote highlights some of the problems I and other people had hit with my previous code, mostly related to the fact it wasn’t executed on the main AutoCAD thread (which meant we were effectively limited in the interactions we had with the AutoCAD application).

To fix this we can derive our application from System.EnterpriseServices.ServicedComponent (also adding an additional project reference to the System.EnterpriseServices .NET assembly). Here is the updated C# code for the LoadableComponent:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

using System.Runtime.InteropServices;

using System.EnterpriseServices;

namespace LoadableComponent

{

[Guid("5B5B731C-B37A-4aa2-8E50-42192BD51B17")]

publicinterfaceINumberAddition

{

[DispId(1)]

string AddNumbers(int arg1, double arg2);

}

[ProgId("LoadableComponent.Commands"),

Guid("44D8782B-3F60-4cae-B14D-FA060E8A4D01"),

ClassInterface(ClassInterfaceType.None)]

publicclassCommands : ServicedComponent, INumberAddition

{

// A simple test command, just to see that commands

// are loaded properly from the assembly

[CommandMethod("MYCOMMAND")]

staticpublicvoid MyCommand()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

ed.WriteMessage("\nTest command executed.");

}

// A function to add two numbers and create a

// circle of that radius. It returns a string

// withthe result of the addition, just to use

// a different return type

publicstring AddNumbers(int arg1, double arg2)

{

// During tests it proved unreliable to rely

// on DocumentManager.MdiActiveDocument

// (which was null) so we will go from the

// HostApplicationServices' WorkingDatabase

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

ed.WriteMessage(

"\nAdd numbers called with {0} and {1}.",

arg1, arg2

);

// Perform our addition

double res = arg1 + arg2;

// Lock the document before we access it

DocumentLock loc = doc.LockDocument();

using (loc)

{

Transaction tr =

db.TransactionManager.StartTransaction();

using (tr)

{

// Create our circle

Circle cir =

newCircle(

newPoint3d(0, 0, 0),

newVector3d(0, 0, 1),

res

);

cir.SetDatabaseDefaults(db);

// Add it to the current space

BlockTableRecord btr =

(BlockTableRecord)tr.GetObject(

db.CurrentSpaceId,

OpenMode.ForWrite

);

btr.AppendEntity(cir);

tr.AddNewlyCreatedDBObject(cir, true);

// Commit the transaction

tr.Commit();

}

}

// Return our string result

return res.ToString();

}

}

}

Some points to note...

We now use an interface to expose functionality from our component, which allows us more flexibility in the way we return data to the calling application.

We're labeling our interface and component with specific GUIDs - generated by guidgen.exe - although we could probably skip this step.

We're now able to use the MdiActiveDocument property safely, as well as being able to write messages via the editor.

When we build the component we can - as before - register it via the regasm.exe tool. Here's the .reg output if you specify the /regfile option:

One thing to mention - I found that the calling application was not able to to cast the returned System.__COMObject to LoadableComponent.INumberAddition unless I updated the project settings to "Register from COM Interop" (near the bottom of the Build tab).

Now for our calling application… here’s the updated C# code:

using Autodesk.AutoCAD.Interop;

using System.Windows.Forms;

using System.Runtime.InteropServices;

using System.Reflection;

using System;

using LoadableComponent;

namespace DrivingAutoCAD

{

publicpartialclassForm1 : Form

{

public Form1()

{

InitializeComponent();

}

privatevoid button1_Click(object sender, EventArgs e)

{

conststring progID = "AutoCAD.Application.18";

AcadApplication acApp = null;

try

{

acApp =

(AcadApplication)Marshal.GetActiveObject(progID);

}

catch

{

try

{

Type acType =

Type.GetTypeFromProgID(progID);

acApp =

(AcadApplication)Activator.CreateInstance(

acType,

true

);

}

catch

{

MessageBox.Show(

"Cannot create object of type \"" +

progID + "\""

);

}

}

if (acApp != null)

{

try

{

// By the time this is reached AutoCAD is fully

// functional and can be interacted with through code

acApp.Visible = true;

INumberAddition app =

(INumberAddition)acApp.GetInterfaceObject(

"LoadableComponent.Commands"

);

// Now let's call our method

string res = app.AddNumbers(5, 6.3);

acApp.ZoomAll();

MessageBox.Show(

this,

"AddNumbers returned: " + res

);

}

catch (Exception ex)

{

MessageBox.Show(

this,

"Problem executing component: " +

ex.Message

);

}

}

}

}

}

You should be able to see straightaway that it’s simpler – we cast the results of the GetInterfaceObject call to our interface and call the AddNumbers method on it.

And when we execute the code, we can see we’re now able to write to the command-line, as well as getting better results from our ZoomAll():

August 15, 2007

In the last post we looked at some code to programmatically purge Registered Application names from the drawing currently active in AutoCAD. In this post we take the "batching" code first used in this previous post and apply it to this problem.

What we end up with is an additional command called PF which asks the user to specify a folder and then purges the RegApps from the DWGs in that folder, saving those files that end up being modified with the "_purged" suffix.

One point to note is the use of the Database.RetainOriginalThumbnailBitmap property: as we're not making any graphical changes it's fairly safe to set this to true, which retains the pervious thumbnail bitmap, rather than it being blank in the new drawing. If you were to set it to true after graphical changes nothing especially serious would happen, but it could be confusing for users if the preview differed substantially from the DWG contents.

August 08, 2007

This post finally takes the code last shown in this previous post, migrating it to use RealDWG to update a folder of DWGs without the need for AutoCAD to be installed on the system. A big thanks to Adam Nagy, a member of DevTech working from our Prague office, who turned around my request to convert the code to work with RealDWG in a matter of hours (if not minutes).

Firstly I need to make it clear that this code will not run without both RealDWG installed (I'm using RealDWG 2007, as the file format didn't change between 2007 and 2008) and a "clear text license key" inserted in the code. You'll see some missing lines (lines 9-15), where it needs to be inserted. Once you've licensed RealDWG you can get this key from Autodesk, allowing you to create RealDWG applications using .NET.

Below is the C# code, with the lines that have been added since the previous entry in red. Firstly, a summary of the changes...

There are clearly lines that are no longer needed - these have just been deleted.

In terms of the additional lines: it's a mixture of code that replaces the use of the AutoCAD editor for user-input, with some additional code needed specifically by RealDWG applications.

Lines 22-122 implement the HostApplicationServices class for our application, which RealDWG will call under certain circumstances, such as when it's trying to find particular support files. The FindFile() function has been implemented to search the Windows fonts folder and the RealDWG install folder for any fonts the system needs to adequately load a DWG. You would need to modify the code to point to the folder your application installs fonts into. Additionally I suspect there's work needed to open files that have fonts missing, mapping alternate fonts in their place: this post assumes that the fonts are all available; in a future post we can certainly look at adding support for alternate font mapping.

These fonts are especially important when dealing with alignment of text and attributes. If RealDWG cannot find the fonts on the system, the DWG will be updated with the new text but the attributes will not be positioned correctly (until they are edited in some way inside the AutoCAD editor). This is quite a common issue when developing with RealDWG, but thankfully one that's fairly easy to solve.

Lines 115-145 replace the use of the AutoCAD editor to prompt the user for the important information. In this case we're just using standard console functions for reading/writing data from/to a command window. This is also the reason for lines 174, 197, 208, 223, 233, 243 & 244 changing.

Line 185 sets the working database: this is very important - especially when working with fonts - and without it your attributes will not align properly.

The protocols of the UpdateAttributesInDatabase() and UpdateAttributesInBlock() functions have also been updated to include the static keyword, although I didn't mark those lines in red as they should probably have been static before. :-)

1using Autodesk.AutoCAD.DatabaseServices;

2using Autodesk.AutoCAD.Runtime;

3using System.Reflection;

4using System.IO;

5using System;

6

7 [assembly: SecuredApplication(

8@"THIS IS AN OBJECTDBX (TM) VERSION 2007 CLIENT LICENSE FOR THE EXCLUSIVE USE OF Kean Walmsley. YOUR USE OF OBJECTDBX(TM) IS GOVERNED BY THE SOFTWARE LICENSE INCLUDED IN THE PRODUCT. USE OF THIS SOFTWARE IN VIOLATION OF THE SOFTWARE LICENSE IS A VIOLATION OF U.S. AND/OR INTERNATIONAL COPYRIGHT LAWS AND TREATIES AND YOU MAY BE SUBJECT TO CRIMINAL PENALTIES FOR SUCH USE.",

16

17namespace AttributeUpdater

18 {

19classProgram

20 {

21 #region RealDWG

22classMyHost : HostApplicationServices

23 {

24privatestring SearchPath(string fileName)

25 {

26// check if the file is already with full path

27if (System.IO.File.Exists(fileName))

28return fileName;

29

30// check application folder

31string applicationPath =

32Path.GetDirectoryName(

33Assembly.GetExecutingAssembly().Location

34 );

35if (File.Exists(applicationPath + "\\" + fileName))

36return applicationPath + "\\" + fileName;

37

38// search folders in %PATH%

39string []paths =

40Environment.GetEnvironmentVariable(

41"Path").Split(newchar[]{';'}

42 );

43foreach (string path in paths)

44 {

45// some folders end with \\, some don't

46string validatedPath

47 = Path.GetFullPath(path + "\\" + fileName);

48if (File.Exists(validatedPath))

49return validatedPath;

50 }

51

52// check the Fonts folders

53string systemFonts =

54Environment.GetEnvironmentVariable(

55"SystemRoot"

56 ) + "\\Fonts\\";

57if (File.Exists(systemFonts + fileName))

58return systemFonts + fileName;

59

60string rdwgFonts =

61"C:\\Program Files\\Autodesk RealDWG 2007\\Fonts\\";

62if (File.Exists(rdwgFonts + fileName))

63return rdwgFonts + fileName;

64

65return"";

66 }

67

68publicoverridestring FindFile(

69string fileName,

70Database database,

71FindFileHint hint

72 )

73 {

74// add extension if needed

75if (!fileName.Contains("."))

76 {

77string extension;

78switch (hint)

79 {

80caseFindFileHint.CompiledShapeFile:

81 extension = ".shx";

82break;

83caseFindFileHint.TrueTypeFontFile:

84 extension = ".ttf";

85break;

86caseFindFileHint.PatternFile:

87 extension = ".pat";

88break;

89caseFindFileHint.ArxApplication:

90 extension = ".dbx";

91break;

92caseFindFileHint.FontMapFile:

93 extension = ".fmp";

94break;

95caseFindFileHint.XRefDrawing:

96 extension = ".dwg";

97break;

98// Fall through. These could have

99// various extensions

100caseFindFileHint.FontFile:

101caseFindFileHint.EmbeddedImageFile:

102default:

103 extension = "";

104break;

105 }

106

107 fileName += extension;

108 }

109

110return SearchPath(fileName);

111 }

112 }

113 #endregion

114

115staticvoid Main(string[] args)

116 {

117// RealDWG specific

118RuntimeSystem.Initialize(newMyHost(), 1033);

119

120// Have the user choose the block and attribute

121// names, and the new attribute value

122

123 System.Console.Write(

124"\nEnter folder containing DWGs to process: "

125 );

126string pathName =

127 System.Console.ReadLine().ToUpper();

128

129 System.Console.Write(

130"\nEnter name of block to search for: "

131 );

132string blockName =

133 System.Console.ReadLine().ToUpper();

134

135 System.Console.Write(

136"\nEnter tag of attribute to update: "

137 );

138string attbName =

139 System.Console.ReadLine().ToUpper();

140

141 System.Console.Write(

142"\nEnter new value for attribute: "

143 );

144string attbValue =

145 System.Console.ReadLine().ToUpper();

146

147string[] fileNames =

148Directory.GetFiles(pathName,"*.dwg");

149

150// We'll use some counters to keep track

151// of how the processing is going

152

153int processed = 0, saved = 0, problem = 0;

154

155foreach (string fileName in fileNames)

156 {

157if (fileName.EndsWith(

158".dwg",

159StringComparison.CurrentCultureIgnoreCase

160 )

161 )

162 {

163string outputName =

164 fileName.Substring(

165 0,

166 fileName.Length - 4) +

167"_updated.dwg";

168

169Database db = newDatabase(false, true);

170using (db)

171 {

172try

173 {

174 System.Console.WriteLine(

175"\n\nProcessing file: " + fileName

176 );

177

178 db.ReadDwgFile(

179 fileName,

180FileShare.ReadWrite,

181false,

182""

183 );

184

185MyHost.WorkingDatabase = db;

186

187int attributesChanged =

188 UpdateAttributesInDatabase(

189 db,

190 blockName,

191 attbName,

192 attbValue

193 );

194

195// Display the results

196

197 System.Console.WriteLine(

198"\nUpdated {0} instance{1} of " +

199"attribute {2}.",

200 attributesChanged,

201 attributesChanged == 1 ? "" : "s",

202 attbName

203 );

204

205// Only save if we changed something

206if (attributesChanged > 0)

207 {

208 System.Console.WriteLine(

209"\nSaving to file: {0}", outputName

210 );

211

212 db.SaveAs(

213 outputName,

214DwgVersion.Current

215 );

216

217 saved++;

218 }

219 processed++;

220 }

221catch (System.Exception ex)

222 {

223 System.Console.WriteLine(

224"\nProblem processing file: {0} - \"{1}\"",

225 fileName,

226 ex.Message

227 );

228 problem++;

229 }

230 }

231 }

232 }

233 System.Console.WriteLine(

234"\n\nSuccessfully processed {0} files, of which {1} had " +

235"attributes to update and an additional {2} had errors " +

236"during reading/processing." +

237"\n[Press ENTER to close window]",

238 processed,

239 saved,

240 problem

241 );

242

243// Delay the closing of the command prompt

244 System.Console.Read();

245 }

246

247privatestaticint UpdateAttributesInDatabase(

248Database db,

249string blockName,

250string attbName,

251string attbValue

252 )

253 {

254// Get the IDs of the spaces we want to process

255// and simply call a function to process each

256

257ObjectId msId, psId;

258

259Transaction tr =

260 db.TransactionManager.StartTransaction();

261using (tr)

262 {

263BlockTable bt =

264 (BlockTable)tr.GetObject(

265 db.BlockTableId,

266OpenMode.ForRead

267 );

268 msId =

269 bt[BlockTableRecord.ModelSpace];

270 psId =

271 bt[BlockTableRecord.PaperSpace];

272

273// Not needed, but quicker than aborting

274 tr.Commit();

275 }

276int msCount =

277 UpdateAttributesInBlock(

278 db,

279 msId,

280 blockName,

281 attbName,

282 attbValue

283 );

284int psCount =

285 UpdateAttributesInBlock(

286 db,

287 psId,

288 blockName,

289 attbName,

290 attbValue

291 );

292return msCount + psCount;

293 }

294

295privatestaticint UpdateAttributesInBlock(

296Database db,

297ObjectId btrId,

298string blockName,

299string attbName,

300string attbValue

301 )

302 {

303// Will return the number of attributes modified

304

305int changedCount = 0;

306

307Transaction tr =

308 db.TransactionManager.StartTransaction();

309using (tr)

310 {

311BlockTableRecord btr =

312 (BlockTableRecord)tr.GetObject(

313 btrId,

314OpenMode.ForRead

315 );

316

317// Test each entity in the container...

318

319foreach (ObjectId entId in btr)

320 {

321Entity ent =

322 tr.GetObject(entId, OpenMode.ForRead)

323asEntity;

324

325if (ent != null)

326 {

327BlockReference br = ent asBlockReference;

328if (br != null)

329 {

330BlockTableRecord bd =

331 (BlockTableRecord)tr.GetObject(

332 br.BlockTableRecord,

333OpenMode.ForRead

334 );

335

336// ... to see whether it's a block with

337// the name we're after

338

339if (bd.Name.ToUpper() == blockName)

340 {

341// Check each of the attributes...

342

343foreach (

344ObjectId arId in br.AttributeCollection

345 )

346 {

347DBObject obj =

348 tr.GetObject(

349 arId,

350OpenMode.ForRead

351 );

352

353AttributeReference ar =

354 obj asAttributeReference;

355if (ar != null)

356 {

357// ... to see whether it has

358// the tag we're after

359

360if (ar.Tag.ToUpper() == attbName)

361 {

362// If so, update the value

363// and increment the counter

364

365 ar.UpgradeOpen();

366 ar.TextString = attbValue;

367 ar.DowngradeOpen();

368 changedCount++;

369 }

370 }

371 }

372 }

373

374// Recurse for nested blocks

375 changedCount +=

376 UpdateAttributesInBlock(

377 db,

378 br.BlockTableRecord,

379 blockName,

380 attbName,

381 attbValue

382 );

383 }

384 }

385 }

386 tr.Commit();

387 }

388return changedCount;

389 }

390 }

391 }

Here's the source file for download. Once again please note that this requires RealDWG and a clear text license key to be inserted in order to work.

When you run the executable this code builds, you should see something like this in a command prompt window: