Advanced Programming Techniques Vol4
Advanced Programming Techniques Vol4
Volume 4
Reinaldo N. Togores
AutoCAD expert’s Visual LISP.
Volume 4
Advanced Programming Techniques
All Rights Reserved. No part of this book may be reproduced or utilized in any form or by any means,
electronic or mechanical, including photocopying, recording, or by any information storage and
retrieval system, without permission in writing from the author.
All data, information and examples provided by this book are for educational purposes only. All
information is provided on an as-is basis. Although the author and publisher have made every effort
to ensure that the information in this book was correct at press time, the author and publisher do not
assume and hereby disclaim any liability to any party for any loss, damage, or disruption caused by
errors or omissions.
Autodesk, AutoCAD, AutoLISP, Visual LISP, DWG, the DWG logo, Revit, 3ds Max and Inventor ar
registered trademarks or trademarks of Autodesk, Inc., and/or its subsidiaries and/or affiliates in the
USA and other countries. Autodesk screen shots reprinted with the permission of Autodesk, Inc.
Contents
Volume 4.
Programming reactors that can trigger actions depending on the occurrence of certain events.
Creating graphical user interfaces for the management of our programs.
Adding alphanumeric information to the drawing, in most cases associated with graphic objects
and the creation of tables using that information within the drawing.
Managing other applications in the Windows environment using Visual LISP programs.
Creating Visual LISP compiled applications operating in their own namespace.
From the point of view of their relationship with the user and the environment in which they run we
can distinguish between three basic types of programs . These three types are usually known as
Sequential, Interactive and Event-driven programs.
The first computer programs were of the sequential type. These are programs that follow a predefined
order: Reading the required data, which must be structured in a fixed way, then performing the
calculations for which it was designed and returning the results usually as a printout or a file written
to a storage device. When running a sequential program the user simply waited while it did its work
until the command prompt returned, indicating that other program could then be run. Such programs
are also known as procedure-oriented programs.
When user intervention is required during program execution we refer to interactive programs. In an
interactive program the user provides data or selects which actions to execute by means of some kind
of communication interface such as the AutoCAD command line. The AutoCAD command that draws
polylines is a good example of this programming style. Initially the user can't do anything but pick a
point on the screen or type its coordinates. This done, he can choose between specifying the next
point or selecting the options for drawing an arc, closing the polyline, specify its width, etc. If
drawing an arc is selected, other options will be offered, such as specifying an angle, indicating the
arc's center, and so on. User participation is still restricted to a predefined order as to when to
intervene and the information that can then be entered or requested.
Event-driven programming is usually identified with programs that display a Graphic User Interface
(GUI) as those in Microsoft Windows, Mac OS X, and X Windows for desktop or laptop computers
and those used in mobile devices using operating systems such as Symbian, BlackBerry OS, Android
or the iPhone OS from Apple. All of them present different alternatives to choose from usually
represented by images (icons), and a range of visual widgets that support the actions necessary to
achieve the user's goals, displayed as a form that can be completed in whatever order the user sees
fit. Usually data are validated within the form, not allowing the action that it is designed for to run
until the data fitness requirements have been successfully attained. Each user action on a component
of this GUI raises an event that will be managed by an event handler that may execute a certain action
in response to the event.
But not only forms are capable of raising events. All of an application's objects are able to do so
including the application itself considered as one more object. Monitoring these events and setting the
appropriate callback functions to the events we have decided to listen to, we can automate operations
to a great extent. User interventions can be combined with actions that are triggered not by the user's
decision but by the way that decision has affected the environment in which the program runs.
Regarding alphanumeric information in the drawing, the topic of linking to external databases will not
be addressed in this book. It would merit a whole book. It is also an aspect that is closely linked to
highly specialized applications such as Geographic Information Systems (GIS), so AutoCAD in its
"vanilla" version would not be the ideal environment. Vertical applications such as AutoCAD MAP
are specifically aimed at this kind of work.
Chapter 21
Reacting to Events: Reactors
An event is an action that takes place in the environment in which we operate, in our case the
AutoCAD application or in a more general sense, the computer. Obviously, not all actions that may
occur while running an application deserve the same attention. The applications programmers are the
ones who decide what actions deserve to be taken into account. Visual LISP has access only to events
raised within AutoCAD.
A reactor is a special object that can be linked to another object in the drawing, even the drawing
editor itself. This reactor is responsible for notifying our program when the event we are interested in
is raised. If an entity linked to a reactor is modified, the notice of such modification can be used by
the program, for example, to modify in the same way (moving, scaling, etc.) other entities that have
been associated to the one that causes the event notification.
A reactor communicates with the application by calling a function that our program has associated
with that reactor. These types of functions are known as callbacks. These are normal functions
(although with certain limitations) that become callback functions when linked to a reactor’s events.
AutoCAD has increased the role of reactors within the application. Features like parametric design or
associative surfaces are implemented using the reactor technology.
Reactor types.
When planning the implementation of a reactor, the events we want our program to react to should be
carefully studied. There are five types of reactors in AutoCAD. Each reactor responds to one or more
events. They are grouped in the Database, Document, Editor, Linker and Object categories.
Each category includes only one type of reactor except for the Editor category, which in Release 14
Visual LISP was a single reactor. This reactor, which is kept for compatibility reasons has been
divided into 14 separate reactors.
The vlr-types function returns a list of all available reactors and the vlr-reactors function
returns a list of those reactors belonging to a certain category or categories that are defined in the
drawing. Reactor types are identified by keywords preceded by a colon. The functions used to create
Reactor objects have the same name without the colon. Evaluating (vlr-types) we obtain the
list:
(:VLR-Linker-Reactor
:VLR-Editor-Reactor :VLR-AcDb-Reactor
:VLR-DocManager-Reactor :VLR-Command-Reactor
:VLR-Lisp-Reactor :VLR-DXF-Reactor :VLR-DWG-Reactor
:VLR-Insert-Reactor :VLR-Wblock-Reactor
:VLR-SysVar-Reactor :VLR-DeepClone-Reactor
:VLR-XREF-Reactor :VLR-Undo-Reactor :VLR-Window-Reactor
:VLR-Toolbar-Reactor :VLR-Mouse-Reactor
:VLR-Miscellaneous-Reactor :VLR-Object-Reactor)
The category these reactors belong to and the events that trigger them are summarized in Table 21.1.
There are many different events to which a reactor can be linked. The user can perform many different
actions, and it is up to the developer the choice of the ones to listen for. Having decided on the action
one wants to respond to, the appropriate reactor can be linked to that event and the callback function
coded.
Database events.
Triggered when a change is made to the drawing database. The events that trigger this reactor are
shown in Table 21.2.
DOCUMENT Reactor
Reacts to opening, closing, creating or activating a drawing in a
::VLR-DocManager-Reactor
multi-document (MDI) environment.
EDITOR Reactor
LINKER Reactor
OBJECT Reactor
Document events.
When he status of a document changes the Document reactor is triggered. The events to which this
reactor responds are shown in Table 21.3.
A command will initiate or complete modifying elements in the document, changing its
::VLR-documentLockModeWillChange
lock status.
::VLR-documentLockModeChanged The document's lock mode of the document is being set or released.
The document has become the current document. It does not imply that it has been
::VLR-documentBecameCurrent
activated.
Another document (it can even be a different application) has become the active
::VLR-documentToBeDeactivated
document.
Editor events.
Events associated to the :vlr-Editor-Reactor are included in the other Editor reactors, so we
will not review it as a separate entity. Its use is not recommended, as it could disappear in future
releases. The Editor events are shown in Table 21.4.
:VLR-DeepClone-Reactor
A deep clone operation has two stages. First, each object and any other object it owns
::VLR-beginDeepCloneXlation are cloned. Second, any object ID references are translated to their cloned IDs. This
callback occurs between these two stages.
::VLR-dwgFileOpened The drawing file has been loaded in the AutoCAD window.
::VLR-databaseToBeDestroyed The contents of the drawing database are about to be deleted from memory.
:VLR-DXF-Reactor
::VLR-beginDxfIn The contents of a DXF file are about to be appended to the drawing database.
The document has become the current document. It does not imply that it has been
::VLR-documentBecameCurrent
activated.
:VLR-abortInsert Insertion has been aborted. The database may be in an unstable state
An insert operation has concluded. However, the inserted objects cannot be used until
:VLR-endInsert
the :vlr-commandEnded notification is received.
:VLR-Lisp-Reactor
:VLR-Miscellaneous-Reactor
:VLR-pickfirstModified The current document’s pickfirst selection set has been modified.
:VLR-Mouse-Reactor
:VLR-beginDoubleClick The user has double-clicked
:VLR-SysVar-Reactor
:VLR-Toolbar-Reactor
:VLR-Undo-Reactor
:VLR-undoSubcommandBegin The UNDO command is being executed with the BEGIN or GROUP options.
:VLR-undoSubcommandEnd The UNDO command is being executed with the END option.
:VLR-undoSubcommandMark The UNDO command is being executed with the MARK option.
:VLR-undoSubcommandBack The UNDO command is being executed with the BACK option.
:VLR-undoSubcommandNumber The UNDO command is being executed with the default NUMBER option.
:VLR-Wblock-Reactor
The command affects the database (objects have been copied but their identifiers but
:VLR-otherWblock
have not yet been resolved).
:VLR-Window-Reactor
:VLR-docFrameMovedOrResized An MDI child frame window (a document window) has been moved or resized.
:VLR-XREF-Reactor
:VLR-otherAttach The XREF’s objects have been added but their markers have not been resolved yet.
The ID of the object is being added to the symbols table of the drawing to which the
::VLR-comandeered
XREF is being attached .
::VLR-beginRestore An existing XREF will be resolved, typically when loading a drawing that includes it.
Linker events.
These are the means provided by AutoCAD to notify when an ObjectARX application is loaded or
unloaded. ObjectARX is AutoCAD programming environment for C++.
::VLR-rxAppLoaded A new ObjectARX program has been loaded. The program's initialization has ended.
::VLR-rxAppUnLoaded An ObjectARX program has been unloaded. The program has released the allocated memory.
Object events.
Object reactors inform the application when a particular object, which has previously been linked to
the reactor, has undergone a transformation, modifying, copying, or erasing it.
::VLR-subObjModified An object's sub-entity (polyline or mesh vertex, blockReference attribute) has been modified.
:VLR-Object-Reactor
21.3 Actions.
Each reactor responds only to those events for which it was designed. The actions implemented in
response to events that trigger the reactors are defined in the callback functions.
A callback function is a user function like any other, created by evaluating a defun expression. In
them, only Visual LISP ActiveX extensions should be used in dealing with the drawing and its
environment.
Except for those corresponding to those for object reactors which receive three, all other callback
functions receive two arguments:
In the case of object reactors, a first argument is added, the VLA-object representing the reactor’s
owner.
The name of the callback function is included as the second term in a dotted pair to create the reactor
object. The first term of this dotted pair will be the keyword which identifies the event to which the
callback function should react.
Implementing a reactor.
When developing an application which uses reactors one must have a clear idea about the behavior
we expect and the best way to accomplish it. A database or editor reactor would be necessary when
trying to keep track of the drawing globally. If we wish to address the behavior of specific objects, an
object reactor that will fire only when the event we are listening for takes place in relation to that
object would be the best choice.
The callback function.
Once what to do in response to which event has been decided, we must define the callback function.
The callback function is no different from a normal function that could be invoked from a program. It
is only necessary to follow some basic rules:
Don't make your action depend on the sequence of reactor notifications. The only certain thing in
an event sequence is that a Begin event will occur before the corresponding End event.
Don't rely on the sequence of actions occurring between notifications. They may not reflect the
true sequence taking place within the drawing database.
A dialog box should never be opened. But warnings such as those generated by the alert
function are admitted.
Never use the command/vl-cmdf interface within a callback function. In fact, we should
rather say that functions designed for user interaction should never be used. This rule would also
exclude get... functions, entsel, etc..
All operations on drawing entities should only be made using ActiveX methods, this also
excluding the entmod function.
An object should never be modified by a callback function triggered by itself. Only retrieving
information from it is admitted.
No callback function should trigger the same event to which it responds, as this would make the
system enter an infinite loop.
Different callback functions should not be associated with the same event. Whether the reactor is
already in use can be checked before associating a new callback function.
Check if a reactor exists before setting it. Doing so could activate several similar responses to
the same event.
Callback functions should be designed to accept two arguments (except for object reactors). The first
argument will contain a reference to the REACTOR object and the second a list of parameters (which
varies according to the event) that is provided by the system. The callback functions for object
reactors will receive as their first argument a reference to the object that triggers the action. The other
two arguments are the same as those required for other reactors.
There are two predefined callback functions that can be used for debugging purposes. The vl-
beep-reaction function causes a beep (or the system’s default sound). The vlr-trace-
reaction function prints the keyword that identifies the event and the arguments received in the
Trace window. Both functions can be included in callbacks to find out which events are raised in a
particular operation and when one of them is taking place .
Once we have programmed our callback function it must be associated to the event that shall trigger
it. There is a specific VLISP function for creating each type of reactor. This is the reason for such a
large number of reactor functions.
Our callback function can be tested before it is associated to a reactor. This can be done by
supplying the required number of arguments, which may well be nil. In the case of the object
reactor callback function shown below, it was tested during its development by supplying an
object selected on screen instead of the one that the reactor would supply when the event was
raised. This ensured that the basic operation was correct, which doesn't mean certain
adjustments were not necessary when associated with the reactor, but these were mostly
related to the event previously chosen than with the function's operation.
The first argument, which may be nil, is a LISP object (e.g., a string) that can serve to identify the
reactor using the function vlr-data. The second is a list of dotted pairs in each of which the first
term is a keyword that identifies the event and the second, the name of the callback function.
To find out what reactors are defined in the current drawing the vlr-reactors function can be
used. Without arguments, this function returns a list with all the existing reactors.
_$ (vlr-reactors)
((:VLR-Object-Reactor #<VLR-Object-Reactor>))
_$
The expression above tells us that there is one reactor object in the current active drawing. The first
term is the keyword identifying the type and it is followed by the pointers to each reactor of that type.
If a reactor type is specified as an optional argument the list will only contain reactors of that type.
_$ (vlr-reactors :vlr-editor-reactor)
nil
_$
The expression above checks if there are any Editor reactors in the drawing. Other functions provide
more information on the existing reactors.
The expression (vlr-type reactor-obj) Returns a keyword identifying the reactor’s type.
The AutoLISP application-specific data associated with a reactor are returned by:
(vlr-data reactor-obj)
To determine a reactor’s callback function and the event that triggers it vlr-reactions may be used.
_$ (vlr-reactors)
((:VLR-Object-Reactor #<VLR-Object-Reactor>))
_$ (setq reactor-obj (cadar (vlr-reactors)))
#<VLR-Object-Reactor>
_$ (vlr-reactions reactor-obj)
((:VLR-objectClosed . SCALE-TEXT))
_$
The expressions above show how to retrieve a reference to a reactor in order to investigate the
callback function (in this case SCALE-TEXT) and the event that triggers it, :VLR-
objectClosed. The callback function can be changed evaluating:
(vlr-reaction-set reactor-obj event function)
Inspecting Reactors.
If we select the name of the variable to which a reactor object is assigned and the Inspect button
(CTRL+SHIFT+I) is clicked, the Inspect dialog box will be displayed (see Figure 21.1) showing
the reactor’s data, including its owners, events and callback functions, if it is active, its associated
data, its range and the document it is defined in.
The optional argument reactor-type allows deleting only those of the specified type.
Persistence.
A reactor usually lasts no more than the work session in which it was created. They are not persistent.
For a reactor to be available the next time the drawing, is opened, the vlr-pers function should be
used. This function receives a reference to the reactor to be preserved as its argument. But making a
reactor persistent does not imply that its callback function will be available the next time the drawing
is opened. For this to be so, care must be taken to include in AutoCAD’s startup files (for example, in
acaddoc.lsp) the required code. An alternative is defining the reactor and callback functions in a
separate-namespace VLX. In that case the associated callback functions will be loaded automatically.
In any case the program’s file must be in the AutoCAD support files search paths.
To check whether a reactor is persistent the vlr-pers-p predicate is used. A list of the persistent
reactors in the drawing them can be obtained with vlr-pers-list. To remove the persistent
character from a reactor vlr-pers-release can be used.
The purpose of this application will be to enable a PaperSpace Viewport to recalculate the
ModelSpace text dimensions so that they maintain the same size even when the scale of that view
relative to paper space changes.
This capability has been present for a long time for non annotative dimension texts by
activating the Scale dimensions to layout option in the corresponding dimension style (or
setting the DIMSCALE system variable to 0). This automatic text scaling characteristic is not
available for normal texts, that to attain a somewhat similar functionality should be set to
annotative, meaning that the text will display only at its predefined scale level.
In our case, what we pretend is changing the existing ModelSpace text’s height each time the
viewport’s scale changes, so their size in the viewport remains constant. This, as shown in Figure
21.2, without affecting any text in PaperSpace or texts in frozen or locked Layers.
In order to do this we must have a predefined text size. This value may be stored in various ways: as
XDATA in the text entity, using the text style’s specified height, etc. Since this is just a reactors
example we don’t want to complicate with topics already studied in other parts of this book, we will
simply use in this program the value assigned to the TEXTSIZE system variable. Basing our
calculations on a single value has the disadvantage that all texts will have the same size, but we
consider it sufficient for our purpose.
Figure 21.2. Effect of the reactor when changing the Viewport's scale.
Listening only to the viewport’s events would have the disadvantage that changing its scale factor by
zooming in ModelSpace does not trigger events that indicate that its value has changed. For this
reason we must also implement besides a viewport object reactor, a command reactor that checks if
the viewport has been closed (which is done by the _PSPACE command) in order to check the scale
factor and trigger the text height modifying callback function. In any case at least two editor reactors
should be implemented to avoid that the object reactor triggers the callback function when any
command is active.
Application components.
A vital consideration for this application is the project’s structure, which must be distributed in two
different files. The code will be organized in this way with the intention that callback functions can be
loaded independently from the command used to link the reactors to the viewport. A folder structure
like the one shown in Figure 21.3 will be used for this project.
Inside the TextScale folder we create two files: scale-text.lsp and reactor-functions.lsp. The
scale-text.lsp file will include:
The reactor-functions.lsp file will include all the code necessary for the reactors to operate in a
drawing with reactors linked to a viewport and made persistent in a previous work session. This file
will include:
Viewport events.
When changing some viewport property a sequence of object events that include the following will be
raised:
Trying to read any property without having closed the object will produce an error. For this reason,
the event that must trigger the reaction we want is :vlr-objectClosed, indicating that the
modification process has concluded.
Command events.
In case the scale factor is modified by zooming in ModelSpace through the open viewport, a
command sequence will take place that includes:
It is on concluding the _PSPACE command that we will be able to examine the viewport’s scale in
order to modify the text size. In that case we must monitor the :vlrcommandEnded event to
implement a callback function in this case customized to run only if the concluded command is
_PSPACE. To prevent the object reactor’s callback function being triggered for any reason during a
command’s execution we will check the value of the CMDACTIVE system variable so the viewport
object reactor’s callback action will not be executed if its value is not zero.
Processing TEXT.
Before getting into specific issues relating to the operation of the reactors, we will examine the
procedure used for updating text heights. This is one of the functions to be included in the reactor-
functions.lsp file.
As for drawing entities modification, we have seen far more complex examples in previous chapters.
We will see that the difficulty in this example lies mainly in the reactors implementation .
Selecting texts.
The selection of texts will be done using the ssget function. A peculiarity of this selection is that
we will do it while a PaperSpace Layout is active, but the texts selected must be precisely those that
are not in PaperSpace, but in ModelSpace. There would be two filters that can be used to achieve
this. One would be using the value associated to DXF group code 67, which if greater than 0
indicates that the entity is in PaperSpace. But this group code is optional according to AutoCAD’s
documentation (although in our experience it usually appears in the entity list). Moreover, since
AutoCAD 2000 there can be several PaperSpace Layouts each containing different objects. So a safer
filter would use the Layout name, associated with group code 410 which, according to the
documentation, is never omitted. Although PaperSpace Layouts can be renamed, the ModelSpace
Layout is always called "Model".
The scale factor is set to (/ 1 CustomScale), CustomScale being the Viewport’s property
that contains its current scale. The text height value is calculated by multiplying the value of the
TEXTSIZE variable by this scale factor. If a different text size is desired, TEXTSIZE can be entered
in the command line to specify the new value.
Once this value is calculated, the selection set is traversed using the index i, which is initially set to
zero and is incremented by 1 for each iteration. Using the ename returned by ssname a reference to
the VLA-object is obtained. It is necessary to check whether this object can be modified, using the
vlax-write-enabled-p predicate. If the text object is in a locked2, frozen or turned Off Layer
it must not be modified, so the can-use? function (Listing 10.40) will be used to check this
condition. It acts as a predicate, returning T if the Layer object obtained by vla-get-Layer is not
locked, frozen or off. Neither will it be modified if it already has the desired size.
In case the program determines that the entity can and should be transformed, it will be done by
setting its Height property to the value assigned to the text-height variable.
(defun process-texts (obj-notif / sel-text
new-scale text-height i
txt-str)
(setq sel-text
(ssget
"X"
'((0 . "TEXT")(410 . "Model")))
new-scale
(vla-get-CustomScale obj-notif)
text-height
(* (getvar "textsize")
(/ 1 new-scale))
i 0)
(while (setq txt-str (ssname sel-text i))
(setq txt-str
(vlax-ename->vla-object txt-str))
(if
(and (vlax-write-enabled-p txt-str)
(can-use? (vla-get-Layer txt-str))
(/= (vla-get-height txt-str)
text-height))
(vla-put-Height txt-str text-height))
(setq i (1+ i))))
VPORT-CALLBACK function.
By the term modification we are referring to any property change. This fact, as we shall see,
introduces numerous difficulties in operating with reactors. As many commands can modify in an
unforeseen way the VIEWPORT object the rule would be, so to speak, to stay still while any
command is active. This is accomplished by checking the CMDACTIVE system variable’s value that
must be 0 if no command is currently active. This is the first thing checked by the vport-
callback function (Listing 21.2) within an and expression.
(defun vport-callback
(obj-notif obj-react lis-param)
(if (and (zerop (getvar "CMDACTIVE"))
(vlax-read-enabled-p obj-notif)
(= (getvar "ctab")
(car (vlr-data obj-react))))
(process-texts obj-notif)))
It is wise to anticipate other possible sources of error. For this reason the predicate vlax-read-
enabled-p is used to check that the notifying object’s properties can be read. This checked, then it
is verified that the current layout’s name (as read in the CTAB system variable) matches the custom
data saved in the reactor. Only if all these conditions are met the text modification process will be
launched.
COMMAND-END-CALLBACK function.
Checking that the event has been raised by concluding the PSPACE command
Checking that the selected viewport is not read-only.
The Viewport is obtained from the custom data associated with the reactor object (first argument
obj-react passed to the vport-callback function) retrieved using the vlr-data function.
These data form a list in which the second term is the viewport’s ename:
(vlax-ename->vla-object
(handent (cadr (vlr-data obj-react))))
The command name is obtained as the first term in the parameter list received by the function as the
param-lst argument.
(defun command-end-callback
(obj-react lis-param / vport-obj)
(setq
vport-obj (vlax-ename->vla-object
(handent (cadr (vlr-data obj-react)))))
(if (and (= (car lis-param) "PSPACE")
(vlax-read-enabled-p vport-obj))
(process-texts vport-obj)))
One of these events corresponds to the object reactor: (:VLR-Object-Reactor)and the other to
the command reactor (:VLR-Command-Reactor), so we have to create reactors for those two
events.
The vport-reactor function (Listing 21.4) creates the object reactor. It receives as argument the
Viewport VLA-object. It evaluates the VLR-Object-Reactor function that in addition to
defining the Viewport as the reactor’s owner, sets a custom data list that includes the name of the
current Layout and the viewport’s handle. Finally it establishes the link between the event and its
callback function. It can be seen that in the dotted pair list '((event. reactor)…) :VLR-
objectClosed is associated with vport-callback.
(defun vport-reactor (vport)
(vlr-pers
(vlr-object-reactor
(list vport)
(list (getvar "ctab")
(vla-get-Handle vport))
'((:VLR-objectClosed . vport-callback)))))
To monitor the commands status the command-reactor function (Listing 21.5) creates a :VLR-
Command-Reactor that listens to the :VLR-commandEnded event, associating it with the
command-end-callback function.
(defun command-reactor (vport)
(vlr-pers (vlr-command-reactor
(list (getvar "ctab")
(vla-get-handle vport))
'((:vlr-commandEnded
.
command-end-callback)))))
Both the creation of the object reactor and the command reactor object are included in calls to the
vlr-pers function, which has the effect of making them persistent, i.e., storing in the drawing these
reactors’ associations. This does not guarantee that the callback functions on which they depend will
be available the next time the drawing is opened. the way to achieve this will be explained later.
User interface for creating the reactors.
C:AUTO-SCALE function.
To create the reactors a user interface that prompts the user in order to avoid mistakes has been
designed. It is implemented as an AutoCAD command in the C:AUTO-SCALE function (Listing
21.6).
The first thing checked is whether a PaperSpace Layout is current. In case it is not a message warning
that this command only works only in PaperSpace is displayed.
If run from PaperSpace a loop will start prompting for an object selection, filtering it so that only a
VIEWPORT will be admitted. Assigning several reactors to the same object can be a source of
problems, so to avoid it we call the owner? function (Listing 21.7) that checks whether the object is
already associated with a reactor. A new reactor is associated only if the object owns no other
reactor. If all these tests are passed, the functions that enable the reactors, vport-reactor
(Listing 21.4) and command-reactor (Listing 21.5), will be called. In case they must be disabled
we can evaluate the expression (vlr-remove-all).
(defun C:AUTO-SCALE (/ viewport vport-obj)
(cond
((/= (getvar "ctab") "Model")
(prompt
"\nSelect viewport for text auto-scaling: ")
(while (not
(setq viewport
(ssget "_:S"
'((0 . "VIEWPORT"))))))
(setq
vport-obj (vlax-ename->vla-object
(ssname viewport 0)))
(if (not (owner? vport-obj))
(progn (vport-reactor vport-obj)
(command-reactor vport-obj))
(alert
"This viewport already owns reactors.")))
(t
(alert
"Command only valid \nin PaperSpace."))))
To verify whether an object owns reactors, the owner? function (Listing 21.7) must go from the
drawing’s environment to the object. We must do so because there is no function that given the object
will return the reactors it owns. But we can get the objects from the reactors. So we will start by
creating a list of the existing reactor objects. The vlr-reactors function returns that list and as
we are only interested in object reactors, we include the optional argument which indicates the
reactor type we want:
(vlr-reactors :VLR-Object-Reactor).
As this function returns a list of lists, each one with the keyword identifying the type followed by
pointers to each reactor of that type, and we are only interested in the reactor objects, we map cadr
on the list to get rid of the unwanted terms. To find the a reactor’s owners we can use the vlr-
owners function. Hence, the lambda expression we map on the list of pointers obtained, which
will return a list containing T for each reactor if the object being tested is found among its owners or
nil if it is not. Applying or to this new list we obtain the final result, T if the object has a reactor
and nil if it does not.
(defun owner? (obj / reactors)
(if (setq reactors
(mapcar 'cadr
(vlr-reactors
:VLR-Object-Reactor)))
(apply
'or
(mapcar
'(lambda (x)
(if (member obj (vlr-owners x))
t))
reactors))))
Once our files are in that folder we will create, if we had not done it before, a file named
acaddoc.lsp in the same folder. That file will contain instructions for loading those files that contain
functions that must be available for all drawings that are opened. Although we can include the code of
functions in this file, this is not advisable. It is far better to include calls to only two functions: load
and autoload.
The name can include the path, using as directory delimiters the slash (/) or two backslashes (\\).
The paths can be relative: the expression (load "./test/test1") loads the test1.lsp file
from a test subfolder in any of the directories included in the support files search paths.
The load function accepts an optional second argument that will be evaluated in case the load fails:
(load "./Test/testXXX" "ERROR: Could not find testXXX"))
which in case the file is not found will print in the command line the message
ERROR: Could not find testXXX.
Autoload takes two arguments, the first is the file name in the same conditions as in load and the
second is a list of the commands defined in that file that should be considered for automatic loading.
In our case, as it is a single command, the expression would be:
(autoload "scale-text" '("AUTO-SCALE"))
21.6 Summary.
The same effect demonstrated in this program instead of being automated through reactors, could
simply rely on the selection of a menu option. In fact, AutoCAD with the introduction of annotative
objects approaches this problem in a different way. Instead of changing the text height, it simply
associates the texts with different scale ranges, out of which the text will be invisible. It’s a different
approach to the subject, but whose implementation also depends on reactors.
On the other hand, as stated in the introduction to this chapter, with each new release more and more
commands are based on reactors functionality with no need for programming. Until recently,
establishing geometric relationships between objects so that changing the orientation of one would
automatically modify the orientation of the other was among the most widespread demonstrations of
reactors programming. This can be done today applying geometric constraints, without a single line of
code.
Finally, we will summarize the concepts expressed regarding reactors. The steps for implementing a
reactor would be:
1. Assess if the desired behavior merits working with reactors. Many times an action can be
automated to a great degree without leaving to the system the decision of what to do and when.
2. Once convinced of the need for reactors, we must decide the action and the event that should
trigger it.
3. Program the action using ActiveX procedures.
4. When programming the action, anticipate all the possible situations that may induce errors.
5. Create the reactor linking the event to the planned callback action.
6. Arm yourself with a great deal of patience while testing your program. We will find that an
event never comes alone. Any operation raises a series of events that may occur in an
unexpected order. It is likely that we'll find other events that should also be listened to, besides
the one that triggers the action we are interested in. In the case we have studied, its operation
would be unfortunate without taking into account the two command events.
As can be seen, working with reactors can be extremely complex. We would recommend their use
only to experienced developers. If you’re now just in your first steps, don’t worry about using
reactors. But it’s our intention to make a book worth keeping. If a topic like this now overwhelms
you, don’t worry. Keep on programming and take a look at it a year from now.
This function can be applied to all except the object reactor, the only difference being that it must
receive an additional argument.
(defun reaction-obj (obj reactor params)
(princ (vlr-data reactor))
(princ " | ")
(princ (vlr-current-reaction-name))
(princ " | ")
(princ params)
(princ "\n"))
To trace the sequence of database events the reactor shown in Listing 21.11 is used.
(vlr-acdb-reactor
"DataBase"
'((:vlr-objectAppended . reaction)
(:vlr-objectUnAppended . reaction)
(:vlr-objectReAppended . reaction)
(:vlr-objectOpenedForModify . reaction)
(:vlr-objectModified . reaction)
(:vlr-objectErased . reaction)
(:vlr-objectUnErased . reaction)))
To trace the sequence of Editor events the reactor shown in Listing 21.12 is used.
(vlr-editor-reactor
"Editor"
'((:vlr-beginClose . reaction)
(:vlr-beginDxfIn . reaction)
(:vlr-abortDxfIn . reaction)
(:vlr-dxfInComplete . reaction)
(:vlr-beginDxfOut . reaction)
(:vlr-abortDxfOut . reaction)
(:vlr-dxfOutComplete . reaction)
(:vlr-databaseToBeDestroyed . reaction)
(:vlr-unknownCommand . reaction)
(:vlr-commandWillStart . reaction)
(:vlr-commandCancelled . reaction)
(:vlr-commandEnded . reaction)
(:vlr-commandFailed . reaction)
(:vlr-lispWillStart . reaction)
(:vlr-lispEnded . reaction)
(:vlr-lispCancelled . reaction)
(:vlr-beginDwgOpen . reaction)
(:vlr-endDwgOpen . reaction)
(:vlr-dwgFileOpened . reaction)
(:vlr-beginSave . reaction)
(:vlr-sysVarWillChange . reaction)
(:vlr-sysVarChanged . reaction)))
For the Database and Editor reactors, evaluating the above expressions is all that is needed to enable
the reactors. As in the case of the object reactor it is necessary to designate the object to be
monitored, the function in Listing 21.13 is used to prompt for an interactive selection of the object.
(defun C:VLR-OBJECT ()
(setq obj
(vlax-ename->vla-object (car (entsel))))
(vlr-object-reactor
(list obj)
"Objeto"
'((:vlr-cancelled . reaction-obj)
(:vlr-copied . reaction-obj)
(:vlr-erased . reaction-obj)
(:vlr-unerased . reaction-obj)
(:vlr-goodbye . reaction-obj)
(:vlr-openedForModify . reaction-obj)
(:vlr-modified . reaction-obj)
(:vlr-subObjModified . reaction-obj)
(:vlr-modifyUndone . reaction-obj)
(:vlr-modifiedXData . reaction-obj)
(:vlr-unappended . reaction-obj)
(:vlr-reappended . reaction-obj)
(:vlr-objectClosed . reaction-obj))))
Exercises.
Exercise 1.
In our sample application the possibility that there could be Layers frozen only for the current
viewport was not taken into account. It wouldn’t be reasonable to include the texts in these Layers
among those which are modified. To check whether a Layer is frozen in the current viewport it is
necessary to read its DXF group code 331 that contain the enames of Layers frozen for that
viewport. This is an additional condition that can be added to the can-use? predicate.
Exercise 2.
Study the events raised by different operations performed in the drawing and their sequence, creating
the informative reactors shown in the previous section. In relation to the sample program, one of the
greatest difficulties we met in its development was the incredible series of events that occur when
switching between Layouts. This would be one of the cases we recommend examining using the
informative reactors.
1 These options correspond to the way we will handle the loading of the persistent reactor callback functions. In Chapter 26
the way of compiling an application as a VLX will be studied.
2 Being in a locked layer will be detected by vlax-write-enabled-p which will return nil in this case, but not the other conditions
(being frozen or off).
Chapter 22
DCL: The Graphical User Interface
Since Release 12 AutoCAD introduced the possibility of displaying dialog boxes from AutoLISP
programs. This way AutoLISP caught up with how current applications interact with their users. The
dialog boxes creation is a field where Visual LISP innovations have not been felt. The Dialog Control
Language or DCL in Visual LISP is still the same Release 12 AutoLISP DCL from 20 years ago
However, Visual LISP’s capacity for acting as an ActiveX client and server makes it possible to link
a Visual LISP application with dialog boxes programmed in other languages and compiled as DLL
files. An alternative would be using the OpenDCL application, Especially developed for its use with
AutoLISP, whose use will be explained in this book’s last chapter.
In addition to displaying the resulting dialog, this tool will report any errors found in the DCL code. If
the error is too severe, for example missing closing braces, the dialog’s name will not even appear in
the initial window1.
Active components.
Containers.
Decorative and informative components.
Text components.
Dialog box exit buttons and error message texts.
Restricted Tiles.
When the user selects an active component, for example a button, an event is raised that is notified by
the dialog box to the application that controls it. An active component can have an associated action,
with effects that may be visible to the user or have only an internal effect in the application. The
notified events are accompanied by a code specifying the reason that gave rise to them. The meaning
of this code depends on the component’s type. The active and selectable components are:
Containers.
Containers are used for grouping tiles into composite rows or columns (collectively known as
clusters). For layout purposes, a cluster is treated as a single tile. The rows or columns can be
enclosed in a box and have a label if desired.
The user cannot select a cluster, only the tiles (selectable and active) within the cluster. Clusters
cannot have actions assigned to them, with the exception of radio rows and columns. The following
components are used as containers:
These components do not raise events nor can be selected, its function is to display information or
visually complement the dialog box.
Images: image.
Texts: text.
Spacers: spacer, spacer0, spacer1.
Text components.
To compose messages with a certain complexity in the dialog box, where fixed text elements can be
combined with others that can assume values set by the program, independent text components can be
combined with concatenation components in continuous lines composed to form paragraphs.
They include the standard button subassemblies used for exiting dialog boxes. Their use ensures a
consistent appearance between your dialogs and AutoCAD’s. The text in these buttons can be
customized using the retirement_button prototype.
Restricted Tiles.
The cluster or tile components must not be used in DCL files. Neither can the basic retirement button
types (cancel_button, help_button, info_button and ok_button) be used unless the
standard retirement button subassemblies are redefined.
This Chapter will demonstrate how to work with DCL and AutoLISP building a dialog box,step by
step, in the Visual LISP IDE.
edit_box: Displays a field in which a single line of text can be entered and edited. It may
include a text label on its left.
slider: provides a means to change a numeric value by dragging a cursor along a path.
radio_button: Always forms part of a cluster (radio column or row). Within a cluster only
one of the radio buttons can be checked. When this tile is used, checking one in the cluster
unchecks all the others. For those cases where multiple selections are allowed the toggle tile
must be used.
button (retirement buttons): will be used to trigger the dialog's action or to cancel it. The
buttons used in this tutorial are predefined in the ok_cancel subassembly.
For making the dialog box’s information more comprehensible a series of decorative and informative
components are included:
image: A tile used for displaying a vector images either drawn at run time or saved as .SLD
files.
errtile: Displays a text string for user information.
These tiles will be used to select the options or parameters required for creating a 3D model. These
will be structured in a series of clusters of the following types:
The dialog box we will design is aimed at entering the data required for a parametric 3D model. For
this we have to analyze the information required by the command we will use.
It is our purpose to create a command that displays a dialog box in which the user can enter these
data. It will also include the profile’s image as an aid in identifying these data. Once the data are
supplied, the command will create the model.
Figure 22.1. Parametric profile.
The dialog box will also allow selecting from among four predefined forms. This selection will be
done through a row of radio buttons in which in addition to the Normal option, in which all
parameters > 0, the following options will be offered:
The dialog will check the input data to prevent geometric inconsistencies such as FilletRadius
being greater than DimX or DimY being less than 2 x FilletRadius.
Other possible choices that can be made in the dialog refer to creating a solid or an associative
procedural surface model and specifying the profile’s rotation angle.
We can conceive a DCL dialog box as a large box in which other smaller boxes are placed, which in
turn may contain other boxes and so on. These "boxes" fix the place where the dialog components
will appear.
All these boxes and their contents fit in a big box that is the dialog itself. This box is automatically
stretched to make room for whatever we put inside. This box is also a component, the dialog
component. This component is identified by the keyword dialog and must be preceded by a name
that will identify it within the application. A colon and the keyword dialog that identifies the
component follow the name. Continuing with an opening brace "{" which marks the beginning of this
container and which must correspond to a closing brace, just like parentheses in LISP.
This box cannot be empty. You must have at least a button to accept or cancel the dialog. This is
because although a dialog box lacking both components may be displayed, there would be no way to
close it. The Visual LISP IDE warns of this error and does not display the dialog.
There are predefined components that are used in these cases. In the code shown in Figure 22.2
includes the predefined subassembly ok_cancel that displays the default OK and Cancel buttons.
Note that all the expressions included between the opening and closing braces end in a semicolon.
This does not apply to comments, marked by two bars. As we can see in Figure 22.3 the Dialog Box’s
size is just the necessary so that the two buttons in the ok_cancel subassembly fit in. This is one of
DCL’s characteristics. There is no need to define the dialog box’s dimensions. The container will
stretch automatically as we add new components, or tiles as they are known in DCL programming.
Figure 22.3. Preview of the dialog generated from the code in Figure 22.2.
The label and key attributes define a tile’s characteristics. In this case the dialog’s title is defined
by the label attribute and key identifies it within the program. All DCL tiles support a number of
attributes that can vary from one another.
Some attributes such as height and width, are common to all of them. Specifying the attributes is
optional. Many of them have default values that are used if not specified. Other attributes may only be
used with certain tiles, for example, an image’s background color. Trying to use these attributes in
tiles that do not support them could cause an error in AutoCAD, however they are usually are
ignored. The key attribute should always be specified so that the program can act on the tile it
identifies.
Integers: Numeric values (both for integers and for real) representing distances such as a tile's
width or height are expressed in character-width or character-height units3.
Real Numbers: A real number less than zero must necessarily include the zero to the left of the
decimal point. For example 0.5 and not .5.
Strings: A string must always be delimited by double quotes. Attribute values are case sensitive.
If quotes must be included within a string, they must always be preceded by the backslash
control character (\").
Reserved words: A reserved word is an identifier made up of alphanumeric characters,
beginning with a letter.. As examples we have true and false. Reserved words are also case
sensitive. True is not the same as true.
Attribute names are also case sensitive. The name Label name cannot be used in assigning a tile’s
label. Applications that manage them always receive attribute values as strings. If the values are
numeric it will be necessary to convert the strings to the appropriate numeric type before using them
in calculations.
The dialog’s tiles may be structured in columns or rows. The tiles included in columns are placed one
above the other and those in the rows, one beside the other. A row may contain columns, columns may
contain rows, or whatever row and column combination we can need. Column is the dialog’s default
tile arrangement.
The dialog we will use has the distribution shown in Figure 22.4. Inside the dialog container we
will place a row subassembly followed by a boxed_radio_row subassembly.
The row will hold two column subassemblies which will contain the following tiles:
Left column:
Right column:
To emphasize the dialog’s structure Boxed Columns and Rows are used. Containers having the
boxed prefix display a border that delimits dialog areas including related controls and can have a
text assigned to their label attribute identifying their contents.
Figure 22.4. Distribution of the dialog box components.
The structure shown in Figure 22.4 will be attained with the code in Listing 22.1, which defines the
tile subassemblies and their arrangement. Notice the code’s formatting, indenting the tiles in relation
to their containing subassemblies and aligning the closing brace vertically with the tile’s initial
position. To indent the lines it will be necessary to use the TAB key, since the VLISP Editor can’t
format DCL code automatically. Following this indentation style, with each component indented in
relation to its container and all its attributes as a column under it, aids in understanding the code.
Something the Editor can do is to check matching opening and closing braces. To highlight everything
between opening and closing braces we can double-click next to one of them. Strings delimited by
double quotes may also be checked this way.
The attributes defined for all of this dialog’s tiles are key, label and value. The image tile will
also have its size specified. The dialog’s total width and height will result from the combination of
the image size with the size of the predefined tiles, specially the errtile and ok_cancel
widths. If the edit boxes are too small for our needs, especially if a long label is necessary, they
could also be assigned a width attribute. This attribute defines its minimum width so it may be
increased to match other dialog components. There is also a fixed_width attribute, but its use is
not advised except in very special cases. The value attribute sets a default value for the edit boxes
that can be changed by the user. If it is not set here, it may be assigned by the program when
initializing the dialog.
parametric : dialog { // Begin dialog
label = "Parametric model";
key = "title";
: row { // Upper row
: column { // Begin left column
: radio_row {
: radio_button { // Solid model
label = "Solid";
key = "sol";
value = "1";
}
: radio_button { // Surface model
label = "Surface";
key = "sur";
value = "0";
}
}
: boxed_column { // Parameter edition
label = "Parameters";
: edit_box {
edit_width = 15;
label = "DimX";
key = "dx";
value = "100.00";
}
: edit_box {
edit_width = 15;
label = "DimY";
key = "dy";
value = "100.00";
}
: edit_box {
edit_width = 15;
label = "FilletRadius";
key = "ra";
value = "25.00";
}
: edit_box {
edit_width = 15;
label = "CenterRadius";
key = "rc";
value = "50.00";
}
errtile; // Warning messages
}
ok_cancel; // Ok and Cancel buttons
}// End left column
: column { // Begin right column
: image { // Image
key = "img";
width = 35;
aspect_ratio = 1;
color = graphics_background ;
}
: boxed_row {
label = "Revolution angle: ";
width = 35;
:edit_box { // Angle value
key = "inf" ;
value = "360";
edit_width = 3;
}
: slider { // Slider bar
key = "ang";
width = 27;
max_value = 360;
min_value = 1;
value = 360;
small_increment = 1;
big_increment = 10;
}
}
} // End right column
} // End upper row
: boxed_radio_row { // Begin boxed radio row
label = "Predefined forms";
: radio_button {
label = "Normal";
key = "nor";
value = "1";
}
: radio_button {
label = "Sphere";
key = "sph";
value = "0";
}
: radio_button {
label = "Bar";
key = "bar";
value = "0";
}
: radio_button {
label = "Tube";
key = "tub";
value = "0";
}
} // End boxed radio row
} // End dialog
To verify the design we can use the tool available in the VLISP IDE.Figure 22.5 shows the dialog’s
preview. The image cannot yet be seen, as it must be loaded at run-time. The values shown in the edit
boxes are the defaults set using the value attribute.
Figure 22.5. Dialog box preview.
The space reserved for the image is the same color as AutoCAD’s graphic screen as the image tile’s
color attribute has been assigned the reserved word graphics_background. The size for the
image tile can be defined in two ways: by assigning values to its width and height attributes, or
by assigning a value to width and to aspect_ratio. In this case we have assigned 1 as ratio to
obtain the same width and height (i.e., a square).
The image displayed in the dialog must have been saved as a slide (SLD) file. To save a drawing as
a slide the AutoCAD MSLIDE command is used. The to create an image of the desired proportions
follow these steps:
1. Load the dialog in memory. This step includes checking that the system locates the DCL file and
that it contains the code for the dialog we want to show. In case an error is found it should be
notified to the user, exiting the program.
2. Set the initial values for the different tiles in case they have not been preset using the value
attributes in the DCL file.
3. Link the tiles to their callback actions. Each active tile raises an event when the user interacts
with it. This linkage, which is set by evaluating an action_tile expression, will define the
function that is triggered following the event raised by each tile.
4. Display the dialog. Even if loaded into memory, the dialog will not be displayed until the
start_dialog expression is evaluated . This function remains active for as long as the
dialog is visible. It ends only when selecting a component that has done_dialog as its
callback action. When concluded, start_dialog will return the numeric value that was
passed to done_dialog which is used to identify the tile that caused the action. This numeric
value can be assigned to a local variable that can be used as an argument when invoking the
function that processes the data collected from the dialog, in our case, creating the 3D model.
5. Close the dialog. The actions linked to certain tiles may call the done_dialog function. This
function is usually called by the OK and Cancel buttons. It indicates that the dialog must be
closed and the numeric argument it receives will determine the dialog's effect. Typically OK
brings forth the dialog's expected consequences and Cancel closes it without any consequence.
1. Establishing, if it did not already exist, the global variable *Position* in which the dialog's
position when it was last used will be saved. The initial value assigned to this variable is the
list '(-1 -1) which indicates the center of the screen.
2. Loading the DCL file: the load_dialog function is evaluated with the file name as argument
and the value returned is assigned to the local variable dcl_id. This value is checked, as if it
were -1 it would mean that the dialog could not be loaded, and in that case a warning message
should be displayed and the program ended.
3. If the DCL file was loaded successfully the new_dialog function is evaluated to display the
dialog. The arguments new_dialog receives are:
a. The dialog's name,
b. The DCL file identifier returned by load_dialog,
c. An AutoLISP expression that will be used as the dialog's default action. If we don't wish to
assign any, it can be replaced with an empty string (""),
d. The dialog's position, specified in the *Position* global variable.
4. The new_dialog function returns nil if the dialog could not be displayed. This will be
checked to display an error message.
5. If no errors occurred, the display-dialog function returns T, value that can be used as the
condition for continuing with the program.
All these operations should be carried out for any dialog. That is why we group them as a separate
function that may be included in any program using DCL dialogs.
The image.
First of all we will set the image’s content. As this always follows the same steps, we will define
another library function that we will name display-image (Listing 22.3). It receives the
arguments key, for the tile’s key, and image which is the SLD file name without its extension4.
The image’s loading process begins with a call to start_image using the attribute key as
argument. Anticipating that, as in the case of this program, the image will change, an initial call is
made to fill_image which will fill the image tile with the AutoCAD drawing area’s background
color using -2 as its last argument.
Then slide_image is evaluated, receiving as its two initial arguments are the X and Y offsets
from the tile’s upper left corner and the next two are the image’s width and height. So that this
function can be used in all cases, rather using the values set in the DCL file, we will use the
dimx_tile and dimy_tile functions to retrieve these values. And as the final argument, the
SLD file name. To complete the image’s creation process, end_image is evaluated.
(defun display-image (key image)
(start_image key)
(fill_image
0
0
(dimx_tile "img ")
(dimy_tile "img ")
-2)
(slide_image
0
0
(dimx_tile key)
(dimy_tile key)
image)
(end_image))
Table 22.1. Functions not allowed while dialog boxes are displayed.
AutoCAD queries and commands.
command vl-cmdf osnap
entupd
These are functions affecting the display or that require user’s data input. Should user’s data entry or
object selection be necessary it would be necessary to hide the dialog box temporarily with a call to
done_dialog, restarting the dialog once the desired action has been performed. If any of these
restricted functions is called while a dialog box is active, it is canceled and the message "AutoCAD
rejected function" is displayed. To check if a dialog is active the CMDACTIVE system variable can
be examined, where a bit value of 8 indicates an active dialog.
The callback functions can access certain variables that identify the tile that triggered the action and
give information about its state when the event was raised. The names of these read-only variables
are reserved words, and their value is only accessible from the callback functions. They are
described in Table 22.2.
The string currently assigned to the value attribute. It applies to all actions. For a list box, if nothing is selected nil
$value
is returned.
A code that specifies why the event was triggered. It is used with edit_box, list_box,
$reason
image_button and slider tiles. It indicates the reason that causes the action.
Reasons may be crucial in choosing the action callback functions should perform. The callback
reason codes are described in Table 22.3.
The user has exited an edit box but a final selection has not been made. This event usually causes the verification
2
of the input values coherence.
3 In sliders, the user has changed its value by dragging the indicator but has not made a final selection.
In list boxes or image buttons This callback reason always follows a code 1. It usually means “commit to the
4
previous selection”. In list boxes it means a double-click.
Edit boxes: used to define the values of the parameters that define the profile's dimensions. Their
associated callback function is param-edit, which receives as arguments the key ($key), the
value ($value) and the reason ($reason).
Radio buttons: those used for selecting predefined profile shapes have form-ops as their
associated callback function. This function receives the key ($key) and the value ($value) as
arguments.
Slider and associated edit box: both are associated with the sel-rotation function which
receives the key ($key), the value ($value) and the reason ($reason).
Edit boxes.
We will use the edit boxes management as an example of how to run a dialog box when a rather
complex interaction exists between different values and possible inconsistencies must constantly be
verified. Other tiles will interact with edit boxes, but with a lower degree of complexity.
PARAM-EDIT Function.
The param-edit function (Listing 22.5) is associated as the callback action to every edit box in
the boxed column labeled "Parameters". It receives as its first argument the triggering tile’s key
($key), its current value ($value) and the triggering action’s reason code ($reason). It has two
purposes:
Checking that the value entered is numeric and non-negative, and displaying the number in the
edit box with two decimal places. It does this by invoking the auxiliary function param-
format (Listing 22.4).
Checking that parameter values are consistent with the selected shape option. To do this one of
two functions is used, test-normal (Listing 22.8) or test-other (Listing 22.6),
according to the "Normal" radio button or any other being selected.
Care must be exercised with the DIMZIN system variable settings which would suppress trailing
zeros in decimal dimensions. To avoid this DIMZIN must be set to 15.
(defun param-format (key value /)
(set_tile
key
(rtos (abs (atof value)) 2 2)))
Listing 22.4. Function that checks and formats edit box values.
Because of the profile’s geometry, DimX values smaller than FilletRadius or DimY values less
than twice FilletRadius cannot be accepted. We should also watch for zero parameter values in
certain options that don’t admit this.
To simplify the code for such verifications two test functions have been defined, one named test-
normal used with the Normal option that is the most complex one, and for the rest of predefined
shapes another function named test-other.
These functions will be called from the param-edit function according to the selected form type,
which is determined by reading the Predefined forms radio button values. The code is executed
whenever the $reason argument has the values 1 or 2 (see Table 22.3). The param-edit
function takes as arguments the key ($key) of the edit box that raised the event, its current value
($value) and the event’s reason ($reason).
(defun param-edit (key value reason / form)
(param-format key value)
(if (or (= reason 1) (= reason 2))
(cond ((= (get_tile "nor") "1") ;Normal
(test-normal key))
((= (get_tile "nor") "0") ;Other
(setq form (form-sel))
(test-other form)))))
Listing 22.5. Parameter edit boxes callback function.
The parameter values verification for the Sphere, Bar or Tube predefined form options are limited
to checking that some parameter values are not zero. These verifications are grouped in a single
function, test-other (Listing 22.6) that will enable or disable the OK button printing informative
messages in the errtile component. The OK button is disabled by the expression (mode_tile
"accept" 1). To enable the button, mode_tile should be re-evaluated with the argument 0
instead of 1.
T he Normal case bears a greater complexity, so to simplify the code three distinct verification
functions are defined (see Listing 22.7):
If the test fails each of these functions will assign a string stating the problem to the variable msg and
will set nil as the value of one of the variables ok1, ok2 or ok3.
These three functions are invoked from the test-normal function (Listing 22.8). This function
converts to real numbers the values of DimX, DimY, FilletRadius and CenterRadius,
assigning them to the variables dim-x, dim-y, rad-f and rad-c.
(defun test-other
(form / rad-f dim-x dim-y rad-c)
(cond
((= form "sph")
(setq rad-f (atof (get_tile "ra")))
(if (<= rad-f 0)
(setq ok nil)
(progn
(setq ok t)
(set_tile
"dx" (rtos rad-f 2 2))
(set_tile
"dy" (rtos (* rad-f 2) 2 2)))))
((= form "bar")
(setq dim-x (atof (get_tile "dx"))
dim-y (atof (get_tile "dy")))
(if (or (<= dim-x 0) (<= dim-y 0))
(setq ok nil)
(setq ok t)))
((= form "tub")
(setq dim-x (atof (get_tile "dx"))
dim-y (atof (get_tile "dy"))
rad-c (atof (get_tile "rc")))
(if (or (<= dim-x 0)
(<= dim-y 0)
(<= rad-c 0))
(setq ok nil)
(setq ok t))))
(if ok
(progn (mode_tile "accept" 0)
(set_tile "error" ""))
(progn (mode_tile "accept" 1)
(set_tile
"error"
"Parameters must be > 0"))))
(defun test-normal
(key / dim-x dim-y rad-f
msg ok1 ok2 ok3)
(setq dim-x (atof (get_tile "dx"))
dim-y (atof (get_tile "dy"))
rad-f (atof (get_tile "ra")))
(test-1 rad-f)
(test-2 rad-f dim-y)
(test-3 rad-f dim-x)
(cond ((= key "ra")
(test-1 rad-f))
((= key "dx")
(test-3 rad-f dim-x))
((= key "dy")
(test-2 rad-f dim-y)))
(if (and ok1 ok2 ok3)
(progn (mode_tile "accept" 0)
(set_tile "error" ""))
(progn (mode_tile "accept" 1)
(set_tile "error" msg))))
Once the values of these variables are set, the three test functions are applied in succession, assigning
values of T or nil to ok1, ok2 and ok3, that will allow checking if any inconsistency between the
different parameters exists. But the error message that is assigned to the msg variable is the one
generated by test-3. In order that the error message actually matches the tile that is checked, the
test function that examines that specific parameter should be run the last. This is achieved by a cond
expression that according to the key received will run again one of the four test functions so that the
appropriate error message, if any, is the one found for that specific parameter.
Radio buttons.
The dialog box includes two radio button rows. The top left one is used to define whether the model
created will be a Solid or a Surface. These buttons don’t have a callback function. Their value is
checked when clicking OK to define the model’s object type.
The radio buttons at the bottom of the dialog are assigned the form-ops (Listing 22.9) callback
function. This function takes as arguments the radio_button tile’s key ($key) and its value
($value), and its action will be enabling or disabling some of the edit boxes and changing certain
values according to the chosen form. For example, FilletRadius must always be zero and cannot
be modified for the Bar and Tube options.
Changing the Predefined forms option will also have the effect of changing the image displayed in
the image tile. This will be done by calling the same display-image function (Listing 22.3)
used when initializing the dialog.
In addition to making these changes in the dialog box, the consistency of the current parameters must
be checked again, using the test functions defined for the edit boxes. To determine the changes to be
made, key and value are used as conditions in a cond expression .
(defun form-ops
(key value / dim-x dim-y rad-f rad-c)
(cond
((and (= key "nor") (= value "1")) ;NORMAL
(setq dim-x (atof (get_tile "dx"))
dim-y (atof (get_tile "dy"))
rad-f (atof (get_tile "ra")))
(mode_tile "dx" 0)
(mode_tile "dy" 0)
(mode_tile "ra" 0)
(mode_tile "rc" 0)
(if (= rad-f 0)
(set_tile "ra" (rtos (/ dim-x 2) 2 2)))
(if (< dim-y (* rad-f 2))
(set_tile "dy" (rtos (* rad-f 2) 2 2)))
(display-image "img" "./img/nor")
(test-normal "dx"))
((and (= key "sph") (= value "1")) ;SPHERE
(setq dim-x (atof (get_tile "dx"))
rad-f (atof (get_tile "ra")))
(if (= rad-f 0)
(progn (set_tile "ra" (get_tile "dx"))
(setq rad-f dim-x))
(set_tile "dx" (get_tile "ra")))
(set_tile "dy" (rtos (* rad-f 2) 2 2))
(set_tile "rc" "0.00")
(mode_tile "dx" 1)
(mode_tile "dy" 1)
(mode_tile "ra" 0)
(mode_tile "rc" 1)
(display-image "img" "./img/sph")
(test-other key))
((and (= key "bar") (= value "1")) ;BAR
(set_tile "ra" "0.00")
(set_tile "rc" "0.00")
(mode_tile "dx" 0)
(mode_tile "dy" 0)
(mode_tile "ra" 1)
(mode_tile "rc" 1)
(display-image "img" "./img/bar")
(test-other key))
((and (= key "tub") (= value "1")) ;TUBE
(setq rad-c (atof (get_tile "rc")))
(set_tile "ra" "0.00")
(if (= rad-c 0)
(set_tile "rc" (get_tile "dx")))
(mode_tile "dx" 0)
(mode_tile "dy" 0)
(mode_tile "ra" 1)
(mode_tile "rc" 0)
(display-image "img" "./img/tub")
(test-other key))))
To inform the user about the slider’s selected angular value it is usually accompanied by a
synchronized edit box where the value is updated whenever the slider is moved. This edit box must
also allow modifications so that when a numeric value is entered the slider will be updated
accordingly. Both the slider and the associated edit box trigger the same callback function, sel-
rotation (Listing 22.11). This function receives as arguments the triggering tile’s key ($key), its
value ($value) and its reason ($reason). If the action is triggered by the slider, the effect is
writing in the edit box the slider’s current value. If the action is triggered by the edit_box, the
effect is to update the slider’s position to the new value. A common mistake would be to enter a non-
numeric value. This is verified should the action be triggered by the edit box applying the numberp
predicate to the value returned by (read value).
_$ (type (read "abcd"))
SYM
_$ (numberp (read "abcd"))
nil
_$ (type (read "1234"))
INT
_$ (numberp (read "1234"))
T
_$
If value contains a text that does not represent a number the value returned by read would be a
symbol and not a number, so numberp will return nil. In that case, instead of changing the cursor,
the slider’s current value obtained by (get_tile "ang") is rewritten in the edit box.
(defun form-sel (/)
(cond ((= (get_tile "nor") "1") "nor")
((= (get_tile "sph") "1") "sph")
((= (get_tile "bar") "1") "bar")
((= (get_tile "tub") "1") "tub")))
To verify that the parameters do not conflict, the selected Predefined forms radio_button tile
must be known. Since this operation must be done in more than one occasion, we include an auxiliary
function named form-sel (Listing 22.10) which returns the key for the selected radio_button.
(defun sel-rotation
(key value reason / form)
(cond
((= key "ang")
(if (or (= reason 3)
(= reason 2)
(= reason 1))
(set_tile "inf" value)))
((= key "inf")
(if (or (= reason 2) (= reason 1))
(if (numberp (read value))
(set_tile "ang" value)
(set_tile "inf"
(get_tile "ang"))))))
(setq form (form-sel))
(if (= form "nor")
(test-normal "ra")
(test-other form)))
The following expression assigns a callback action to the edit box whose key is "dx":
(action_tile "dx" "(param-edit $key $value $reason)")
All of the callback action assignments are grouped into the assign-actions function (Listing
22.12). This function includes the assignment of the callback actions described below to the
ok_cancel predefined buttons.
These are the variables that would be used as arguments in the modeling function executed when the
dialog is unloaded. The done_dialog function is then evaluated with the numeric argument 1 and
its returned value is assigned to the global variable *Position*.
display-dialog (Listing 22.2), which looks for the "parametric" dialog in the
"./dcl/parametric.dcl" file. This is the condition to continue running the rest of the
function. If it is not found a message box is displayed warning the required code is not available.
display-image (Listing 22.3) that searches for the image it receives as argument and draws
it on the image tile.
assign-actions (Listing 22.12) that assigns dialog tiles their callback actions.
start_dialog that displays the dialog box ready for user input. The value returned by
start_dialog is assigned to the action variable. If this value is 1 the function that creates
the 3D model is invoked.
unload_dialog as the last expression, whose effect is unloading the DCL file from memory.
(defun param-dialog (/ action)
(setvar "DIMZIN" 1)
(if (display-dialog
"parametric"
"./dcl/parametric.dcl")
(progn (display-image
"img" "./img/nor")
(assign-actions)
(setq action (start_dialog))
(if (= action 1)
(param-dwg
obj-type dim-x dim-y
rad-f rad-c ang-r))
(unload_dialog dcl_id))))
To determine the action to take the value returned by start_dialog, assigned to the variable
action, is examined. This test is performed within an if, although if there were more values, it
could be done using a cond expression. In case action is 1, the paramdwg function is called,
using the variables assigned by the OK button’s callback.
T he param-dwg function (Listing 22.17) begins by calculating the 2D point values of for the
polyline’s vertices. Once the points are calculated the rev-profile function (Listing 22.16) is
used to draw the profile. For the arc segments the auxiliary function bulge calculates the bulge
(curvature) magnitude that must be associated with that segment’s group code 42.
The bulge function receives as argument the arc’s subtended angle in radians. For profile shape
options other than the normal one there will be superposed vertices, but the LWPOLYLINE entity will
admit this. When the profile is created, its ename is assigned to the profile variable.
(defun rev-profile
(max-x med-x max-y min-y rad-c / pts)
(setq
pts (list
(list rad-c min-y)
(list med-x min-y)
(list max-x (+ min-y rad-f))
(list max-x (- max-y rad-f))
(list med-x max-y)
(list rad-c max-y)))
(if
(entmake
(list ‘(0 . "LWPOLYLINE")
‘(100 . "AcDbEntity")
‘(100 . "AcDbPolyline")
(cons 90 (length pts))
‘(70 . 1)
(cons 10 (nth 0 pts))
(cons 10 (nth 1 pts))
(cons 42 (bulge (/ pi 2)))
(cons 10 (nth 2 pts))
(cons 10 (nth 3 pts))
(cons 42 (bulge (/ pi 2)))
(cons 10 (nth 4 pts))
(cons 10 (nth 5 pts))
‘(210 0.0 0.0 1.0)))
(setq profile (entlast))))
(setq profile (entlast))))
The model is created by calling the _REVOLVE command to sweep the profile around the Y
coordinate axis. After creating the 3D object a southwest isometric view will be set and the visual
style we’ve explained in previous chapters will be set as the current visual style.
(defun param-dwg (mode dim-x dim-y rad-f rad-c
ang-r / max-y min-y max-x med-x
pts profile)
(setq max-y (/ dim-y 2)
min-y (- max-y)
max-x (+ rad-c dim-x)
med-x (- max-x rad-f))
(rev-profile max-x med-x max-y min-y rad-c)
;; Profiles
(if profile
(progn
(if (= mode "_SU") ; Model
(progn
(setvar
"SURFACEMODELINGMODE" 0)
(setvar
"SURFACEASSOCIATIVITY" 1)
(vl-cmdf
"_AutoConstrain"
profile "")))
(vl-cmdf
"_REVOLVE" "_MOde" mode
profile "" "_Y" ang-r)
(ax-SWt))))
22.9 Summary.
We have seen how to implement a DCL dialog box for a 3D modeling application using the functions
available in AutoLISP’s Programmable Dialog Box (PDB) language.
Although we have not explored more than a few tiles, how to design these dialogs and make them
operational through LISP programming has been exposed in its fundamental aspects.
We must also note that these dialog boxes can be packed with the Visual LISP code that make them
work as Visual LISP compiled executable (VLX) files. This capability simplifies the distribution of
applications because it is not necessary to install the DCL file independently.
More elaborated dialogs may be created by other means like the OpenDCL application to which we
devote this book’s final chapter. In both cases, as they are created by procedures specifically
designed for AutoLISP/Visual LISP, we are able to create standalone applications that include in a
single file both the application’s code and the user interface’s definition, thing that is not possible
using dialogs created by other means.
Exercises.
Exercise 1.
Compile this application as a Visual LISP Executable, including the DCL dialog definition file.
However, the SLD files will have to be installed in the system where the application is to be run. As
a way to facilitate this we recommend using the SLIDELIB program.
Exercise 2.
Surely the reader will have a program where a drawing is made using a series of dimensions, angles,
etc. Using this tutorial’s dialog box and its callback functions as a model, he can give these old
programs a renewed aspect.
1 But this will not work in recent OS like Windows Vista or 7. A message like ; warning: cannot create tmp file for DCL:
"C:/Program Files/Autodesk/AutoCAD 2014/$vld$.dcl" is displayed in the Visual LISP Console. A workaround in case these
options must be used is to include the current user in the Administrators group with all privileges and start AutoCAD as
administrator.
2 This file, as the others included in the application’s folders should not be modified, for this could compromise the system.
3 Character width usually means the width of the letter M. The capital X is usually used for measuring the height. The
characters used in a DCL dialog box are drawn using a proportional typeface so the width is different for each character. For
this reason, the actual length of a text as it appears in the dialog box is usually less than the width we can expect from the value
assigned to the width attribute. In some cases whitespace can be added to compensate for this especially in edit boxes where the
width of the text label affects the space available for the box itself.
4 AutoCAD images are contained in a SLB (for Slide Library) file. These files can be created with the utility SLIDELIB.EXE that
is supplied with AutoCAD and can be found in the program folder.
5 In fact, for units expressed in the metric system any value less than 4 turns off zero suppression as DIMZIN less than 4 only
applies to feet and inches. A value of 1 includes zero values also in feet and inches values.
Chapter 23
Associating information to Graphic
Objects
Although this book focuses on the management of graphical entities, we will refer briefly to an aspect
of unquestionable relevance as is the association of alphanumeric information to drawing objects.
Computer aided design applications have evolved into sophisticated systems providing intelligent
solutions in a number of highly specialized fields. A special mention is deserved by those related to
the automated generation of thematic maps, facilities management and georeferenced databases,
usually identified by the well-known acronyms AM (Automated Mapping) FM (Facilities
Management) and GIS (Geographic Information Systems). The key to all these developments is the
possibility of linking additional information fields to the data that defines the entity in a strictly
graphical sense.1
AutoCAD is a good example of the developments that have been made in this field. A timeline for the
introduction of features aimed at accomplishing these objectives is shown in Table 23.1.
Release 2.0
User defined Layer names. Attributes that associate alphanumeric information to blocks.
October 1984
Release 2.18 First version of the AutoLISP programming language. Access to the entity database was not added to AutoLISP
January 1986 until Release 2.6 in June of 19861. It had been partially included in Releases 2.1 and 2.16.
Release 10
Optional permanent hexadecimal identifiers (HANDLES) for drawing entities.
October 1988
Release 11 Extended Entity Data (XDATA) may be appended to drawing entities by processing the DXF files and by
October 1990 AutoLISP programs.
The AutoCAD SQL Extension (ASE)/Autodesk SQL Interface (ASI) enables linking AutoCAD with an SQL
Release 12
database using AutoLISP. The Autodesk Data Extension (ADE) technology is introduced as an option for
June 1992
managing structured data.
Release 13 Entity handles are always enabled. DICTIONARY and XRECORD objects can be used as standard data
November 1994 containers. ADE 2.0 for R13c4 introduces the concept of Object Data Tables.
AutoCAD MAP is released as a vertical application addressed to the emerging GIS market. It is based on the
AutoCAD MAP
AutoCAD Data Extension (ADE) which is marketed since Release 12 and that from this date on is no longer
August 1996
available in AutoCAD.
Release 14 T h e DICTIONARY and XRECORD objects can now be managed as universal data containers for user
February 1997 applications development. First Visual LISP release is sold separately.
Visual LISP ActiveX extensions introduce LDATA dictionaries as a means of storing LISP objects in the drawing.
Release 2000 Visual LISP's ability to act as an ActiveX client allows operating on spreadsheets and databases. The ASE/ASI
March 1999 interface is for this reason obsolete. The recommended practice now is to establish data links directly through the
Microsoft ADO libraries. The final version of AutoCAD MAP's ActiveX object hierarchy is released.
The new TABLE command introduces anAcadTable object used for presenting data structured in rows and
columns. A AcadTable object can be linked to data in a Microsoft Excel spreadsheet. Tables support formulas
Release 2005 that do calculations using the values in other table cells.
March 2004
Since this release text objects can include fields whose contents can be formulas or drawing object properties.
They are automatically updated when data is modified. Fields can be included in table cells and block attributes.
The extraction of block attribute information is simplified with the enhanced EATTEXT command. This command
Release 2006
displays the Attribute Extraction dialog box. Run as -EATTEXT from the command line or a script it can export
March 2005
the data specified in an attribute extraction template file (BLK).
Release 2008 The EATTEXT command is replaced by the DATAEXTRACTION command, much more powerful, capable of
March 2007 extracting information both from attributes and drawing properties combining them into tables or external files.
Chronologically ordered, the features AutoCAD has been adding for associating alphanumeric
information to graphic objects are:
Block Attributes
Extended Entity Data (XDATA)
XRECORD objects and Dictionaries
LDATA Dictionaries.
Incorporating TEXT entities to the block definition does not involve any difficulty according to this
idea of a block. However, the presence of unchangeable alphanumeric data would be of scarce utility.
Including in a block definition alphanumeric information that can vary for each occurrence is an
anomaly that presents obvious difficulties. It would no longer be a copy transformed in a strictly
geometrical sense. To solve this a special graphic entity is used, the ATTDEF or attribute definition
object which acts as a placeholder predefining the visual characteristics the text will adopt.
Figure 23.1. Block with invisible attributes and its insertion.
Its content is defined only when each specific instance is generated, i.e., when the Block (see Figure
23.1) is inserted and a new entity, the Attribute Reference (ATTRIB) object, always associated to the
Block Reference (INSERT) object is created. The Attribute Reference is associated with the Block
Reference by following a sequence in the entities database, the INSERT entity acting as the header
for a succession of ATTRIB entities which comes to an end with a special sequence-end (SEQEND)
entity. This relationship was shown in Chapter 10 when programming for the creation of blocks with
attributes.
Both the Attribute Definition object as the Attribute Reference object are only instances of subclasses
(AcDbAttributeDefinition and AcDbAttribute) derived from the text objects
AcDbText subclass. What we’re doing is including texts which are modified to add an identifying
tag, a prompt and a default value, related to the block header (INSERT) by their sequential
position in the database. This expedient, although simple, is highly effective, but with the limitations
we’ll discuss later.
This makes the old ATTEXT command obsolete. The required external template file is an added
difficulty for this command . To automate the data extraction process, instead of scripting the
DATAEXTRACTION command line option we can design Visual LISP user functions.
Variable attributes extraction using ent... functions.
Listing 23.1 shows a function of this kind. It receives an entity name, from which its definition list is
retrieved. The rest of the function is conditioned to the selected entity being an inserted block with a
group code 66 value greater than 0. Group code 66 indicates that this block insertion is followed by
attributes. If both conditions are fulfilled, a while loop is used to retrieve the entities following the
INSERT (which will be variable attributes) and read the values associated with their group codes 1
and 2 that correspond to the attribute text’s value and its ID. A new feature of AutoCAD in regard to
attributes is the possibility to include multiline texts. In this case, in addition to group codes 1 and 2
we must take into account group code 3.
In the case of multiline texts, the ATTRIB entity will include more than one instance of group code 1,
the first one probably empty, and as many group codes 3 as were necessary, given the 250 characters
limit for the text in each sublist. To foresee this possibility a loop is implemented to traverse the
attribute’s entity list collecting in a list assigned to the txt variable the values associated with each
group code 1 or 3 found. This list is converted to a single string applying strcat to the reversed
list. In this string a series of "\\P" format codes used as paragraph ends may appear. To clean up
this text the replace function (Listing 5.12) proposed in Chapter 5 will be used. Using the attribute
tag associated with group code 2 and the string composed from group codes 1 and 3 a dotted pair is
created and added to a list. The loop ends when a SEQEND entity is found. The function returns the
reversed list so the values are ordered as they were found.
(defun ent-read-attributes
(ename / ent lst txt)
(setq ent (entget ename))
(if
(and (equal (cdr (assoc 0 ent)) "INSERT")
(> (cdr (assoc 66 ent)) 0))
(progn
(setq ent (entget (entnext ename)))
(while
(not (= (cdr (assoc 0 ent)) "SEQEND"))
(foreach datum ent
(if (or (= (car datum) 1)
(= (car datum) 3))
(setq
txt (cons (cdr datum) txt))))
(setq
lst (cons
(cons
(cdr (assoc 2 ent))
(replace
""
"\\P"
(apply ‘strcat
(reverse txt))))
lst)
ent (entget
(entnext
(cdr (assoc -1 ent))))))))
(reverse lst))
We must remember that the attributes that can be recovered this way are only the variable attributes
that are those included in the sequence between the INSERT and the SEQEND entities. Constant
attributes, if any, should be read in the block’s definition. Although constant attributes tend to be less
used, it is something that should be considered. This is especially inconvenient because no
information in the INSERT entity can be used to detect if the block definition includes constant
attributes.
The Apropos results include 9 constant names prefixed with ac and 17 function names with names
that include the vla- prefix.
Of these functions only 8 can be applied to blocks with attributes (see Table 23.2). The others apply
to Table (AcadTable) objects. To determine if the block contains attributes, the vla-get-
HasAttributes predicate, which reads the INSERT’s (BlockRef VLA-object)
HasAttributes property. Unlike group code 66, HasAttributes returns true (:vlax-
true) if constant or variable attributes or a mixture of both are present.
Figure 23.2. Available attribute symbols.
This list contains the attributes as VLA-objects. The properties we are interested in can be
queried for each attribute. If as in the previous example we are interested in its tag and value, we
must query the TagString and TextString properties. The quickest way for checking the
available properties is vlax-dump-object (Figure 23.3). The same procedure applies to
constant attributes that will be retrieved using the vla-GetConstantAttributes
function.
Method that creates an attribute definition with the specified position and properties. The space
argument can be the ModelSpace, the PaperSpace or a Block.
(vla-AddAttribute space height mode message ins-point value)
Mode values may be:
acAttributeModeInvisible Value not displayed.
AddAttribute
acAttributeModeConstant Has a fixed value.
acAttributeModeVerify Prompts for verifying the attribute value when inserting the block.
acAttributeModeLockPosition Locks position.
acAttributeModeMultipleLine Allows multiline text as value.
acAttributeModePreset Sets the attribute to its default value when the block is inserted.
AX-EXTRACT-ATTRIB.
Since the procedure is identical for all attributes, constant or not, we can design a single attribute
extraction function (Listing 23.2), which receives the block insert as a VLA-object and a boolean
argument (T or nil) to indicate whether the attributes to extract are constant or not.
The program must check if the value returned by the vla-GetConstantAttributes and vla-
GetAttributes functions is not empty. This should be done before trying to evaluate vlax-
safearray->list.
As in the previous case the character strings are processed so that multiline paragraph ending control
characters are removed. In this case we can check whether it is multiline text by applying the
condition (= (vla-get-MTextAttribute attrib) :vlax-true).
(defun ax-extract-attrib
(block constant / attributes lst)
(setq attributes
(vlax-variant-value
(if constant
(vla-getconstantattributes
block)
(vla-getattributes block))))
(if
(>= (vlax-safearray-get-u-bound attributes 1)
0)
(foreach attrib
(vlax-safearray->list attributes)
(setq lst
(cons
(cons
(vlax-get-property
attrib
"TagString")
(if
(=
(vla-get-MTextAttribute
attrib)
:vlax-true)
(replace
""
"\\P"
(vlax-get-property
attrib
"TextString"))
(vlax-get-property
attrib
"TextString")))
lst))))
lst)
AX-READ-ATTRIBUTES function.
The above function is called twice from ax-read-attributes (Listing 23.3), once with the
argument constant as T and the other as nil and the lists returned by both function calls are
joined using append. Before doing that, it has been verified that the object is a block and that it has
attributes. This verification is done checking if applying vla-get-HasAttributes to the
object whose ename has been received raises an error. There is no need to use the vl-catch-
all-error-p predicate, as checking whether the value assigned to result equals :vlax-
true will be enough.
(defun ax-read-attributes
(ename / block result lst)
(setq block (vlax-ename->vla-object ename))
(setq result (vl-catch-all-apply
‘vla-get-HasAttributes
(list block)))
(if (eq result :vlax-true)
(append
(reverse (ax-extract-attrib block t))
(reverse (ax-extract-attrib block nil)))))
The beginning of the XDATA fields in the entity’s data record is signaled by a group code -3 entry.
The data for each registered application is included under a 1001 code associated to the
application’s name. The data are grouped according to their type, in fields identified by group codes
ranging from 1000 to 1071. The extended data (as well as other entity definition data fields) can be
retrieved using the entget function, supplying a list containing the application’s name as an
optional argument.
Scaled, rotated or reflected along with the parent entity, but not stretched or
1012 WCS displacement
moved.
1013 WCS direction Unit vector that is rotated or reflected, but not scaled, stretched or moved.
1041 Distance Scaled along with the parent entity
1042 Scale Factor Transformed along with the entity to which the extended data belongs
It is advisable to choose an application name which is not likely to match one used by other
developers. A way for attaining this would be to use the company and/or product name linked to a
randomly chosen number (e.g., date and time) or any other hard to duplicate combination. The number
identifying the product version may be part of this name or can be included in an additional numeric
field, for example, (1040 2.1). Autodesk supports a developer registration service to prevent
namespace conflicts between different applications. Each developer registers with Autodesk one or
more "registered developer symbols" (RDS) to use as part of any name that will appear in a global
namespace.
Trusting the order in which the values were entered may not be reliable enough. The use of opening
and closing braces (group code 1002) can be used to create lists that include descriptive strings for
the rest of the data in each list. The AutoCAD Data Extension (ADE) 1.0 application prefixes the
string containing the data with "XX =" where XX represents a particular identifier for that data
within the application. Data thus coded can be retrieved using the Autodesk MAP API by the
ade_expreval function or an AutoLISP function could be defined to do it.
The code in Listing 23.4 demonstrates how to add data (always as strings) to any entity using the
ADE 1.0 format described above.
The ename argument is an object’s entity name, appname a string with the application name, lis-
id a list of identifiers such as '("DAT1 =" "DAT2 =" ... "DATn ="), lis-val a list of
values '("value-1" "value-2" ... "value-n") matching those identifiers, also as
strings. First, the function must ensure that the application is registered in the APPID table. If it is not
registered the expression (tblsearch "appid" appname) will return nil, otherwise it will
return T. If it is not already registered, regapp is used to register it.
(defun ent-xdata (ename appname lis-id lis-val /
data lst obj)
(if (not (tblsearch "appid" appname))
(regapp appname))
(setq data
(list
-3
(cons
appname
(foreach term (mapcar ‘strcat
lis-id
lis-val)
(setq lst
(append
lst
(list
(cons 1000 term))))))))
(setq obj (append (entget ename) (list data)))
(entmod obj))
Reading XDATA.
To read these data we will define a function that receives four arguments: ename, for the entity
name, appname for the application’s name, id for the data identifier (not including the "=") and
as-string, a boolean argument that can be T or nil, indicating if the data is to be preserved as a
string or if it should be converted to the data type that matches its printed representation by applying
the read function.
(defun ent-read-xdata
(ename appname id as-string / val)
(setq
id (strcat id "=*")
val (cdar
(vl-remove-if-not
‘(lambda (x) (wcmatch (cdr x) id))
(cdadr
(assoc
-3
(entget ename
(list appname)))))))
(if val
(progn
(setq val (vl-string-left-trim id val))
(if as-string
val
(read val)))))
The following expression assigns extended data to any object selected on the screen:
(ent-xdata (car (entsel)) "APP-TEST"
'("TEXT=" "NUM=" "POINT=") '("TEST" "104.5" "(15.0 22.0 0.0)"))
In this case we have chosen to return the data as a list and not as a string. Finally we must note that
any new data will overwrite the old one, so the programmer must implement the necessary control
measures to prevent that. The proposed function will not affect XDATA that belongs to an application
other than the one it receives as argument. This is because the inner workings of AutoCAD are
designed so as to ensure the integrity of data belonging to other applications.
XRECORD objects are used to store and manage arbitrary data and consist of DXF group codes
corresponding to normal object groups (i.e., group codes that are not XDATA) that range from 1 to
369. They are conceptually similar to XDATA, but are not limited by size and order.
AutoCAD uses dictionaries to store a variety of application related data. These dictionaries have
names in which the prefix ACAD appears. These dictionaries cannot be renamed. Among the
information stored in AutoCAD dictionaries are multiline styles, groups, Layouts, plot styles, etc.
The dotted pairs identified by group code 3 contain the names of the drawing’s dictionaries. In
vertical applications built on AutoCAD other specific dictionaries maintained by the application will
be found.
It may be useful to define a function (see Listing 23.6) that will return the list of names for the
dictionaries in a drawing in order to check whether a certain one already exists.
(defun dict-list ()
(mapcar ‘cdr
(vl-remove-if-not
‘(lambda (x) (= (car x) 3))
(entget (namedobjdict)))))
Searching a dictionary.
The same way we can check for an entry in a symbol table with tblsearch, dictsearch checks
for a dictionary in the drawing’s master dictionary:
(dictsearch ename symbol [setnext])
The function takes three arguments: ename, representing the dictionary to be searched, symbol as a
string with the name of the object to look for and the optional argument setnext which, if not nil
adjusts the dictnext entry counter so that the object returned by the following call to the dictnext
function (similar in this to entnext and tblnext) is the one following the one returned by this
dictsearch call.
The following expressions search for the "Layout1" object in the ACAD_LAYOUT dictionary.
(setq lyt (dictsearch (namedobjdict) "ACAD_LAYOUT"))
(dictsearch (cdr (assoc -1 lyt)) "Layout1")
Sequential search.
The dictnext function is used to retrieve dictionaries sequentially.
(dictnext ename [rewind])
The ename argument is the entity name of the dictionary where to search and the optional argument
rewind if not nil will make the function return the first object in the dictionary. Each time the
function is called without the rewind argument the next object in the dictionary is returned. For the
ACAD_LAYOUT dictionary, the following expression will return the name of the ModelSpace Layout.
_$ (cdr (assoc 1 (dictnext (cdr (assoc -1 lyt)) T)))
"Model"
_$
The entity type in this case is LAYOUT, as can be seen in the value associated with group code 0.
_$ (cdr (assoc 0 (dictnext (cdr (assoc -1 lyt)) T)))
"LAYOUT"
_$
Custom Dictionaries.
An application can also use dictionaries to store its own information. This information is permanently
recorded in the drawing, so that is not lost at the end of a work session as would happen to any data
assigned to global variables.
There is a special data container for these custom dictionaries. This container is the XRECORD
object. XRECORD objects can perform functions similar to XDATA but without their size limitations.
The XRECORD objects are created from lists of dotted pairs associated to DXF group codes 1
through 369. To create these XRECORD objects we use a variant of entmake: the entmakex
function that creates an object without an owner.
The entmakex function is able to create both graphic and non-graphic objects . It receives an
association list with DXF format identical to the one supported by entmake. But instead of
returning the entity list, it returns the newly created object’s entity name or nil if unable to create it.
This is an object without an owner. The entity name returned must be stored in a variable so an owner
can be set after its creation. This would be the way for creating custom dictionaries:
Changes in this sequence do not influence the results. The XRECORD objects could be created before
the dictionary, provided their references are preserved assigning them to variables.
Although the Visual LISP documentation warns that ownerless objects created with
entmakex are not saved unless they are assigned an owner (indicated by the DXF group
code 330), graphic objects created by entmakex are automatically assigned to the current
space, in this being just the same as entmake. In fact, if inspected immediately after they are
created group code 330 will contain the current space's ename.
A new dictionary must be created by entmakex before it can be added to the main dictionary. The
entity list needed is very simple. Only group codes 0 and 100 must be included. Once the dictionary
is created it must be added to the main dictionary using the dictadd function which assigns it a
name.
The dictadd function’s syntax is: (dictadd dict key newobj) where:
dict is the ename for the dictionary to which the object will be added.
key is a unique name (as string) that does not already exist in the dictionary. It will be used to
identify the added object.
newobj is the new Dictionary or XRECORD object.
_$ (setq new-dict
(entmakex '((0 . "DICTIONARY")(100 . "AcDbDictionary"))))
<Entity name: 4007c880>
_$ (dictadd (namedobjdict) "COUNTIES" new-dict)
<Entity name: 4007c880>
And using the function dict-list (Listing 23.6) defined above we can see that indeed, this
dictionary has been included in the main dictionary.
_$ (member "COUNTIES" (dict-list))
("COUNTIES")
_$
T he XRECORD entities will also be created using entmakex. Suppose we want to create an
XRECORD with two fields, each one containing a string. We must remember that the numerical values
used for DXF group codes that identify the data are selected by the programmer, provided they are
within the 1 to 369 range. The code should be chosen according to the type of data you want to save.
The correspondence between data types and group codes are shown in Table 23.4.
_$ (setq d1 '((0 . "XRECORD") (100 . "AcDbXrecord") (1 . "Marin")))
((0 . "XRECORD") (100 . "AcDbXrecord") (1 . "Marin"))
_$ (setq xrec (entmakex d1))
<Entity name: 4007c888>
As for the other entities, the information regarding the meaning of DXF codes for XRECORD
and DICTIONARY entities and can be found in the online documentation's DXF Reference.
The above expressions have created a new XRECORD entity. Supposing we must save names as text
strings associated with an existing map’s graphic entities. In the previous example we can assume that
the value associated with group code 1 is a toponym we want to associate with the graphic entity. For
each entity-name relationship a new XRECORD would be added to the dictionary where the entity’s
handle would be used as key.
_$ (setq id (cdr (assoc 5 (entget (car (entsel))))))
"AF"
_$ (dictadd new-dict id xrec)
<Entity name: 4007c888>
_$
This would allow creating an application from which obtaining the graphic entity’s identifier and
knowing the dictionary’s name, we could retrieve the string containing the associated toponym.
_$ (setq toponyms (cdr (assoc -1 (dictsearch (namedobjdict) "COUNTIES"))))
<Entity name: 4007c880>
_$ (cdr (assoc 1 (dictsearch toponyms "AF")))
"Marin"
_$
This application will include two commands. One of them, C:TOPONYMS will associate data to
graphic entities. And the other one, C:IDENTIFY will query these data by selecting the graphic
entities. We will describe now the functions that make up the first part, the ones used for associating
data.
Data Entry.
Initially the data-input function (Listing 23.7) enters a loop in which the user is prompted for
selecting a graphic entity and once it has been selected, to enter a name for it. That entity’s handle and
the name supplied by the user will be used in forming a dotted pair that would be added to a list. The
loop will end if ENTER is pressed in response to either prompts.
We can then implement a function capable of creating a new XRECORD for each dotted pair, adding it
to the custom dictionary. The toponym and the handle which acts a the key for retrieving the
information will be saved in the XRECORD.
(defun data-input (/ ent name lst)
(while
(and (setq ent
(car (entsel
"\nSelect entity:")))
(setq name
(getstring t "\nSpecify name:"))
(not (= name "")))
(setq lst
(cons
(cons (cdr (assoc 5 (entget ent)))
name)
lst)))
lst)
But before that we must ensure that the dictionary exists, otherwise it must be created.. This is done
by the make-dict function (Listing 23.8), after checking that it doesn’t exist. To do this it calls the
dict-list function defined in Listing 23.6. The dictionary is then created using entmakex and
added to the drawing’s main dictionary using as key the name argument, which must be a string. If it
existed, its entity name is retrieved. In both cases the function will return the DICTIONARY’s
ename.
(defun make-dict (name)
(if (not (member name (dict-list)))
(dictadd
(namedobjdict)
name
(entmakex
‘((0 . "DICTIONARY")
(100 . "AcDbDictionary"))))
(cdr
(assoc -1
(dictsearch
(namedobjdict) name)))))
Listing 23.8. Function that creates a dictionary, or retrieves its ENAME in case it already existed.
Once we have the data list and the dictionary’s ename, we can implement the data insertion
procedure. Processing (Listing 23.9) Is done within a foreach cycle that traverses the data list. It
verifies that there is no record associated with the same handle. This would be the case in which a
toponyms is to be changed. To do this the existing XRECORD must be removed using the
dictremove function. The XRECORD removed from the dictionary still exists, but it is now
ownerless. So once removed from the dictionary, the XRECORD entity is erased using entdel.
Once this phase is concluded, the new XRECORD entity is created and if this step is successful it is
added to the dictionary using dictadd. If this operation does not succeed an error message is
printed in the command line.
(defun new-records (dict-ent data-list / xrec)
(foreach datum data-list
(if (dictsearch dict-ent (car datum))
(entdel (dictremove dict-ent (car datum))))
(if (setq xrec
(entmakex
(list ‘(0 . "XRECORD")
‘(100 . "AcDbXrecord")
(cons 1 (cdr datum)))))
(dictadd dict-ent (car datum) xrec)
(prompt "\Error adding record"))))
To invoke all these functions a new command is defined as the C:TOPONYMS function (Listing
23.10). This function checks in the clauses of a cond conditional:
If the dictionary’s ename has been retrieved within the cond expression, an additional condition is
checked: that the data-input function doesn’t return nil. If these conditions are met, new-
records is called to link the information to the selected objects. Otherwise an alert box is
displayed notifying an application error.
(defun C:TOPONYMS (/ data dict)
(cond
((setq dict (dictsearch
(namedobjdict)
"TOPONYMS"))
(setq dict (cdr (assoc -1 dict))))
((setq dict (make-dict "TOPONYMS")))
(t
(prompt
"\nError creating the dictionary")))
(if (and dict (setq data (data-input)))
(new-records dict data)
(alert "Application error"))
(princ))
This application would not be complete without the ability to query the linked data. This is done by a
function that, despite being very simple, demonstrates the process for reading data linked to the
graphic entities selected on screen. In this example only one data item has been associated, but it
could be easily modified to include more information.
(defun C:IDENTIFY (/ dict ent place)
(if (and (setq dict (dictsearch
(namedobjdict)
"TOPONYMS"))
(setq dict (cdr (assoc -1 dict))))
(while (setq ent
(car
(entsel
"\nEntity to identify: ")))
(if (setq place
(dictsearch
dict
(cdr (assoc 5 (entget ent)))))
(alert
(strcat
"The entity represents\n"
(cdr (assoc 1 place))))
(alert
"The entity does not have a name.")))))
The C:IDENTIFY function (Listing 23.11) begins by checking that the application dictionary exists
and that its ename can be recovered. Having made these verifications, the user is repeatedly
prompted in a while loop for the selection of an entity. Once the selection has been made its handle
is searched for in the dictionary. If found, the value associated with the XRECORD’s group code 1 is
read and the linked toponym is displayed in a message box. If none appears, a message box is
displayed to inform that the designated entity has no associated name.
But there is a simpler set of ActiveX dictionary functions. These dictionaries belong to a special type
known as LDATA. We already used this format in the Chapter 1 Tutorial.
LDATA functions can save a lot of programming effort. In the following section we will summarize
the use of these functions when dealing to storing non-graphical information in the drawing, with or
without links to graphic entities.
Caution should be exercised when using this kind of object because the LDATA information
for Release 2000 and above is not compatible with AutoCAD Release 14. If the drawing is
saved in R14 drawing format information may be lost or corrupted. Although it's not likely
today, a drawing to be used with such an old version should use XRECORD dictionaries. But
everything is relative, since we know that XRECORD would be lost when using the drawing in
a release prior 3c4, and XDATA would only survive the round trip to Release 11, but not
beyond it.
There is no specific function for the creation of LDATA dictionaries. Adding data will automatically
create them. The dictionary can exist by itself in the drawing with a name that identifies it, but it can
also be attached to any drawing object.
Adding data.
For adding data to a LDATA dictionary the vlax-ldata-put function is used.
(vlax-ldata-put dict key data [private])
The dict argument may be a VLA-object, An AutoCAD drawing entity, or a string with the name
of a global dictionary. If an object or entity, the information will remain linked with it. Otherwise it
will remain as a global object in the document. The dictionary created in the Volume 1, Chapter 1
tutorial was of this latter type. The argument key is a text string that identifies the associated data.
This associated data can be any valid LISP expression. Thevlax-ldata-put function can
receive an optional argument [private] that in case of having any non-nil value indicates that this
information, if created by a separate namespace VLX application will not be accessible from any
other application.
LIST->LDATA function.
The following function (Listing 23.12) creates a dictionary from an association list using a foreach
loop that repeatedly calls vlax-ldata-put:
(defun list->ldata (dict lst)
(foreach sublist lst
(if (listp sublist)
(vlax-ldata-put
dict
(vl-princ-to-string (car lst))
(cdr sublist)))))
Listing 23.12. LIST->LDATA function.
To test it let’s draw some entity, such as a circle. Its entity definition list can be stored in a dictionary
with the following expression:
_$ (list->ldata "DAT-ENT" (entget (entlast)))
(0.0 0.0 1.0)
_$
The arguments dict and key assume the same values as in the previous function. The optional
argument default-data would be the LISP expression returned in case key is not found in the
dictionary.
_$ (vlax-ldata-get "DAT-ENT" "0" "Data does not exist")
"CIRCLE"
_$ (vlax-ldata-get "DAT-ENT" "1" "Data does not exist")
"Data does not exist"
_$
If a value other than nil is specified in the optional argument private and the vlax-ldata-
get is called from a separate namespace VLX application, the dictionary’s private information is
returned. As the argument is identified by its position, to specify the argument private, the
argument default-data must be included before even if its value is nil.
A separate namespace VLX application can store both private and non-private information using the
same dictionary and key. Information marked as private can only be read by the application that
created it, but any other application can access non-private information.
Arguments dict and private are the same described above. The information is returned as an
association list in which sub-lists are dotted pairs. Mapping print on this list would print all the
values in a column. Enclosing the mapcar expression in a progn block ending with (princ) will
provide a cleaner output on screen, since otherwise the list as it is returned by mapcar would print
again as a row.
("8" . "0")
("67" . 0)
("5" . "31")
("410" . "Model")
("40" . 25.0)
("330" . <Entity name: 40086cf8>)
("210" 0.0 0.0 1.0)
("100" . "AcDbCircle")
("10" 50.0 100.0 0.0)
("0" . "CIRCLE")
("-1" . <Entity name: 40086d88>)
Removing LDATA
To remove information from a dictionary the vlax-ldata-delete function is used.
(vlax-ldata-delete dict key [private])
The function returns T if successful, and nil otherwise (for example, if the data does not exist).
_$ (vlax-ldata-delete "DAT-ENT" "5")
T
_$ (vlax-ldata-delete "DAT-ENT" "6")
nil
_$
We must remember that these functions are destructive. Which means that they permanently modify the
dictionary object they receive as argument.
Including this predicate in that same list->ldata function would avoid including information that
could not be restored over the session boundary.
(defun list->ldata (dict lst)
(foreach sublist lst
(if (and (listp sublist)
(vlax-ldata-test (cdr sublist)))
(vlax-ldata-put
dict
(vl-princ-to-string (car sublist))
(cdr sublist)))))
Some of the LDATA information we have included in the dictionary is automatically updated between
sessions. This is the case of entity names that may change between sessions. Let’s draw some more
objects and evaluate once more:
(list->ldata "DAT-ENT" (entget (entlast)))
Now we check the value stored in the "-1" key:
_$ (vlax-ldata-get "DAT-ENT" "-1")
<Entity name: 40089e50>
_$
Because this value depends at least in part, on the entities sequential order in the drawing’s database,
we can delete some entities, taking care not to delete the last object drawn. If once we have created
the DAT-ENT dictionary we save the drawing, close and re-open it, we will find that the data
associated with the "-1" key has changed:
_$ (vlax-ldata-get "DAT-ENT" "-1")
<Entity name: 40087e10>
_$
To verify that this is actually the same object we can use the entdel function to erase it:
_$ (entdel (vlax-ldata-get "DAT-ENT" "-1"))
<Entity name: 40087e10>
_$
We can now see that this object has disappeared from AutoCAD’s graphic screen. If we evaluate
again the same expression above, the object will be recovered. This is a very interesting feature we
can use to establish permanent links that are updated automatically between objects. The function in
Listing 23.14 associates an object to another by attaching LDATA to the one it receives as the ent-
main argument:
(defun associate (ent-main ent-assoc key)
(vlax-ldata-put ent-main key ent-assoc))
This data is retained even if both objects are exported from the AutoCAD drawing using the
_WBLOCK command and are inserted into a different drawing. This functionality has been available
in AutoLISP since Release 11 by using the XDATA format, but as we have seen, the use of XDATA
required, just as with XRECORD dictionaries, programming by user all of the assignment, reading and
modification functions.
Large volumes of information are more conveniently stored it in external databases. This is a topic to
which a whole book can be devoted2. But this chapter must not be concluded without having at least
indicated the way to follow for those interested in the subject.
Access to external databases is done in AutoCAD 2000 and subsequent releases through the ActiveX
Data Objects (ADO) provided by Microsoft. AutoCAD’s Connectivity Automation Objects (CAO)
are used to establish links between AutoCAD objects and external databases. In both cases these are
ActiveX component libraries that must be imported before its methods and properties are available
as Visual LISP functions.
The names preceded by a colon are not arguments. They are keywords indicating the role for the
arguments that follow. They are protected system symbols, as we can verify from their color-coding
when typed in the editor.
The :tlb-filename keyword indicates that the argument that follows is the name of the file that
contains the library. It will usually have a TLB or OLB extension, although sometimes it may be a
DLL or EXE. The other keywords indicate the prefixes to be assigned to constants, properties and
methods. Although this is not essential, it is a quite beneficial practice as it allows us to recognize the
library from which the function proceeds and if it is a constant, a method or property. In our own
practice we use the prefixes adoC- for constants, adoP- for properties and adoM- for methods in
the ADO library. When importing the CAO library we use caoC-, caoP- and caoM- respectively.
You must include the search path in which to look for those files, but these can change depending on
the system. For their use in real applications some way to establish the correct path is needed. In
versions 2010 through 2013 the CAO and ADO libraries are located in common program files
subdirectories whose path can be retrieved from the COMMONPROGRAMFILES environment
variable. Its value is returned by getenv:
_$ (getenv "COMMONPROGRAMFILES")
"C:\\Program Files\\Common Files"
_$
The locate-ADO-CAO function (Listing 23.15) uses this expression in trying to locate the ADO
(msado15.dll) and the CAO libraries. Of the latter there are a localized versions that can be
imported if working in a localized version. Otherwise the English version (cao16enu.tlb) will be
used. Localized versions can be identified, as explained in Volume 2, Chapter 10 from the
application’s LocaleID property. LocaleID values are shown in Table 10.8.
(defun locate-ADO-CAO (/ dir CAOdir ADOdir)
(setq dir (getenv "COMMONPROGRAMFILES")
ADOdir (findfile
(strcat
dir
"\\system\\ado\\msado15.dll"))
lcid (vla-get-LocaleID
(vlax-get-acad-object))
CAOdir
(findfile
(strcat dir
"\\AUTODESK SHARED\\"
(cond ((= lcid 1028)
"cao16cht.tlb")
((= lcid 1029)
"cao16csy.tlb")
((= lcid 1031)
"cao16deu.tlb")
((= lcid 1034)
"cao16esp.tlb")
((= lcid 1036)
"cao16fra.tlb")
((= lcid 1038)
"cao16hun.tlb")
((= lcid 1040)
"cao16ita.tlb")
((= lcid 1041)
"cao16jpn.tlb")
((= lcid 1042)
"cao16kor.tlb")
((= lcid 1046)
"cao16ptb.tlb")
((= lcid 1049)
"cao16rus.tlb")
((= lcid 2052)
"cao16chs.tlb")
(t "cao16enu.tlb")))))
(list ADOdir CAOdir))
Although in the current Visual LISP version libraries are imported at run time, rather than at
compile time, this may change in future releases. Visual LISP documentation advises that
vlax-import-type-library should be called directly in the code from a top-level
position at the beginning of the source code file, and not inside other AutoLISP expression.
The functions shown below were developed for the current conditions and may not be valid in
case the current way of importing component libraries changes.
To load the component libraries we can use the standard function import-ADO-CAO (Listing
23.16). This function uses the locate-ADO-CAO function described above.
It returns T if imported successfully and nil if not, value which can be used to interrupt program
execution and warn the user if any of the libraries is not installed.
_$ (import-ADO-CAO)
T
_$
A first clause has been added to check if the library has been already loaded, which operates by
obtaining the list of symbols returned by atoms-family and using member to check if a symbol
matching a pattern composed by the prefix followed by a wildcard is found in the list. Once the
libraries are imported we can use the Apropos tool to inspect the new functions, searching for them
by prefix. Figure 23.4 shows the results of this search.
(defun import-ADO-CAO (/ libs)
(vl-load-com)
(setq libs (locate-ADO-CAO))
(if (car libs)
(cond
((vl-member-if
‘(lambda (x) (wcmatch x "ADOM-*"))
(atoms-family 1))
t)
(t
(vlax-import-type-library
:tlb-filename
(car libs)
:methods-prefix
"adoM-"
:properties-prefix
"adoP-"
:constants-prefix
"adoC-")))
(prompt "\nERROR: ADO not found"))
(if (last libs)
(cond
((vl-member-if
‘(lambda (x) (wcmatch x "CAOM-*"))
(atoms-family 1))
t)
(t
(vlax-import-type-library
:tlb-filename
(last libs)
:methods-prefix
"caoM-"
:properties-prefix
"caoP-"
:constants-prefix
"caoC-")))
(prompt "\nERROR: CAO not found.")))
Moving forward...
We announced that our purpose is not to describe the procedures for external database access and
linking. Once the necessary components have been imported the programmer will be working in an
area that is out of the scope of this book. As a programming language, Visual LISP provides many
resources, but to understand the behavior of these new functions we must refer to these libraries
specific documentation. For ADO it may be found in Microsoft’s ado210.chm Help file. The
information on CAO components can be found in the online Help, under the Connectivity
Automation (CAO) heading. For more information about ADO, you can search the Microsoft
Website.
23.7 Summary.
This chapter has outlined the different ways in which Visual LISP ban be used to assign, retain and
retrieve alphanumeric information within the drawing.
The different ways to do this reflect the application’s development from its origins to the present day.
We have seen that the information can be stored in three ways:
As part of the entity, assigning it to block attributes or as extended entity data (XDATA).
In autonomous containers within the drawing, which can be linked or not to entities: XRECORD
objects, Dictionaries and LDATA Dictionaries.
In external databases, linked or not to the drawing’s entities.
Some applications built on AutoCAD incorporate their own variants for data management built on the
features offered by these basic procedures. As an example we can point to the Object Data used by
Autodesk MAP, implemented with a combination of Dictionaries and XDATA.
Although we have not delved into the topic of external databases, one of the applications included in
this book shows how to extract the data in block attributes and export it to an Excel spreadsheet.
Exercises.
Exercise 1.
Modify the C:TOPONYMS application so that instead of asking the user to associate names to
graphical entities it read these names from a external text file and prompts the user only for selecting
the entity to which each name belongs.
2 Those interested in this subject can read Scott McFarlane's AutoCAD Database Connectivity, Autodesk Press/Thomson
Learning, ISBN 0-7668-1640-0.
Chapter 24
Tables
Since AutoCAD 2005 a new kind of object, the AcadTable can be used to automate the creation of
tables in the drawing. This is a compound object containing data structured in a variable number of
rows and columns. The AutoCAD 2012/2013 user interface is highly effective for managing tables
using the commands described in Table 24.1.
To create tables from Visual LISP we have the methods and properties exposed by the AcadTable
object. These reproduce the effects of these commands from a Visual LISP application. The
AcadTable class is the one which more specific methods (181) and properties (28) exposes, so
addressing this topic in an exhaustive way in a book like this is not possible. However, for the usual
applications just a very small subset of these methods and properties is necessary. To them we
dedicate this chapter.
Creating a Table.
A table is created by the AddTable method. This method receives as parameters the space in which
to create the table, its insertion point, the number of rows and of columns, the row height and the
column width. After adding the table to the drawing, content may be added, and the number of
columns, the number of rows and other parameters can be changed. The syntax for the AddTable
method is:
(vla-AddTable
space insertion-point num-rows
num-cols row-height column-width)
insertion-point Coordinates for the Table's upper left corner as the safearray type returned by vlax-3d-point.
The ins-table function (Listing 24.1) creates an empty Table after prompting the user for the
necessary information. The space in which to create the Table (Model or any of the Layouts) is
obtained using the current-space function defined in Listing 10.31.
(defun ins-table
(/ pt row-height col-width nrows ncols)
(setq pt (getpoint "\nUpper left corner: ")
row-height (getdist pt "\nRow height: ")
col-width (getdist pt "\nColumn width: ")
nrows (getint "\nNumber of rows: ")
ncols (getint "\nNumber of columns: "))
(vla-AddTable
(current-space *aevl:drawing*)
(vlax-3d-point pt)
nrows
ncols
row-height
col-width))
The format the Table adopts depends on the current TableStyle. This style is specified in the
CTABLESTYLE system variable. The default style is "Standard". This style includes a cell style
named "title" that merges the row’s cells and centers the text. This style is used in an additional
title row that is inserted as the new Table’s first row. Because of this, the Table created using the
"Standard" style will contain one more row than the number vla-AddTable receives as the
num-rows argument.
The SetText method adds text to a Table cell. This method receives as parameters the Table as a
VLA-object, the row index, the column index and the text string. Its syntax is:
(vla-SetText table-obj row-index column-index text)
GetText method.
The GetText method retrieves text from any of the Table’s cells. This method returns a string with
the text assigned to the cell specified by its row and column indices. Its syntax is:
(vla-GetText table-obj row-index column-index)
The SetTextHeight method can be used to set a row’s text height. This method receives as
parameters the Table object to modify, the row type and the text height. Its syntax is:
(vla-SetTextHeight table-obj row-type text-height)
text-height Zero-based index for the column where the text will be inserted.
The SetTextHeight method changes the text height in all rows of a given type. If the text in an
isolated cell must be modified it will be necessary to use the SetTextHeight2 method, which
receives as parameters the Table’s VLA-object, the row and column indices (row-index and
column-index), a content index (icont) usually 0, which provides for multiple contents in the
cell, and the new text height. Its syntax is:
(vla-SetTextHeight2
table-obj row-index column-index icont text-height)
Using this method it would be possible to change the text height of a single cell and in a loop, of an
entire row or column. The set-row-text-height function (Listing 24.2) shows the use of this
method to change a whole row’s text height.
(defun set-row-text-height
(table-obj i-row text-height /)
(setq i 0)
(repeat (vla-get-Columns table-obj)
(vla-setTextHeight2
table-obj i-row i 0 text-height)
(setq i (1+ i))))
GetTextHeight/GetTextHeight2 Methods.
The GetTextHeight method retrieves the text height for a type of row. The parameters it receives
are the Table as a VLA-object and the AcRowType enum constant for the row type. Its syntax is:
(vla-GetTextHeight table-obj row-type)
The GetTextHeight2 method retrieves the text height for an individual cell. The parameters are
the same as described for the previously explained methods. Its syntax is:
(vla-GetTextHeight2 table-obj row-index column-index icont)
SetCellAlignment method.
The SetCellAlignment method sets the text alignment for a cell specified by its row and column
indices. Its syntax is:
(vla-SetCellAlignment
table-obj row-index column-index cell-alignment)
GetCellAlignment method.
The GetCellAlignment method retrieves the enum constant that defines the text alignment in a
cell specified by its row and column indices. Its syntax is:
(vla-GetCellAlignment table-obj row-index column-index)
Inserting Rows.
For inserting rows into an existing Table we can use the InsertRows method or the
InsertRowsAndInherit method.
InsertRows method.
The InsertRows method receives as parameters the Table as a VLA-object, the zero-based
index (row-index) for the first row of the group to be inserted, the row’s height (row-height)
and the number of rows (num-rows) to be inserted. Its syntax is:
(vla-InsertRows table-obj row-index row-height num-rows)
InsertRowsAndInherit method.
Instead of the row height, the InsertRowsAndInherit method receives the index
(inheritFrom-index) for the row whose characteristics the new rows will adopt. The other
parameters are similar to those for the previous method. Its syntax is:
(vla-InsertRowsAndInherit
table-obj row-index inheritFrom-index num-rows)
Inserting Columns.
For inserting columns into an existing Table we can use the InsertColumns method or the
InsertColumnsAndInherit method.
InsertColumns method.
The InsertColumns method receives as parameters the Table as a VLA-object, the zero-
based index (column-index) for the first column of the group to be inserted, the column’s width
(column-width) and the number of columns (num-cols) to be inserted. Its syntax is:
(vla-InsertColumns
table-obj column-index column-width num-cols)
InsertColumnsAndInherit method.
Instead of the column width, the InsertColumnsAndInherit method receives the index
(inheritFrom-index) for the column whose characteristics the new columns will adopt. The
other parameters are similar to those for the previous method. Its syntax is:
(vla-InsertColumnsAndInherit
table-obj column-index inheritFrom-index num-cols)
Both are checked within an and expression that includes a ssget single object selection filtered for
the INSERT entity type and verifying that this object’s HasAttributes property is :vlax-
true. If both conditions are not met the message "Select a block with attributes" is
printed in the command line.
(defun sel-block
(name / ss count ename data data-list)
(setq count 0)
(if (setq ss (ssget "X"
(list (cons 0 "INSERT")
(cons 2 name))))
(while (setq ename (ssname ss count))
(setq obj (vlax-ename->vla-object ename))
(setq data
(list
(cons "LAYER"
(vla-get-layer obj))
(cons
"COORDS"
(vlax-safearray->list
(vlax-variant-value
(vla-get-insertionpoint
obj))))))
(setq data-list
(cons
(append
data
(ax-read-attributes
ename))
data-list))
(setq count (1+ count))))
data-list)
Listing 24.4. Selection of the block to process and extraction of its attributes as a list.
Once the user has selected a block insert with attributes its name is retrieved and used as argument
for the sel-block function. This function is not limited to extracting the attribute values. It also
retrieves the name of the Layer in which it has been inserted and its insertion point coordinates, data
which is added to the values obtained from the attributes. For extracting attributes the ax-read-
attributes function defined in the previous Chapter (Listing 23.3) is used. This function calls in
turn the auxiliary function ax-extract-attrib (Listing 23.2). The list returned for each entity is
accumulated in the data-list variable which is returned by sel-block.
This is a list with three nesting levels. The top-level list, delimited by the outermost parentheses
represents the entire Table. This list must be contain other nested lists (second level lists) in varying
numbers, each of which represents a row. Each of these sublists in turn contain other lists in which
the head (its first term, as returned by the car function) represents the meaning of the data in the list’s
tail (the rest of the list as returned by the cdr function). Each of these third level lists represents a
cell. The second level lists (rows) are structured as association lists similar to those used to
represent entity properties in AutoLISP, i.e., those returned by the entget function. The steps
followed are:
After selecting the INSERT and obtaining the data from its attributes, the user is prompted for the
Table’s insertion point (upper left corner) and for the opposite corner, which will define the area
occupied by the Table. From these two points the program calculates the row height, the column
widths and the text height. The first two are used in creating the Table and the text height is defined
once it has been created.
We are assuming that the default TableStyle "Standard" is the current one. This style includes a
first row in which all cells are combined into a single one. That first row that is intended for the
Table’s title is of the AcTitleRow type. The second row, Intended for the column headers, is of the
AcHeaderRow type and the rest, which will be used for data are all of the AcDataRow type. For
the title row cell’s text the block’s name, assigned to the variable name, is used. Once the title text
has been inserted, the column headers will be inserted in the Table’s second row. The text to be used
will be the first term of each sublist in the first data list. A format property we will modify is the text
alignment, but as this is a cell property it will be done when inserting each cell’s text.
Text will be inserted by vla-SetText. The title row has only one cell with index 0 for both the
row and the column. The row index for header cells is 1, with their column indices varying from zero
to (1- data-length).
(vla-SetText table-obj 0 0 name) ; Title
(setq i 0)
(foreach datum (car data-list)
(vla-SetText table-obj 1 i (car datum)) ; Header
(setq i (1+ i)))
Upon completion of the title and headers rows the list is processed in two nested repeat loops,
where for each row the text in the cdr of each sublist is inserted in a cell. Note that as the inclusion
of texts in the data rows must begin with the third row, 2 is always added to the index used to specify
the row.
(setq i 0)
(repeat (1- nrows) ; Data rows
(setq j 0)
(repeat ncols
(vla-SetCellAlignment
table-obj
(+ i 2)
j
acMiddleCenter)
(setq txt (vl-princ-to-string
(cdr (nth j (nth i data-list)))))
(vla-SetText table-obj (+ i 2) j txt)
(setq j (1+ j)))
(setq i (1+ i)))
Column widths.
As the contents of the different cells cannot be predicted we must have a way to adjust column widths
to the data that must be inserted in the Table. Column widths will be calculated from the information
contained in data-list by the col-w function.
This function receives as arguments that list and the number of columns (ncols). This function will
traverse each row sublist obtaining the maximum number of characters for each cell, considering both
the data and the header text. This is done by two nested mapcar expressions that will return in a list
assigned to lst-w, for each row, a list with the number of characters in each cell.
This list will then be processed in a repeat loop which will obtain the maximum number of
characters found in each column. these are included in a list assigned to the variable ls using cons.
This list’s order will be reversed and returned by col-w. These values will be used in the main
function C:ATTRIB-TABLE to set the width of each column. This will be done by adding them to
obtain the maximum possible number of characters in a row. Dividing the Table width specified by
the user by this number we will get a width-per-character value which will be multiplied by the
maximum number of characters in each column to obtain the width that will be assigned to each
column in a foreach loop:
(setq char-per-cols (col-w data-list ncols)
width-per-char (/ table-width
(apply ‘+ char-per-cols))
i 0)
(foreach w char-per-cols
(vla-SetColumnWidth
table-obj
i
(* width-per-char w))
(setq i (1+ i)))
Listing 24.5. Function that calculates the approximate relative widths for columns.
The Table created this way will use many properties define by the TableStyle. Once it is created it
can be easily adjusted by changing Style parameters. It can be also adjusted in size using the grips
which are displayed when the Table is selected.
(defun C:ATTRIB-TABLE (/ *error* curr-layout
block name char-per-cols
width-per-char data-list
nrows ncols pt-ins
pt-corner row-height
col-width table-obj i j
txt)
(vla-StartUndoMark *aevl:drawing*)
(defun *error* (msg)
(vla-EndUndoMark *aevl:drawing*)
(vl-cmdf "_U")
(prompt msg))
(if (/= (setq curr-layout (getvar "CTAB"))
"Model")
(setvar "TILEMODE" 1))
(while
(not
(and (setq
block (ssget "_:S"
‘((0 . "INSERT"))))
(equal (vla-get-HasAttributes
(vlax-ename->vla-object
(ssname block 0)))
:vlax-true)))
(prompt
"\nSelect a block with attributes: "))
(setq name (cdr (assoc 2 (entget (ssname block 0))))
data-list (sel-block name)
nrows (length data-list)
ncols (length (car data-list)))
(setvar "CTAB" curr-layout)
(initget 1)
(setq pt-ins
(getpoint
"\nSpecify table insertion point: "))
(initget (+ 1 32))
(setq pt-corner (getcorner pt-ins
"\nSpecify table size: ")
row-height (/ (abs (- (nth 1 pt-ins)
(nth 1 pt-corner)))
(1+ nrows))
table-width (abs (- (nth 0 pt-corner)
(nth 0 pt-ins)))
table-obj ; Table creation
(vla-AddTable
(current-space *aevl:drawing*)
(vlax-3d-point pt-ins)
(1+ nrows)
ncols
row-height
(/ table-width ncols)))
(vla-put-RegenerateTableSuppressed
table-obj
:vlax-true)
;;Column widths are adjusted to cell text content
(setq char-per-cols (col-w data-list ncols)
width-per-char (/ table-width
(apply ‘+ char-per-cols))
i 0)
(foreach w char-per-cols
(vla-SetColumnWidth
table-obj
i
(* width-per-char w))
(setq i (1+ i)))
(vla-SetText table-obj 0 0 name) ; Title
(setq i 0)
(foreach datum (car data-list)
(vla-SetText table-obj 1 i (car datum))
; Header
(setq i (1+ i)))
(setq i 0)
(repeat (1- nrows) ; Data rows
(setq j 0)
(repeat ncols
(vla-SetCellAlignment
table-obj
(+ i 2)
j
acMiddleCenter)
(setq txt
(vl-princ-to-string
(cdr (nth j (nth i data-list)))))
(vla-SetText table-obj (+ i 2) j txt)
(setq j (1+ j)))
(setq i (1+ i)))
(vla-put-RegenerateTableSuppressed
table-obj
:vlax-false)
(vla-EndUndoMark *aevl:drawing*))
Figure 24.1. Drawing that includes a Table generated by the C:ATTRIB-TABLE program.
24.3 Summary.
We have seen how to program a Table with drawing data, in this case block attributes, but easily
extensible to any type of data as those described in Chapter 23. We have only explained in this
chapter the Table’s essential features, which in most cases may be enough. To create tables more
elaborate in their format the AcadTable class exposed methods and properties may be used, but in
many cases it will be easier to use one or more custom Table Styles.
Chapter 25
Visual LISP as ActiveX client
Visual LISP can act as an ActiveX client and server. This chapter demonstrates an alternative to the
sample program in Chapter 24. If in that chapter we showed how to create a Table with drawing data,
in that case the Block INSERT attributes. we will devote this chapter to a variant that exports those
data to an Excel spreadsheet. As we saw in Chapter 22 how to design a DCL dialog box, we will use
this exercise to demonstrate the use of another tile, the list_box. The user interface of AutoCAD
since release 2008 provides commands specially designed to establish these links without
programming. The _DATAEXTRACTION command can be used for extracting drawing information
to an Excel spreadsheet, an Access database table or to CSV and TXT exchange files. This command
displays a dialog box to select the information and set its destination. It is our goal to explain in this
chapter the basic principles for doing this as a part of a Visual LISP application.
We will implement an utility function, called list->excel that will receive as argument an
association list (see Figure 25.1) Identical to the one used for creating the Table in Chapter 24 and
use the information it contains to fill an Excel spreadsheet. Any list that respects the structure
described above can serve as an argument for the list->excel function.
As columns headers the association list keys will be used. Clearly, a correct result is obtained only if
all these second-level lists have the same structure, i.e., the same keys in the same order.
Figure 25.1. Data list structure.
CONNECT-EXCEL function.
The connect-excel function (Listing 25.1) has been defined to establish this connection.
(defun connect-excel (/)
(setq excel-app (vlax-get-or-create-object
"Excel.Application")
wbook-coll (vlax-get-property
excel-app
"Workbooks")
wbname (strcat (vl-filename-base
(getvar "dwgname"))
".xls"))
(setq wbook (vl-catch-all-apply
‘vlax-get-property
(list wbook-coll
"Item"
wbname)))
(cond
((vl-catch-all-error-p wbook)
(setq wbook (vlax-invoke-method
wbook-coll
"Add"))
(vlax-invoke-method
wbook
"SaveAs"
(strcat (getvar "dwgprefix") wbname)
; Filename
56 ; FileFormat
"" ; Password
"" ; Write Reservation Pwd
:vlax-false ; ReadOnlyRecommended
:vlax-false ; CreateBackup
1))) ; XlSaveAsAccessMode
(setq sheet-coll
(vlax-get-property
wbook
"Sheets"))
(vla-put-visible excel-app :vlax-true))
The function vlax-get-or-create-object creates the initial link returning an instance of the
Excel Application object, a reference to which is assigned to the LISP variable excel-app.
If Excel is not running, it will be launched. The Application is the root for the Excel hierarchy of
objects. From there on this hierarchy is traversed down to the collection (Range) of cells. All these
VLA-objects are assigned to variables. Information about Excel’s object hierarchy can be found in
that program’s Help files, but in recent versions an understandable description of the object model is
missing. As always we can read these objects’ properties and methods using vlax-dump-
object. Information about Excel ActiveX Object properties and methods can also be found via the
Object Explorer in the Excel Visual Basic Editor. The steps to reach the Cells collection are:
1. From the Application object we obtain its "Workbooks" property. This property returns the
Workbooks collection that includes all the open Excel Workbooks in the current instance of
the Application. A reference to this object is assigned to the wbook-collection variable.
A Workbook is the Excel Document. In our program we want to create an Excel XLS
Document named as the current drawing.
2. If a Workbook by that name is open in the current Excel session it will be retrieved using the
Workbooks collection's Item property. In case it does not exist an error will be thrown and
this will be used as the condition for adding a new Workbook to the collection and saving it to
the drawing's folder. Although the Workbook object has a Name property, it is read-only. To
name a Workbook we must use its SaveAs method. This method's arguments are shown in
Table 25.1. The Workbook will be assigned to the wbook variable.
3. From this Workbook we'll obtain through its "Sheets" property the collection of all the
Sheets in the Workbook, assigning it to the sheet-collection variable.
FileFormat XlFileFormat enum Specifies the file format to save. See Table 25.2.
:vlax-true or
ReadOnlyRec If True displays a message recommending that it be opened as read-only
:vlax-false
:vlax-true or
CreateBackup True to create a backup file.
:vlax-false
XlSaveAsAccessMode
AccessMode Workbook access mode. See Table 25.3.
enum
DISCONNECT-EXCEL function.
Due to the importance of this process, a call to the vlax-release-object function is also
included in the app-err error handling procedure (Listing 25.3), in order to avoid that in case of an
error this object would not be released. Before calling this function the error handler checks that the
object exists and that it has not been released. If vlax-release-object were not employed, the
only way to release Excel would be to quit AutoCAD’s session. Keeping the link has consequences
not only for AutoCAD. Any operation in Excel without releasing this link may in some cases make the
system unstable. The app-err function replaces the default error handler represented by the symbol
*error*.
(defun app-err (msg)
(if
(and excel-app
(not (vlax-object-released-p
excel-app)))
(vlax-release-object excel-app))
(prompt msg))
Prior to Release 2000, redefining the standard *error* function required saving the original
function assigning it to a variable in order to restore it on ending the program. With Visual LISP
setting a custom error function becomes simpler, because we can just declare *error* as a local
symbol in the function parameters list where a custom error routine is used. Thus, the scope of this
function is limited to where it is declared as local. Once it concludes, *error* will regain its
default value or any other that had been assigned globally. For this reason *error* appears as a
local variables in the list->excel function.
The connect-excel and disconnect-excel functions are called from the list->excel
function (Listing 25.4). This function also replaces the standard error routine by the app-err
routine and calls the vl-load-com function that will load the necessary vlax functions. It
receives as arguments the name of the block and the list of data with which the spreadsheet will be
filled.
Writing to the Worksheet itself is included in the process-table function which receives as
input a list with the structure explained above. The sequence of operations performed by list-
>excel is:
PROCESS-TABLE function.
Writing data to the Excel spreadsheet is performed by the process-table function (Listing 25.5).
In this function, as in the following routines, an extensive use of LISP’s list processing functions
foreach, mapcar and apply is made.
T he process-table function in turn calls two auxiliary functions defined for this program:
data->cell and process-row.
The argument received is an association list with the characteristics described above represented by
the lst argument. The tasks accomplished by process-table can be described as follows:
This function (Listing 25.6) is in charge of filling each row. It receives from process-table the
list row and the integer numrow. The process-row function performs the following operations:
Data is written to each cell by the data->cell function (Listing 25.7). This function is conceived
as a utility function, which can be used in any program that must carry out this operation. It takes as
arguments the row and column indices (as integers) that determine the cell in which to write, and the
value to be written.
This value can be any of the Visual LISP supported types. The cell’s value is a property of the Excel
Range object which was obtained in the connect-excel function and assigned to the variable
excel-cells. The Visual LISP functionvlax-put-property can then be applied. Note that
the value’s string nature is ensured by the vl-princ-to-string function.
(defun data->cell (row col value)
(vlax-put-property
excel-cells
"Item"
row
col
(vl-princ-to-string value)))
The dialog’s main component is a list_box tile that displays the block names that exist in the
drawing. Whether the block has attributes is indicated by adding the text "ATTRIB" to the
corresponding row. The arrangement of these texts as a column to the right of the list box is achieved
by inserting a tab character "\t" between the block name and the text "ATTRIB ", and assigning
the to the list_box tile’s tabs attribute a value of 33, being 40 its total width. This list box allows
only the selection of a single block name, which is determined by setting its multiple_select
attribute to false.
The other dialog components are the OK and Cancel buttons that are part of ok_cancel predefined
subassembly and a line of text for messages, also predefined as the errtile component. The
complete code file for the attributes.dcl file is shown in Listing 25.8. This file must be located in a
folder included in the AutoCAD support files search paths. As for earlier examples in this book we
can save it to a folder named DCL within the Visual Lisp Projects folder that we added to the
search paths using the Options dialog box.
attributes : dialog {
label = "Attribute extraction"; // Dialog title
key = "title";
: list_box { // Start list
key = "block_list"; // Component ID
label = "Block to process:";
allow_accept = true; // Double-click
multiple_select = false; // Single select
width = 40;
height = 12;
tabs = "32"; //Tab
} // End list
ok_cancel; //Accept & Cancel
errtile; //Messages
}
Listing 25.8. Code for the dialog box.
Once the dialog box is displayed, the user will decide the action to execute: extracting the attributes
of one of the blocks or exiting without performing any operation. The procedure can be summarized
as follows:
1. The dialog box specified by its dialog and file names is loaded. Although these names are the
same in our example it does not have to be that way. In one DCL file several different dialogs
can be defined. This process, with the required error handling is done by the display-
dialog function (See Listing 22.2) that returns T if the dialog has been successfully loaded and
nil otherwise.
2. If successfully loaded, two initialization processes will take place:
a. Assigning values to the dialog's tiles. A tile's value can be predefined in the DCL file code,
employing its value attribute. But in our case the list should display the blocks in the current
drawing. These values must be assigned at run-time. This is done in two steps:
i. The list of blocks is retrieved by the read-blocks function and assigned to the
block-list variable.
ii. The fill-list function is called with the list_box tile's key and the list of
blocks as arguments.
b. After filling the list, the callback functions must be assigned. Callback functions will be
called when the tile's mouse-click event is raised. The list_box, the OK button and the
Cancel button will have callback functions. Callback actions are defined by AutoLISP
action_tile function. The assigned callback actions are:
i. list_box: Calls the check-attributes function using the value of the selected
line represented by the $value variable. (this variable is created automatically by
the system). If the selected line represents a block without attributes, a message
indicating this is printed in the errtile component.
ii. OK Button: Calls the extract function which receives the list_box selected
line's position as a zero-based index that is returned by the get_tile function. Note
that in this case we cannot use $value because that variable contains the value of the
selected tile, in this case the OK button. To get the value of another tile we use
get_tile with the key attribute as argument.
iii. Cancel button: Its only action is to close the dialog by calling the done_dialog
function.
3. Displaying the dialog: The start_dialog function displays the dialog. This function remains
active until done_dialog is called to close the dialog. Once the dialog has been closed,
unload_dialog unloads it, freeing the memory it occupied.
Auxiliary functions.
The auxiliary functions called from C:EXCEL-ATTRIBUTES are described below.
DISPLAY-DIALOG function.
This function, which was defined in Chapter 22 (Listing 22.2) will verify that the DCL file is
available and that the dialog definition can be loaded, alerting of any errors found. If one of these
messages appears, we must make sure the DCL file has been placed in the appropriate folder and that
it contains no syntax errors. This is a function that can be used for any dialog.
If all goes well, the list_box shall be filled by the fill-list function (Listing 25.10) with the
names of the blocks found in the drawing. These names are obtained by the read-blocks function,
which is somewhat complex and will be explained later. For the moment being we can say that the
names are stored in an association list composed of dotted pairs assigned to the block-list
variable. The procedure for filling a DCL list_box is the usual one:
1. A call to the start_list function with the tile's key as argument: (start_list
"block_list").
2. A call to the mapcar function applying 'add_list to each list member. We will use another
call to mapcar nested in the previous one, to create a string that will convert the dotted pair
("BLOCK" . "ATTRIB") into the string "BLOCK\tATTRIB".
(mapcar '(lambda (term)
(strcat (car term) "\t" (cdr term)))
text-list)
3. If block-list returns an empty list, rather than filling the list_box an error message will
be written in the errtile component:
(set_tile "error" "No blocks in the active drawing")
4. The list writing process finishes with a call to end_list.
(defun fill-list (list-tile block-list)
(start_list list-tile)
(mapcar 'add_list
(mapcar '(lambda (term)
(strcat (car term)
"\t"
(cdr term)))
block-list))
(end_list))
The callback function check-attributes has been assigned to the list_box. This action will
be triggered when the user selects a row. We must also note that besides this action that is triggered
by a single mouse click, the double-click event will also trigger the OK button's callback action as
defined in the DCL code with the expression allow_accept = true;.
The $value argument passed to the check-attributes function (Listing 25.11) contains the
zero-based index of the row on which the mouse is clicked. The list box’s management requires
keeping the original block-list list in memory. To identify the block name on the selected row
check-attributes applies the nth function to retrieve the term of the list that occupies the
position specified by the index in $value:
The returned value is a String so it must be converted to an Integer using the atoi function.
(defun check-attributes (value)
(if
(not
(equal
(cdr (nth (atoi value) block-list))
"ATTRIB"))
(set_tile
"error"
"The selected block has no attributes")
(set_tile "error" "")))
The dotted pair’s second term is then checked to verify if it is the "ATTRIB" string. If not, a
message is displayed indicating in errtile that "The selected block has no
attributes".
Clicking on the OK button activates the attribute extraction process. The action for this button is
defined in the extract function which receives the selected row's index, this time obtained by
get_tile:
(action_tile "accept"
"(extract (get_tile \"block_list\"))")
This function (Listing 25.12) verifies that the selected block has attributes and in that case calls the
list->excel function to export them. It proceeds as described below:
1. Check that block-list exists. If so the value for the selected row in block-list is
assigned to the variable selection. The value we obtain from the list_box is only the
index that indicates the selected row. That row's text is not returned. To retrieve the text we have
to keep the list that was used to fill the list_box (the argument passed to fill-list).
Having the index and the original list, the selected row's text is retrieved using the nth function.
As the value returned by a DCL dialog box is always a String, it must be converted to an Integer
using atoi before passing it to nth.
2. Verify that the dotted pair's second term is the string "ATTRIB". If this verification succeeds,
the list->excel function is called with the value returned by the sel-block function as
argument. In case the second term is not the "ATTRIB" string, the function does nothing.
(defun extract (value / selection name)
(if block-list
(setq selection (nth (atoi value)
block-list)))
(if (= (cdr selection) "ATTRIB")
(progn
(set_tile
"error"
"Processing, please wait...")
(setq name (car selection))
(list->excel name (sel-block name))
(done_dialog))))
This function that was proposed in Chapter 22 (Listing 24.4) receives as argument the block name
assigned to the variable name by the extract function. A selection set is created based on name
including all of that block’s insertions in the current drawing. Then this selection set will be
processed to build an association list with the structure required by list->excel. This list shall
include, in addition to the attributes with their tags, the name of the Layer in which the block is
inserted and its insertion point coordinates.
READ-BLOCKS function.
1. The Blocks collection can be retrieved from the Document's Blocks property using the
vla-get-Blocks function. The Blocks collection is traversed using the vlax-for
function in a way very much alike to traversing a list with foreach.
2. From each of the objects in the collection which we retrieve sequentially, we obtain its
"Name" property: (vla-get-Name obj).
3. But we are not interested in all the objects recovered from the Blocks collection. We only
want the names of user-defined blocks. There are a number of possible Block objects we must
exclude: the so-called anonymous blocks generated and managed by AutoCAD that are
distinguished by having an asterisk "*" as their name's first character (that will be detected
using as wcmatch pattern the escaped sequence `** in which the initial reverse quote makes
the first asterisk to be read literally while the second asterisk acts as a wildcard which will
match any character sequence) , the user-defined blocks dependent on Xrefs identified by
including the ASCII vertical bar (pipe) character "|" (ASCII 124 ) and the Xrefs themselves
(identified by their IsXRef property). Hence the conditional included in the code used to build
the block list. For the first two cases the wcmatch function is used with the string "`**,*|*"
as pattern.
4. But only those blocks actually inserted should be considered. This is checked by selecting them
with the expression:
(ssget "X" (list '(0 . "INSERT") (cons 2 name)))
5. If there are Inserts for this block we must check whether the it includes constant or variable
Attribute definitions. This is the has-attributes? function's purpose. Once all this has
been verified, we add the bock`s name to a list that will be used to populate the dialog's list box.
For those blocks with attributes the text "ATTRIB" will be added to the right of the list.
(defun read-blocks (/ name blk-list)
(vlax-for obj
(vla-get-blocks *aevl:drawing*)
(setq name (vla-get-name obj))
(if
(and (not (wcmatch name "`**,*|*"))
(equal (vla-get-IsXref obj)
:vlax-false))
(if
(ssget "X"
(list (cons 0 "INSERT")
(cons 2 name)))
(setq blk-list
(cons
(cons
name
(if
(has-attributes?
obj)
"ATTRIB"
""))
blk-list)))))
(setq blk-list
(vl-sort
blk-list
'(lambda (n1 n2)
(< (car n1) (car n2)))))
blk-list)
HAS-ATTRIBUTES? function.
This will require examining, one by one, the block definitions found. A Block Definition is also a
Collection. In this case composed by entities among which Attribute Definitions may be found.
The has-attributes? function (Listing 25.14) traverses each Block Definition using vlax-
for assigning T (true) to result in case an attribute definition (an object whose "ObjectName"
property is "AcDbAttributeDefinition") is found.
(defun has-attributes?
(blkdef-obj / result)
(vlax-for obj blkdef-obj
(if (equal (vla-get-ObjectName obj)
"AcDbAttributeDefinition")
(setq result t)))
result)
Table 25.1 includes all the functions used in this application. We must remember that some of these
functions have been explained in previous chapters so their code is not in this chapter’s listings. We
have distributed the functions according to their purposes in three different source code files. This
distribution is justified by the possibility to reuse code in other applications pursuing similar goals.
ATTRIBUTES.DCL
Function: Listing:
DCL code Listing 25.8
25.5 Summary.
We believe that, once explained, it may surprise how simple it is to set up a connection between
Visual LISP and an application like Excel in order to export drawing data directly to the spreadsheet.
In fact, most of the code in this application has nothing to do with this. It deals with data extraction
and GUI management. I’ve waited to have an application like this, of a certain complexity to address
a pending topic: compilation. To it we will devote the next chapter.
Exercises.
Exercise 1.
Other alphanumeric information that, as studied in the previous chapter can be stored in XDATA,
XRECORD or LDATA format can also be exported to spreadsheets. And regarding geometric
information, it would be possible to do it with almost all the graphic entity’s properties. Precisely the
list->excel function requires an association list such as the one returned by entget. As an
exercise we could define a function able to export these data types. For example, the coordinates for
the vertices of polylines.
Chapter 26
VLX: The Visual LISP Executable
In the previous chapter we completed the code for a program that exports attribute values of the
selected block to an EXCEL spreadsheet. This program includes three LISP source files and one with
the DCL code for a dialog box. Managing a project of some complexity like this one can profit from
the tools provided by the Visual LISP Integrated Development Environment (IDE).
The source code files can be structured as a project. This allows us to open any file by double-
clicking in the project window. It also enables searching for expressions in all of the project files,
and loading all the source code files at the same time. But the greatest advantage in applications like
this one is the possibility packing it for its distribution as a single compiled file, including the dialog
interface.
It can be run either from the VLISP console or from AutoCAD’s command line (provided the Visual
LISP IDE has been previously loaded). The arguments it requires are:
'Mode (observe the single quote) that represents the desired compilation type:
'st standard mode.
'lsm optimize and link indirectly.
'lsa optimize and link directly
"source-filename" the name of the LISP source code as a string, if the extension is not
specified, LSP is assumed.
"output-filename" the name for the compiled output file. This argument is optional, if not
specified the output file name will be the same as the source code file with the extension FAS,
and will be located in the same folder.
Optimization Modes.
The results of the compilation process are at least two. First, it obviates the translation to the LISP
virtual machine language that takes place every time the AutoLISP source code is loaded. The FAS
file resulting from the compilation contains instructions directly understandable by the system, with
no need to further code interpretation. Besides saving time (hence the FAS extension, derived from
FASt loading), the code obtained is understood by the machine but not by snoopers. This would be
achieved with the standard mode.
The other optimization modes make programs even more efficient, marking a difference as they grow
in size and complexity.
They allow:
Linking function calls so direct references to the compiled function are created, instead of
invoking the symbol that represents the function, which improves performance and avoids that a
redefinition of the symbol may affect the program's behavior.
Discarding the function names to make the compiled code more secure, while reducing the
program size and loading time.
Discarding local variables names directly linking their references, gaining even more speed and
achieving a further reduction in size.
Not all can be applied in every case. Some degree of optimization can cause errors.
The project maintains a list of source files and a set of detailed compilation rules, allowing to:
Check which of the application's LSP code files have been modified so they are automatically
recompiled.
Open the listed files with a mouse click.
Search text strings in all the files that make up the project.
Link directly to the appropriate parts of the code even if they belong to different files.
All the project management functions are integrated in the Project Window that displays the list of
files, a button bar at the top (see Figure 26.1) and a contextual menu which is reached by pressing the
right mouse button (see Figure 26.2).
The other files generated by the IDE and the compiler are described in Table 26.1. To hold these files
three additional subfolders will be created within the ATTRIBUTES folder, with the names FAS,
TMP and VLX. This process is the same followed in the Tutorial from Volume1, Chapter 2. But the
compilation options will now be examined in more depth.
Location and names of the LSP files and compilation parameters and
.PRJ Project definition
rules
.GLD Global declarations Detailed settings for a more precise compilation control
ATTRIBUTES/TMP .OB Object Code AutoLISP compiled code used in building the FAS files
AutoLISP compiled
ATTRIBUTES/FAS .FAS Compiled programs that can be executed or packaged into VLX modules
code
Defines the files and options used to build the VLX application using the
.PRV Application's Make file
Application Wizard
To build our project we have the New Project... option from the Project menu. When selected, a
dialog box is displayed in order to select the project folder and its name. We will select the
ATTRIBUTES folder in the structure explained above.
Figure 26.4a. Project Properties: Files Tab.
Generation Options.
We will strive for the highest optimization possible. The options we will use involve a greater risk,
but they can provide the highest efficiency in our application. The options we will choose are shown
in Figure 26.4 (Right). They are:
Merging files in a single module, i.e., one FAS file. This will speed loading and avoid having to
manage three different files.
Choosing Internal link mode, so the compiler tries to resolve all explicit function calls by
referencing the function's definition in memory, dropping then the function name altogether so
that the function becomes invisible to users.
Selecting Localize variables, so the compiler will also remove all local symbol names directly
linking their references if possible, rather than a symbol representing the variable's memory
address.
To prevent as far as possible errors due to this high degree of optimization, we also select Safe
optimize that instructs the compiler to reject certain optimizations that may be problematic.
In Message Mode we select Full reports to ensure we have all the information about the
compilation process including errors, warnings and compiler statistics.
In this tab we must also indicate the folders where FAS and temporary files will be stored. For
now we won't use the Edit Global Declarations… button.
By selecting the OK button we will have completed the project definition. Before compiling the
project we must remember that Visual LISP ActiveX extension functions (vlax-... etc.) will not
be available without evaluating (vl-load-com).
Once these functions are loaded, we can compile the project, selecting the Build Project Fas button
on the Project window’s toolbar. Examining the messages in the Build Output window can be very
instructive. We should consider especially the lines highlighted in blue which show the Safe
Optimize warnings (see Figure 26.5).
Figure 26.5. Messages in the Build Output window.
Double-clicking on any highlighted line and will open the source code file containing the related code
snippet. These warnings make up most of the Build Output window’s content. Detailed compilation
instructions can be set by creating a GLD (Global Declarations) file. These compiler directives
functionality is not documented and unless there is an error that can be traced to a specific aspect,
their use is not recommended. To modify the compiler options the Edit Global Declarations...
button can be used to create a GLD file for the project.
The FAS module must be loaded to test it. If we have organized the project as indicated, with the
Visual Lisp Projects folder in the AutoCAD search paths, we can load the FAS module evaluating the
expression:
(load "./ATTRIBUTES/FAS/EXCEL-ATTRIBUTES.fas")
We can also use the Load Project Fas button on the project window's toolbar.
Although compilation as a FAS file can provide effectiveness and security to our program, we still
have to rely on an external DCL dialog file that must be installed in a location where AutoCAD can
find it. This is something that can be solved by packaging the application as a Visual LISP
Executable, i.e., as a VLX file.
26.3 Namespaces.
Up to Release 14 AutoCAD worked as what is known as Single Document Interface (SDI). To open a
new drawing the current one should be closed. The concept of namespaces was introduced when
AutoCAD 2000 enabled the possibility of working as a Multiple Document Interface (MDI). Its
purpose is to prevent interference between programs running on different drawing windows. A
namespace is a LISP environment that contains a set of symbols (variables, functions, etc.). Each
drawing open in AutoCAD has its own namespace. LISP objects defined in a document's namespace
are isolated from those defined in other drawing. Sharing information between different namespaces
is possible but it must be done explicitly.
To make a function defined in a separate namespace VLX application accessible from the drawing's
environment, it must be exported explicitly by calling the vl-doc-export function. This function
takes a single argument which is the symbol that identifies the function's name. The vl-doc-
export expression must be invoked before the defun expression where the function is defined. An
exported function can be imported by another separate namespace VLX application using the vl-
doc-import function.
To determine which functions have been exported from a separate namespace application vl-
list-exported-functions can be used. It requires as argument a string with the name of the
application whose exported functions must be checked.
An application compiled as a separate namespace VLX can access variables defined in the drawing's
namespace using the functions vl-doc-ref and vl-doc-set functions. The vl-doc-ref
function copies the value of a variable defined in the drawing's namespace. It requires a single
argument which is the symbol that identifies the variable to be copied. Assuming the existence of a
variable named point-list, its value is copied evaluating the expression (vl-doc-ref
'point-list). When called from the document's namespace it produces the same results as
eval.
The vl-doc-set function assigns a value to a variable in the drawing’s namespace. It requires two
arguments: the symbol that identifies the variable and the value to assign.
The following expression assigns a list of three real numbers to the variable pt:
(vl-doc-set 'pt (list 100.0 250.0 10.0))
When evaluated from the document's namespace it produces the same effect as setq.
If the value of a variable defined in the VLX application’s namespace must be copied to the
namespaces of all open drawings the vl-propagate function can be used. The following
expression sets the value of the variable website in the namespaces all open drawings:
(setq website "http://www.togores.net/")
(vl-propagate 'website)
The effect of vl-propagate is not limited to open drawings. The variable will be automatically
copied to the namespace of all drawings to be opened during the current AutoCAD session.
Visual LISP provides a namespace that is not linked to any drawing or any VLX application. This
namespace is known as the blackboard namespace. Its purpose is to establish and reference variables
that can be used by any drawing or VLX application. The vl-bb-set function is used to set a
variable and vl-bb-ref to retrieve the assigned value. As in previous cases, these functions
require the variable name as a symbol preceded by quote. Variables with the same name and
different values can coexist in the drawing and blackboard namespaces.
_$ (setq test "This is in the drawing's namespace")
"This is in the drawing's namespace"
_$ (vl-bb-set 'test "This is in the blackboard namespace")
"This is in the blackboard namespace"
_$ test
"This is in the drawing's namespace"
_$ (vl-bb-ref 'test)
"This is in the blackboard namespace"
_$
Error handling.
Applications that have their own namespace can define *error* functions using vl-exit-
with-error a nd vl-exit-with-value to transfer control to the drawing's *error*
function. With vl-exit-with-error a string informing about the error's nature can be passed to
the document's *error* function. When it is executed the stack is unwound, and control returns to a
command prompt. The vl-exit-with-value function can be used to return a numeric value to
the program that called the VLX.
Trying to load an already loaded separate namespace VLX application will raise an error. Whether a
separate namespace VLX application is loaded can be verified using the vl-vlx-loaded-p
predicate. These applications can be loaded with the same load function used for .LSP and .FAS
files.
The vl-list-loaded-vlx function returns a list with the names the separate namespace VLX
files associated with the current document. This function returns only the file name, without extension
or path. The vl-unload-vlx function unloads a separate namespace VLX application. It receives
as argument the application’s name as a string, also without extension or path. If the application is not
loaded this function raises an error.
An application that operates in its own separate namespace avoids this risk.
The second of these variants is safer allowing the application to be used in any environment. For this
we will create a new LSP file with the code shown in Figure 26.6, the same included in the excel-
globals.lsp file, but in this case there is no need to protect them with a pragma expression.
Figure 26.6. EXCEL-GLOBALS.LSP file code.
This file is not going to be added to the project, as it is only necessary if we create a separate
namespace VLX application.
The compiled VLX file includes the DCL dialog definition, simplifying the application's distribution.
The application can be loaded with
(load "./ATTRIBUTES/VLX/EXCEL-ATTRIBUTES")
provided that the application folder is the one that has been indicated in this tutorial.
26.5 Summary.
This chapter forms a unit with the previous one, showing the steps to build a compiled application,
from our source code. This application although simple, meets all the requirements expected of a
professional job. At this point the reader will have accumulated sufficient knowledge to venture into
the development of their own ideas.
Exercises.
The topics studied in this chapter shall be from now on the finishing stage in or applications
development. Whether as a FAS file, or in cases that merit it as a VLX application, we can resolve
from now on that not a single line of our work is distributed as source code.
This does not mean we should not share our knowledge. What keeps the AutoCAD users community
alive is precisely such a generosity and willingness to help others who undertake this rewarding task
of programming. A good example is the forums on the Internet like those in
autodesk.autocad.customization and theswamp.org.
But now the programs we generate, whether to distribute it commercially, for use within our company
or simply to make everyday life easier to us, will have that touch of professionalism for which we
can feel proud.
Chapter 27
OpenDCL
The DCL language has extremely interesting features. The procedure used to control dialog structure
using the column and row containers is comparable to the containers and Layout Managers in Java,
which undoubtedly is far better than the way Visual BASIC forms are programmed. For many
applications, its performance is more than enough. This can be verified just by checking the many
DCL files in a typical AutoCAD installation. ButDCL is limited to modal dialog boxes and can only
use a few of the wide range of widgets available in the Windows graphic user interface.
Having a truly “visual” environment for their design that allows on-screen placement is also missed.
This second aspect, given the extreme simplicity of the DCL language, is not so problematic.
This chapter will be devoted to examining OpenDCL, an alternative that even if it does not meet all
the requirements we would wish to find in a tool like this, is currently the most practical solution for
the Visual LISP programmer wishing to enrich the graphical user interface beyond that which DCL
offers.
A Non-Modal Dialog is required, i.e., a dialog that allows working in AutoCAD despite the
dialog box being open.
The use of controls (widgets) not available in DCL.
This chapter will discuss the operation of OpenDCL developing an application with these
characteristics.
The downloaded package includes an installer that creates the application’s folders and installs the
necessary files. The OpenDCL runtime module is automatically registered for all versions of
AutoCAD installed in the host computer.
Once installed, the OpenDCL development environment may be started as a stand-alone program by
double-clicking the icon created on the desktop or the Start menu.
Events.
In DCL programming the action triggered by an event is defined by the action_tile function or
by the tile's action attribute, being limited to one event per tile. For example, a button tile raises
the mouse-click event and an edit_box only raises an event when switching to another tile.
However OpenDCL can monitor different events for the same control. The term control will be used
from now on to designate the dialog’s widgets. A PictureBox control can notify a Visual LISP
function such events such as mouse single or double clicks, mouse movements including pointer
coordinates and its entering or leaving the control’s area, key pressing and others.
Object: An object is a programming data structure that has properties (data) defining its
characteristics and methods (procedures) that specify the way they interact. An object recognizes
certain events it can respond to. All the OpenDCL controls and forms are objects.
Methods: A method is a function that can be used to retrieve or change data.
Properties: they supply the way to define the object's characteristics, such as size, position, font
or color used. Properties are also used in retrieving this data.
Events: user actions and program or operating system notifications. Examples include pressing a
key, a mouse click, etc.
Forms: A Form is a template used at runtime to create a dialog box instance superposed to the
AutoCAD graphic screen or docked to one of its sides. Forms are created and edited
interactively in the OpenDCL Studio, dragging controls onto a form and setting their design
time properties. OpenDCL can create applications using six different Form types.
Controls: reusable objects that provide parts of a program's GUI. An OpenDCL control is
usually analogous to a standard Windows control, such as a Label, a TextBox or a
TextButton. But some of its controls are more complex, made up by of several synchronized
Windows. In OpenDCL the forms are also controls.
Image List: An internal collection of images stored independently of their location. An Image
List is linked to the control using it. The TreeControl uses an Image List to specify the
image displayed with the tree's elements.
Picture Folder: A global ImageList, its images can be used for various controls that are able to
display them. Picture Folder images are numbered starting with 100 while the ImageList
numbering begins with 1.
Menu Bar.
The menu bar offers access to all the Editor's commands. In addition to the usual File, Edit, View,
Window and Help menus, it includes two which are specific to this program, the Projects and Tools
menus.
Toolbar.
The OpenDCL Toolbar includes the usual Windows program commands as well as a button for the
Picture Folder and a pull-down button used to add new forms.
Picture Folder.
To add images to GraphicButton or PictureBox controls, these images must be previously
loaded in the Picture Folder. This is where the images shown by these two controls are stored.
Images in ICO, BMP, JPG and WMF format can be added to the Picture Folder. The Project menu
option or the Toolbar button may be used to open the Picture Folder. Images that are assigned
directly to a specific control's Picture property in the Properties panel are also added to the
Picture Folder.
In developing the application that is discussed further on a series of numbers are referred to, being
those that were assigned to the images when adding them to the Picture Folder. This numbering
depends on the order in which images are selected. To add an image, the Add button in the Picture
Folder window must be used. Doing so displays a file selection dialog box. Using it we can browse
to the folder where the image is stored in order to select it. The other buttons are used to replace an
image (Change), Delete it, accept the changes (OK) or Cancel the operation.
Toolbox.
Controls from which dialog can be composed (See Table.27.1) are included in the ToolBox. They
are described below, specifying its DCL equivalent in case there is one. When the control selection
(arrow) button (See Figure 27.3) is selected one or more of controls can be picked for editing.
Multiple selections are useful when the same property value must be assigned to several controls.
Modal Dialog.
A Modal dialog has the focus of the application while it is open and does not allow any action
outside itself. For this reason Modal dialogs are easier to manage since AutoCAD’s state does not
change during the dialog’s lifetime. A modal dialog box in AutoCAD usually will not make any
changes until it is closed, typically using a button labeled OK. Modal dialogs return a status code that
is used by the application to decide which actions to take. OpenDCL can create the usual type of
Windows dialog boxes working in a modal fashion.
Modeless dialogs.
Modeless dialogs allow the user to operate outside their boundaries. Modeless dialogs present
additional difficulties that arise from the fact that our work is done in a multiple document
environment. For this reason, changes in the AutoCAD application’s state such as opening or closing
documents must be taken into account. In this case the fact that each drawing has its own AutoLISP
namespace must be considered. This means that when a new document becomes current the events’
callback code will no longer be available. OpenDCL can create five different types of non-modal
dialogs.
It is a standard Windows dialog box that can remain open while the user continues working in
AutoCAD. It is identical in appearance to the modal dialog box.
Control Bar.
A Control Bar form is a modeless dialog that can be docked to one side of the AutoCAD window.
Options Tab.
The Options Tab form is displayed as an additional tab in the AutoCAD Options dialog.
File Dialog.
This form allows creating custom file search dialog boxes based on the standard Windows file
dialog. It includes a FileExplorer control along with other custom controls chosen by the
programmer.
Palette.
The Palette form displays a modeless dialog box can remain floating or be docked to the AutoCAD
window. Pallets have a vertical title bar, and include the ability to collapse or expand automatically
in response to the cursor’s movement.
Label text
Displays a text that cannot be edited and only raises click and double-click events.
TextButton button
Button that triggers an action. It can only display text caption.
GraphicButton image_button
Button that triggers an action, but that can display an image (ICO, BM P or JPG). It may have two different border styles,
raised or flat, like in recent Windows applications. The DCL image_button can only display vector images (SLD).
TextBox edit_box
Similar to the M FC Edit box control, used both for displaying and entering text. It incorporates many data input control
filters for strings, multiline text, uppercase, lowercase or hidden (as for passwords) characters, AutoCAD symbols, integers,
linear and angular drawing units format.
CheckBox toggle
A small square box that is marked when selected.
OptionButton radio_button
A small circular box marked with a dot when selected. Usually forms part of a group, in which only one of the buttons may be
checked.
ComboBox popup_list
Displays a text box with a button that can be used to display a list of values as in the ListBox. The main difference with its
DCL equivalent is that there are a number of specialized ComboBox styles equivalent to the AutoCAD controls used for
colors, dimension arrows, Lineweights, and plot style names. Three additional styles are available: Combo (with editable text
boxes), Simple (in which the list appears under an editable text box) and DropDown (where the text is read-only).
ListBox list_box
Displays a list of text items which the user can select.
PictureBox image
Used for displaying a variety of images. A large number of methods are available for drawing on the control at runtime. The
DCL equivalent only supports vector graphics (SLD).
radio_column
OptionList
boxed_radio_column
The OptionList control displays a list of radio buttons. Unlike the Option button control, this list can be easily modified at
runtime.
Insert ActiveXControl No DCL equivalent.
ActiveX controls are Windows controls that expose their functionality through a COM interface. OpenDCL ActiveX Control
acts as a container for the actual control used. ActiveX controls must be registered (usually when the application is installed)
in the system which will be used. In addition to the OpenDCL control's specific properties, most ActiveX controls expose their
own properties, methods, and events.
Geometry: Assigns resizing rules for each control. The way controls will move when the form is
resized is defined by these rules.
Background and Foreground: Specifies a control's background or foreground colors.
Font: Selects a control's text font and size.
Button Styles: Specifies the GraphicButton control's presentation style.
ComboBox Styles: Some styles define the ComboBox control's behavior and others define the
information listed (AutoCAD colors, Lineweights, etc.).
TextBox Filter Styles: Assigns a predefined filter to the text box input.
Image List: Used for selecting the images displayed in the Tree control. Unlike the
PictureBox and GraphicButton controls, the Tree control requires that all the images
used are loaded on initializing the program. This is accomplished through the use of the
ImageList object. The wizard displays all the loaded images and allows adding or removing
items. All images added to the ImageList must be the same size.
Tabs/Pages: Defines the number of tabs displayed in a TabStrip control and their captions.
ToolTips: Sets the text, the formal characteristics and the style for a control's tooltips.
Establishing how to react to different events is the fundamental task of the AutoLISP code in an
OpenDCL application. The OpenDCL Studio aids in implementing event callback functions. The
events to which a control will listen are set in the Events tab by marking the checkbox beside the
event’s name. The name of the event’s callback function is shown in a text box and under it a function
template ready to be copied to the clipboard and pasted into the AutoLISP file. This Clipboard Copy
button can be used for this. The template function includes a call to the dcl_MessageBox function
that displays a message warning that the function does not yet contain the necessary code. The
function template can be written directly to the AutoLISP file via the Add to File button that is
displayed if Write Events to Lisp File is selected in the Tools menu.
Control Browser.
Another way to generate an event callback function’s template is the Control Browser, which also
provides context-sensitive reference for all the properties, methods and events. The Control
Browser is displayed by double-clicking a control in the forms workspace. When a property, method
or event is selected in the Browser the documentation on its use and syntax is shown. The Copy-To-
Clipboard button copies the function template to the clipboard.
Figure 27.6. Control Browser.
Project Window.
The Project window is a fundamental part of the development environment. In it all the project’s
forms are represented as a tree structure. This tree includes placeholders for the source code file’s
name and for a username and password in case the form’s code should be password-protected.
The application can be used to perform some Layer operations such as turning them on and off,
isolating selected Layers by turning off the others and changing the Layer colors. More operations
were not included to keep the code as simple as possible, but by creating more buttons and their
callback actions the functionality of AutoCAD’s native Layer command can be reproduced.
For this project a non-modal Palette form will be used. This form can be docked to one side of the
AutoCAD window and can also be shown or hidden automatically according to the cursor location.
Being a non-modal form is will be necessary:
To load the AutoLISP code for each open drawing, which could be done with an expression
included in the acaddoc.lsp file.
To foresee possible environment changes such as opening new files or changing the focus
between different open drawings in order to update the Drawings and Layers Tree view.
To consider the possibility that all drawings are closed, entering a no-document state. In this
case, the form should be closed.
In addition to this, resizing will be enabled showing how to keep a consistent arrangement of the
controls in the resized form.
OpenDCL follows the event-driven programming paradigm, so what happens in the user interface
triggers the action specified by that particular event's callback function. A typical OpenDCL
application includes in an associated LSP file:
To simplify the project's structure a LSP file should be created for each form, with the code
organized in such a way that the main function is placed first, followed by the auxiliary functions and
finally the event callback functions.
To create the new project, select New from the File menu. An Untitled project will be created, which
we will save as XLayer.odcl to the folder with that same name.
At the same time, using the Visual LISP Editor we will create a new file which will be saved with the
same name to the same folder. This nomenclature is not essential, but it can avoid confusion.
Another problem relates to the necessity of updating the form’s content according to the new
environment found when the active drawing changes. Some information must be updated only when
initializing the form but not when opening a new drawing. This would present no difficulties as this
task can be assigned to the Initialize event’s callback function. In other cases, as in the sample
application we will develop, the current drawing’s information must be reloaded each time we
switch between open drawings. Or at any given time all the drawings may have been closed while
AutoCAD is still running. This would require defining the callback actions for theDocActivated
and EnteringNoDocState events.
Modeless Dialogs are normally activated in the AutoCAD application context and not in the document
context. In that case the AutoLISP code running in the application context would have difficulties
when invoking document context dependent functions. Demonstrating a solution for this is one of this
chapter’s goals.
We will begin our project by adding a Palette type modeless form (Project>Add Palette). If we
select the new form we will see in the Properties tab (Properties/Events Pane) that its (Name)
property reads Form1. Using a more explicit nomenclature is advisable in order to avoid confusion,
especially in large applications that include several different forms. Controls are usually named using
a three-letter prefix that identifies the type of control. In this case we shall use the name frmPal.
Below (Name) we find the (VarName) property. This row is now empty. When a form or control
instance is created, OpenDCL sets the value of an AutoLISP variable to a handle used to reference it
in the project’s functions. If the (VarName) property field is not empty, that name is used for the
variable. Otherwise a variable name is generated by OpenDCL using the pattern:
<project-name>_<form-name>_<control-name>
This aims to ensure a unique name for the variable, given that will be global in the drawing's context.
Using names generated by OpenDCL leaving (VarName) empty is the safest practice. This will also
avoid errors when a control is renamed. Let's now review the form's properties.
The user should be able to resize it. For this reason we mark the checkbox next to the Allow
Resizing property. The caption then changes to True.
We are not interested in changing the Background Color property, which by default appears as
-24 (transparent). This property supports AutoCAD color index and RGB colors, or Windows
default color constants like this one.
Dockable Sides specifies to which side of the AutoCAD window the palette may be docked.
Due to this form's nature in which the vertical dimension will probably be the largest one, we
choose the left and right sides (0 - Left & Right).
Event Invoke is left as 0 - Synchronous. A Synchronous function call forces the dialog to wait
until the event handler returns, while in Asynchronous mode the dialog box code continues
running and the event handler is not executed until AutoCAD is ready to process commands. Any
events that have return values are always called synchronously (otherwise the return value
would be lost). When event handlers need to use the AutoLISP command function the Event
Invoke property must be set to 1.
A number of properties (Height, MaxHeight, MaxWidth, MinHeight and MinWidth) refer to the
form's dimensions. As we have decided that it can be resized, we are only concerned with two
of these properties, MinWidth and MinHeight. They will prevent that some components will not
fit in the form. Both will be set to 170. All these dimensions should be interpreted as pixels.
Finally we will set the Title Bar Text property to "XRef Explorer" so it will be displayed
as the form's title.
Figure 27.8a. Form's Properties.
Figure 27.8b. Form's Events.
All the form properties have now been set. Now we must define in the Events tab those to which the
form must react. Three events will be selected here: Initialize, DocActivated and
EnteringNoDocState.
When each of these events is checked the template for the event handler function is shown under the
AutoLISP Function to Call On Event label. This template can be copied by clicking on the
Clipboard Copy button and pasted in the XLayer.lsp source code file.
After running this command, we can use the Apropos tool to search for functions including
the dcl_ prefix. This way we can verify the number of new OpenDCL functions loaded in the
Visual LISP environment. These functions enable access to the methods and properties of
OpenDCL controls. These functions are defined as protected system symbols, which means
that they will also be colored blue when typed in the editor window, just like AutoLISP/Visual
LISP primitives.
The call to the OPENDCL command should be followed by a call to dcl_loadproject to load
the project and to dcl_form_show that displays the form. These functions will be grouped in the
C:XLAYER function (Listing 27.1) that will act as a new AutoCAD command.
(defun C:XLAYER (/)
(command "_OPENDCL")
(dcl_loadproject "./XLayer/XLayer.odcl" t)
(dcl_form_show XLayer_frmPal))
Before the form is displayed the message included in the default Initialize event's callback
function will be shown. This is the event that must be used to set the form's content before it is
displayed. Once we click on this message's OK button the Palette form will be displayed, anchored to
the left side of AutoCAD's window. Dragging it we can verify the minimum resizing limits. If other
drawing is opened the DocActivated event's message is displayed. And on closing all the
drawings the EnteringNoDocState event's message will appear. Surely we'll be surprised at
how scarce the code is. This is a characteristic of event driven programming. Most of the code will
appear in the event callback functions.
We must establish the properties that will ensure that the control’s relation to the Form is maintained
when it is resized in run time. The Control Property Wizard assists in setting the correct values.
Geometry.
This wizard is displayed by clicking in the Properties Pane the field to the right of the (Wizard)
property or by the control's contextual (right-click) menu. The control's position is defined in the
Geometry tab. This tab displays four drop-down buttons with the options shown in Table 27.2:
For those with experience in resizing forms in languages such as Visual Basic the way controls are
aligned in OpenDCL is innovative. To set a Visual Basic control's position we only have the
properties defining its size, Height and Width, and defining its position, Left and Top. When a
form`s size is changed the Resize event is triggered and according to the new Height and Width
values, the new Left, Top, Height and Width values for all its controls must be recalculated.
Bottom From Bottom Distance in pixels from the control's bottom edge of the lower edge of its parent window.
Left From Right Distance in pixels from the control's left edge to the right edge of its parent window.
Right From Right Distance in pixels from the control's right edge to the right edge of its parent window.
Top From Bottom Distance in pixels from the control's top edge to the bottom edge of its parent window.
OpenDCL avoids the need for programming all these calculations adding to controls the properties
described in Table 27.3.
When resizing the form only those logical properties described in Table 27.4 that are enabled (i.e.,
whose value is 1) are taken into account.
Use Bottom From The specified distance between the control's bottom edge and the parent window's bottom edge is
Bottom maintained.
Use Left From Right The specified distance between the control's left edge and the parent window's right edge is maintained.
The specified distance between the control's right edge and the parent window's right edge is
Use Right From Right
maintained.
The specified distance between the control's top edge and the parent window's bottom edge is
Use Top From Bottom
maintained.
The wizard provides a graphical interface for setting the logical properties described in Table 27.4.
The relationships between the selected control (shown in the wizard as an OK button) and its
container (represented by a form titled Example) are shown as dimension lines. For our
trcLayers control we will set the following options:
For Left Side Alignment we select [Offset from Left Edge]. The effect of this choice is that
when resizing, the Left property is always 0 pixels, as established at design time. The Use Left
From Right property is turned off, adopting 0 as its value.
For Top Side Alignment we select [Offset from Top Edge]. The effect of this choice is that
when resizing, the Top property will maintain the value of 0 pixels set in design time. The Use
Top From Bottom property is disabled.
For Right Side Alignment we select [Offset From Right Edge]. With this setting, the value of
the Right From Right variable, that when designing the form was set to 0 pixels, will be kept
when the form is resized. The Use Right From Right property is enabled, adopting 1 as its
value.
For Bottom Side Alignment we select [Offset From Bottom Edge]. This way, when resizing
the form, the distance between the control's bottom edge and the parent window's bottom edge
will be kept constant with the value defined for Bottom From Bottom, in this case 40 pixels.
The Use Bottom From Bottom property is enabled taking the value 1. Having established the
geometrical properties it would be advisable to save the project and run the main function
C:XLAYER to verify that the Tree control is resized correctly when resizing the form.
Figure 27.9. Geometry tab for the trcLayers control Properties Wizard.
Image List.
The other Control Property Wizard tabs are used to set the font type and size (Font tab), set the
images to be used (Image List tab) and define the control's help and warning messages (ToolTips
tab). As regards the font type and size no changes will be made. Where we will have some work to
do is in adding images to the Image List and setting up a Tooltip.
To identify the Tree control’s nodes we will use 14 x 14 pixel images. We can create them in an
image editing program or extract them from AutoCAD or other programs .exe or .dll resource files
using some of the programs available online. Or they could start as screen captures to be later
processed in any image editor. Numbers that will be used to identify them are assigned according to
the order in which the images are added to the list. Figure 27.10 shows the images used for this
tutorial, with the numbers assigned by the Wizard.
Tool Tips.
The Wizard's ToolTips tab can be used to define messages that are displayed when the pointer hovers
over the control. In this case we want to show a small box with the "Select layer" text. We want the
text in bold font-weight, so we type it in the Title edit box. The simple box style we want for the
tooltip is achieved by deselecting the Balloon style tooltip checkbox. To see how the message will
be displayed we can click the Preview button.
Figure 27.10. Images used in the Tree control.
Label is the text caption for the selected item. Key is the item's identifier. The Key argument is a
string if it was specified when creating the control, otherwise it is a unique identifier (handle) that
is automatically assigned. We will click on the Clipboard Copy button and paste the template in the
project's AutoLISP code file. For the time being we are finished with the Tree control, so we'll go
on to create the buttons that will launch the application's commands.
Picture Folder.
From the two types of buttons available in OpenDCL we choose the GraphicButton, to which we
can assign icons and tooltips indicating their purpose. The Palette will include five graphic buttons
that will be used for:
Since we will use images, we must use an object similar to the Tree control's Image List. That
object is used for that control exclusively. OpenDCL can create a Picture Folder object that can be
used by other of the form's controls.
The Project>View/Edit Picture Folder menu option displays the Picture Folder dialog box. It can
also be displayed by clicking on the Toolbar button with the camera icon. The images used in graphic
buttons are stored in the Picture Folder. Those we will be using are displayed in Figure 27.13. The
images are duplicated with slight hue and lightness differences that will highlight the button when the
pointer hovers over it. These differences will also reflect the selected Layers state. For example, the
Layers On/Off button will display a different image depending on this condition in the selected Layers
group.
Figure 27.13. Graphic Button Images.
The order in which they are added is important, since their PictureID numbers that will identify them
will depend on this order.
Graphic Buttons.
The way in which the Graphic Buttons are created is the same for all five. We will start with the
rightmost button, the one that will be used to update the Tree should any external reference be
attached or detached. To create this button a Graphic Button control is selected in the Toolbox
(Figure 27.14).
Once the control is selected, it is placed by clicking in the free space under the Tree control and
dragging to define a square with the desired size. This done, we change its (Name) property to
btn_UPDATE in the Properties Pane.
Geometry.
We must check that its Height and Width properties are both 32 pixels. As we want this button to
be located in the form's lower right corner and vertically centered in the free 40 pixels margin we
have at the bottom of the form, we will set the following properties:
Now we will resort to the Control Property Wizard to set the way this button will behave when the
form is resized. We want that the distance from the button to the form's right and bottom edges is kept
constant. Figure 27.15 shows the settings used to achieve this.
We will see in the Properties Pane that the properties Use Bottom From Bottom, Use
Left From Right, Use Right From Right and Use Top From Bottom have all been
set (value = 1).
Button Styles.
The Button Styles tab is where we define the properties related to the button's appearance. This tab
allows choosing between buttons with a raised border, a flat appearance, an XP visual style or no
border at all. As an alternative a series of predefined buttons like those used in AutoCAD can be
selected. We can also choose here the image to be displayed on the button among those we have
added to the Picture Folder. The image to be used when the mouse hovers over the button cannot be
defined in this wizard. It must be defined in the Properties Pane as the Mouse-Over Picture
property value.
ToolTips.
The button can display a message when the cursor hovers over it. We already saw this possibility
when defining the Tree control's properties. We will now explore other style options for these
messages. Selecting the Balloon style tooltip checkbox will display a cartoon-style "balloon" with a
stem pointing to the button. An image may be added; in this case it will be the same one that identifies
the button. The ToolTip's text can be multiline if it is entered in the Main Text edit box. It is also
possible to apply format to the text (bold, italics, underlines and color).
The Graphic Buttons must listen to the Clicked event. This is the event generated by pressing the
left mouse button when the pointer is over the object. By selecting it in the Properties/Events Pane's
Events tab a template for the callback function is displayed. This template must be copied to the
AutoLISP code file.
The other buttons are created in the same way. When creating each, its Clicked event’s callback
function template must be copied to the AutoLISP code file. The properties that must be set for these
buttons are specified in the Table 27.5.
Callback Functions.
Global variables.
In the event callback functions we frequently reference the application and the document objects. We
are counting on the file acaddoc.lsp to define the global symbols *aevl:acad* for the
application object and *aevl:drawing* for the document object. Since this is a modeless form,
these variables must be available in each drawing's context. As the condition of protected system
symbols has been assigned to these variables, they should not be redefined them in the program's
code.
Initialize Event.
The function template generated by OpenDCL Studio (see Listing 27.2) only includes an expression
that displays a message warning that the code for our function is still pending.
(defun c:XLayer_frmPal_OnInitialize (/)
(dcl_MessageBox
"To Do: code must be added to event handler\r\nc:XLayer_frmPal_OnInitialize"
"To do"))
To populate the Tree we begin by obtaining a list of all the names of the drawings attached as
external references (dwg-list function, Listing 27.3). These drawings appear as objects in the
drawing's BLOCK collection that is traversed using the vlax-for function. To differentiate
between normal blocks and external references, the block's IsXref property is checked. The dwg-
list function returns a list of all the external references. This list is sorted alphabetically and the
current drawing's name (retrieved from the DWGNAME system variable) is added as its first term.
(defun dwg-list (/ blocks-coll xref-lst)
(setq blocks-coll (vla-get-Blocks *aevl:drawing*))
(vla-get-Blocks
*aevl:drawing*))
(vlax-for block blocks-coll
(if (equal (vla-get-IsXref block)
:vlax-true)
(setq xref-lst
(cons
(vla-get-name block)
xref-lst))))
(setq xref-lst (acad_strlsort xref-lst)
xref-lst (cons (getvar "DWGNAME")
xref-lst)))
To obtain the drawing Layer list the Layers collection is traversed to extract the Name property for
each of the objects in the collection. The process is similar to the one used to iterate through the
blocks collection, sorting the list alphabetically. When it comes to XRef-dependent Layers, their
names are composed by the drawing's name and the Layer's name, separated by the pipe "|"
character. The Layer name extraction is performed by the layer-list function (Listing 27.4).
(defun layer-list (/ layer-coll lay-lst)
(setq layer-coll
(vla-get-Layers *aevl:drawing*))
(vlax-for lyr layer-coll
(setq
lay-lst (cons (vla-get-Name lyr)
lay-lst)))
(acad_strlsort lay-lst))
To create the drawings/Layers association list we use the dwg-layers function. This function takes
as arguments the drawings list returned by dwg-list and the list of Layers returned by layer-
list. It operates as follows:
Finally we will have two lists, res1 and res0, the first including the drawing's Layers and the
second one with the Layers that depend on external references. These two lists are joined using the
append function. This way we ensure that the first item in the tree corresponds to the current
drawing, followed by the XRefs.
(defun dwg-layers (drawings layers / current
pos tmp0 tmp1 res0 res1)
(setq current (getvar “dwgname”))
(foreach drawing drawings
(foreach lyr layers
(cond
((setq pos
(vl-string-search “|” lyr))
(if
(wcmatch lyr
(strcat drawing “|*”))
(setq tmp0
(cons
(substr lyr
(+ pos 2))
tmp0))))
(t
(if (= drawing current)
(setq tmp1 (cons lyr tmp1))))))
(cond (tmp1
(setq res1
(cons
(cons current
(acad_strlsort tmp1))
res1)))
(tmp0
(setq res0
(cons
(cons drawing
(acad_strlsort tmp0))
res0))))
(setq tmp0 nil
tmp1 nil))
(append res1 (reverse res0)))
Listing 27.5. Function that creates the drawings and Layers association list.
The list returned by dwg-layers is passed to the make-nodes function (Listing 27.6). This is the
function that populates the Tree control. To populate it make-nodes must use LISP functions that
are specific to the OpenDCL API. This is an example of how Visual LISP functions can be enriched
by those added by other applications.
(defun make-nodes (tree / res_root res_xref
id-img sel-img res_layer)
(setq
res_root (dcl_tree_addparent
XLayer_frmPal_trcLayers
“All”))
(dcl_tree_setitemimages
XLayer_frmPal_trcLayers
res_root
0
1)
(foreach refx tree
(setq res_xref
(dcl_tree_addchild
XLayer_frmPal_trcLayers
res_root
(car refx)))
(if (= (getvar “dwgname”) (car refx))
(setq id-img 2
sel-img 3)
(setq id-img 4
sel-img 5))
(dcl_tree_setitemimages
XLayer_frmPal_trcLayers
res_xref
id-img
sel-img)
(foreach lyr (cdr refx)
(setq res_layer
(dcl_tree_addchild
XLayer_frmPal_trcLayers
res_xref
lyr))
(dcl_Tree_SetItemImages
XLayer_frmPal_trcLayers
res_layer
6
7)
(dcl_tree_expanditem
XLayer_frmPal_trcLayers
res_root
1)
(dcl_tree_selectitem
XLayer_frmPal_trcLayers
res_root))))
1. The Tree's root node is created by the dcl_Tree_AddParent function. This function takes
as its first argument the name identifying the control that OpenDCL created automatically. In this
case this identifier is XLayer_frmPal_trcLayers. The other argument is that node's label
as it will appear in the Tree control, in this case the word "All". When the root node is
created, the function returns its handle which is assigned to the res_root variable.
2. After creating the root node we assign the images by which it will be identified. They will be
two, because we want one to represent its unselected state and the other one for the selected
state. This is done using the dcl_Tree_SetItemImages function. This function receives as
arguments the Tree control's handle, the value returned by the function that creates the node and
the ImageList index numbers.
3. Now we proceed to create the child nodes. This is done calling repeatedly the
dcl_Tree_AddChild function, which receives in addition to the arguments that
dcl_Tree_AddParent received, the root node's handle which was assigned to res_root.
The nodes for drawings are created in an initial foreach loop that traverses the list creating a
root node's child for each sublist in the list it receives as the tree argument. Within this
foreach a second foreach loop is nested that traverses the cdr of the list assigned to the
variable xref. This second foreach is the one which creates the Layer nodes by passing to
dcl_Tree_AddChild the handle returned when the drawing node was created assigned to
the variable res_xref. The creation of both the drawing and Layer nodes will be followed by
the assignment of the images for the node's two states, selected and unselected.
4. Once this double foreach concludes there only remains to define how the Tree will be
displayed when starting the application. We have opted for expanding the root node to display
the drawing nodes. This is set using dcl_Tree_ExpandItem. Finally we will preselect the
root node using the dcl_Tree_SelectItem function. This will be noticed by its icon color
and by its highlighted label.
The draw-buttons function (Listing 27.7) affects the Graphic Button used for turning the
selected Layers On and Off. This button changes its image depending on the Layers in the selected set
being all on, all off or some on and some off. To do this, three functions will be used, with the
intention of simplifying the program to make it more understandable.
1. The list of Layers returned by layer-list is traversed, checking for each Layer name, if it
matches the pattern received. In the case of the Initialize event's callback function, all the
Layers will match the wildcard pattern "*" received, but draw-buttons will be called
every time a new selection is made, so the pattern used will vary.
2. In case the Layer name matches the pattern, it checks the value associated with its entity list's
group code 62. If its value is negative, it means that the Layer is Off. In that case nil is added
to the active-lst list and if positive T is added.
CHECK-STATE function.
T he check-state function (Listing 27.8) examines the list received from draw-buttons
analyzing it to check:
BTN-ACT function.
This function assigns an image to the On/Off button (Listing 27.9). According to the value it receives
as the status argument, it will call the dcl_Control_SetPicture function assigning to the
button identified as XLayer_frmPal_btnACT the Picture Folder images 100, 101 or 102 to
i ts Picture property and the dcl_Control_SetMouseOverPicture function to assign
images 107, 108 and 109 to its MouseOverPicture property.
(defun btn-act (state)
(cond
((= state 1)
(dcl_Control_SetPicture
XLayer_frmPal_btnACT
100)
(dcl_Control_SetMouseOverPicture
XLayer_frmPal_btnACT
107))
((= state 0)
(dcl_Control_SetPicture
XLayer_frmPal_btnACT
101)
(dcl_Control_SetMouseOverPicture
XLayer_frmPal_btnACT
108))
(t
(dcl_Control_SetPicture
XLayer_frmPal_btnACT
102)
(dcl_Control_SetMouseOverPicture
XLayer_frmPal_btnACT
109))))
From the list’s first term the numerical value is obtained using atoi and assigned to the variable
num. In case the list is empty 1 is assigned to num. This is the number that will be used for the new
Layer State that will be created by the expression:
(layerstate-save
(strcat (itoa num) "_XLayer") (+ 1 32) nil)
The expression (+ 1 32) is a bitcoded mask that defines the properties to be restored: 1 for saving
the Layers’ On or Off value and 32 for saving the Layers’ color setting. The last argument nil
indicates that VPLAYER settings are not to be saved.
(defun save-state (/ num)
(if (setq num
(car
(vl-sort
(vl-remove-if-not
‘(lambda (l)
(wcmatch l
“*_XLayer”))
(layerstate-getnames))
‘>)))
(setq num (1+ (atoi num)))
(setq num 1))
(layerstate-save
(strcat (itoa num) “_XLayer”)
(+ 1 32)
nil))
Undoing Layer settings changes will be the purpose of the btnUNDO button. This will be done by
calling the prev-state function (Listing 27.11). This function retrieves the sorted Layer State
names created by the application as was explained for the save-state function and assigns it to
the variable states. As the current Layer State will be the highest numbered one, its name will be
retrieved from the first term of the states list and assigned to the variable current. The second
term of the list will be assigned to the variable previous. If there is a previous Layer State it
will be restored with a call to layerstate-restore and the current Layer State will be deleted
with a call to layerstate-delete. The Layer State when the Form is initialized will be saved
in order to have an initial state to which return.
As some of the layer modifications will require a REGEN in order to be displayed, this function
calls vla-Regen. If the current Layout is ModelSpace it is enough with a simple REGEN done by
supplying the acActiveViewport argument. In case it is one of the Paperspace Layouts a
REGENALL will be forced supplying the acAllViewports argument.
(defun prev-state
(/ states current previous)
(setq states
(vl-sort
(vl-remove-if-not
‘(lambda (l)
(wcmatch l “*_XLayer”))
(layerstate-getnames))
‘>))
(if states
(progn
(setq current (car states)
previous (cadr states))
(if previous
(progn
(layerstate-restore previous)
(layerstate-delete current)
(if
(= (getvar “CTAB”) “Model”)
(vla-Regen *aevl:drawing*
acActiveViewport)
(vla-Regen *aevl:drawing*
acAllViewports)))
(prompt
“\nNo previous state saved.”)))))
In order to the Layer States restoring functionality to operate the XLayer Layer States must be
initialized on starting the application. This is done by the init-states function (Listing 27.12)
that will be called from the Form's Initialize event's callback function. The init-states
function looks for Layer States with names matching "*_XLayer" and if any is found it is deleted.
Then the current state is saved with the name "0_XLayer".
(defun init-states (/)
(if (setq states
(vl-remove-if-not
‘(lambda (l)
(wcmatch l “*_XLayer”))
(layerstate-getnames)))
(mapcar ‘layerstate-delete states))
(layerstate-save
“0_XLayer”
(+ 1 32)
nil))
The Tree is populated during the form's Initialize event, but it must also be done on changing
the active document, within the DocActivated event's callback function. If the attached external
references are changed it will also be necessary to update its contents. This could be managed using a
reactor, but to simplify this example we will just have a button to do this. For this reason, it is more
practical to group this sequence of functions in a single one that that can be invoked whenever
updating the tree's content is necessary. This will be done in the start-view function (Listing
27.13).
(defun start-view (/ tree)
(setq tree (dwg-layers
(dwg-list)
(layer-list)))
(make-nodes tree)
(setq *XLayer* “*”)
(draw-buttons *XLayer*))
SelChanged event.
This Palette's most important event is the one raised when the user selects a different Tree control's
node. This event's callback function must prepare, according to the information received from the
Tree control, a Layer name search pattern that identifies all the Layers affected by this selection.
This event’s callback function (Listing 27.15) will receive two arguments, the selected node’s
Label and the Handle that identifies the node. To find out which kind of node it is: Layer,
Drawing or All, we will retrieve the selected node’s ascendants. The procedure is:
1. The text of the selected node's Label is included in the list s-list.
2. A while loop is entered using as test condition that the previously selected node's parent is
returned. The loop will end on arriving to the root node.
3. The selected node's Handle (Key argument) is used to retrieve its parent using
dcl_Tree_GetParentItem. The Handle returned for this parent node is assigned to the
variable Key.
4. The node's Label text is extracted using dcl_Tree_GetItemLabel and added to the list s-
list.
5. Upon the loop's completion, which can include at most three iterations in the event that a Layer
node had been selected, a cond expression is evaluated using the length of the list s-list
as condition.
a. If s-list has only one term, it means that the root node has been selected. The pattern
assigned to *XLayer* will then be the wildcard "*".
b. If s-list has two terms and the second term is equal to the drawing's name, the pattern
will then be "~*|*", which selects all the Layers that do not include the pipe "|"
character in its name. The character "~" acts as the logical NOT.
c. In case it is not the name of the current drawing, then the pattern is composed by
concatenating the name of the drawing with the string "|*" to obtain a pattern of the kind
"drawing-name|*".
d. If s-list has three terms and the second term is the current drawing's name, the third term
(which is the Layer's name) shall be assigned *XLayer*.
e. If none of the above is true the default clause is evaluated, concatenating the second term in
the list, the character "|" and the list's third term, creating a pattern with the structure
"drawing-name|layer-name".
6. Like other functions, it concludes with a call to draw-buttons that updates the On/Off
button's image.
(defun c:XLayer_frmPal_trcLayers_OnSelChanged
(Label Key / s-list)
(setq s-list (cons Label s-list))
(while (setq Key (dcl_Tree_GetParentItem
XLayer_frmPal_trcLayers
Key))
(setq s-list
(cons
(dcl_Tree_GetItemLabel
XLayer_frmPal_trcLayers
Key)
s-list)))
(cond ((= (length s-list) 1)
(setq *XLayer* “*”))
((and (= (length s-list) 2)
(= (nth 1 s-list)
(getvar “DWGNAME”)))
(setq *XLayer* “~*|*”))
((= (length s-list) 2)
(setq
*XLayer* (strcat (nth 1 s-list)
“|*”)))
((and (= (length s-list) 3)
(= (nth 1 s-list)
(getvar “DWGNAME”)))
(setq *XLayer* (nth 2 s-list)))
(t
(setq
*XLayer* (strcat (nth 1 s-list)
“|”
(nth 2 s-list)))))
(draw-buttons *XLayer*))
That completes the implementation of the Tree control, including the reaction to the SelChanged
event the application listens to. Although the buttons are not yet operational, we can now verify the
Palette's operation displaying the populated Tree view.
When a new document is made current the situation is similar to that when the form is initialized. The
only difference here is that the tree is already populated. For this reason before calling the start-
view function, the dcl_Tree_Clear function must be called to delete the existing tree before
creating the one that reflects the new context (Listing 27.16).
(defun c:XLayer_frmPal_OnDocActivated (/)
(dcl_Tree_Clear XLayer_frmPal_trcLayers)
(start-view))
In the case that no open documents remain in AutoCAD, the simplest solution is to close the form.
This is achieved with the form's CloseAll method. This method takes a numeric argument
indicating the type of forms close. The values for this argument are specified in Table 27.6.
The code for the EnteringNoDocState event's callback function is shown in Listing 27.17.
(defun c:XLayer_frmPal_OnEnteringNoDocState (/)
(dcl_Form_CloseAll 64))
Button Actions.
All buttons raise the Clicked event. All their Clicked event handler functions follow the same
general procedure: they traverse the Layers executing a certain action on those matching the pattern
string returned by the Tree control's SelChanged event handler.
btn_ACT (Layer On/Off) Button.
Listing 27.18. Event handler for the btnACT button's Clicked event.
This is an event handler that not only performs an action on the drawing's objects, in this case its
Layers, turning them On or Off, but that also acts on the control itself, changing the image it displays.
At the same time, the information about which is the button's current image is one of the criteria the
function uses to decide what action to take. The button's Clicked event handler's action can be
described as follows:
The activate-layer function (Listing 27.19) receives three arguments: pattern, which is the
string returned from the Tree control's SelChanged event handler; lst which is the Layers list
returned by layer-list and state which is the keyword that indicates if the Layers must be
turned On (:vlax-true) or Off (:vlax-false).
Its action is limited to traversing with foreach the Layer names list to set their LayerOn property
a s :vlax-true or :vlax-false depending on their names matching or not the pattern. The
Layer object is retrieved using the Layers collection Item method.
(defun activate-layer (pattern lst state / layer-coll)
(setq layer-coll (vla-get-Layers *aevl:drawing*))
(foreach lyr lst
(if (wcmatch lyr pattern)
(vla-put-LayerOn (vla-item layer-coll lyr) state))))
1. The color selection dialog is displayed. The value selected in the dialog is assigned to the s-
col variable.
2. The drawing's Layers collection is traversed searching for those whose name matches the pattern
contained in the global variable *XLayer*. To do this the Layers collection retrieved with
vla-get-Layers is traversed using vlax-for.
3. If the Layer's name (contained in the VLA-object's Name property) matches the pattern, its
Color property is modified to the selected color number using vla-put-color.
4. Once the loop concludes, the AutoCAD viewports are regenerated with a call to vla-Regen.
5. A call to save-state saves the new Layer state.
(defun c:XLayer_frmPal_btnCOLOR_OnClicked ()
(if (setq s-col (acad_colordlg 256 t))
(progn
(setq layer-coll
(vla-get-layers
*aevl:drawing*))
(vlax-for lyr layer-coll
(if
(wcmatch (vla-get-name lyr)
*XLayer*)
(progn
(vla-put-color lyr s-col))))
(if (= (getvar “CTAB”) “Model”)
(vla-Regen *aevl:drawing*
acActiveViewport)
(vla-Regen *aevl:drawing*
acAllViewports))
(save-state))))
Listing 27.20. Event handler function for the btnCOLOR button's Clicked event.
The effects of selecting this button are partly similar to those of the _LAYISO command. All Layers
whose names do not match the pattern assigned to *Layers* will be turned off, leaving only the
selected ones visible. T he c:XLayer_frmPal_btnISOLATE_OnClicked function (Listing
27.21) is its Clicked event handler. This function works as follows:
Listing 27.21. Event handler for the btnISOLATE button's Clicked event.
For each button action a new Layer state has been saved by the event handler functions described
above. This allows us to include a button that can be used to undo those modifications. If several
changes have been made, they will be undone one by one each time the Undo Changes button is
clicked. This is done by recovering the previous Layer state each time the button is clicked calling the
prev-state function defined in Listing 27.11.
Once a previous state has been recovered, the buttons are updated with a call to draw-buttons
using "*" as pattern.
Listing 27.22. Event handler for the btnUNDO button's Clicked event.
Being a modeless form, Layers and external references may change while the palette is displayed. In
many cases the list will be updated when operating on the palette. A reactor could also be
implemented to detect these changes. But as that is not this chapter's subject we will simply solve this
by adding a fifth button which can be used to update the Tree control. The event handler function of
this button's Clicked event merely reproduces the behavior that would take place when changing
the active document: deleting the existing tree and repopulating it to reflect the current situation
(Listing 27.23).
(defun c:XLayer_frmPal_btnUPDATE_OnClicked (/)
(dcl_Tree_Clear XLayer_Trv_Capas)
(start-view))
Listing 27.23. Event handler for the btnUPDATE button's Clicked event.
27.8 Summary.
A Graphic User Interface allows a programming style based on reacting to events raised within a
dialog box. Two chapters have been devoted to this subject. One of them explores the Programmable
Dialog Box (PDB) language introduced with AutoCAD Release 12. The one we are concluding
examines the advanced features offered by OpenDCL, an open source tool, which may well be the
prototype for what would be a welcome extension to Visual LISP.
Exercises:
Exercise 1.
The application developed in this chapter could be expanded to not only turn Layers On and Off, but
also to Lock/Unlock or Thaw/Freeze Layers.
Including these new features in the XREF Explorer would demonstrate the mastery attained by he
who has concluded studying this book.
Exercise 2.
1 The URL for OpenDCL is http://www.sourceforge.net/projects/opendcl. This chapter has been prepared using the
ENU.7.0.0.4 version. The OpenDCL help is available online at http://www.opendcl.com/HelpFiles/index.php.
Index of Function Listings.
Chapter 21
Listing 21.1. Function that processes the texts.
Listing 21.2. Callback function triggered by changes in the viewport.
Listing 21.3. Callback function triggered on concluding a command.
Listing 21.4. Creating the Object reactor.
Listing 21.5. Creating the Command reactor.
Listing 21.6. C:AUTO-SCALE. User interface for creating the reactors.
Listing 21.7. Function that determines if an object owns reactors.
Listing 21.8. Code to include in ACADDOC.LSP.
Listing 21.9. Callback function that reports on events.
Listing 21.10. Object reactor callback function.
Listing 21.11. Creation of the Database reactor.
Listing 21.12. Creation of the Editor reactor.
Listing 21.13. Function to link an informative object reactor.
Chapter 22
Listing 22.1. Code for the PARAMETRIC dialog.
Listing 22.2. Function that starts a dialog.
Listing 22.3. Function that loads a SLD image in a dialog.
Listing 22.4. Function that checks and formats edit box values.
Listing 22.5. Parameter edit boxes callback function.
Listing 22.6. Function test-other.
Listing 22.7. Verification functions test-1, test-2 and test-3.
Listing 22.8. Function test-normal.
Listing 22.9. Callback function to the Predefined Forms radio buttons.
Listing 22.10. Detecting the selected predefined form radio_button.
Listing 22.11. Slider callback function.
Listing 22.12. Assigning callback actions to tiles.
Listing 22.13. Function that determines the model type.
Listing 22.14. Function that activates the dialog box.
Listing 22.15. Function for calculating the bulge magnitude.
Listing 22.16. Function that draws the profile as a LWPOLYLINE.
Listing 22.17. Function that creates the 3D model.
Listing 22.18. Main function C:DCL-PARAM.
Chapter 23
Listing 23.1. Function that reads a block insert's variable attributes.
Listing 23.2. Standard function to extract attribute values.
Listing 23.3. Processing a block to extract a list with its attributes.
Listing 23.4. XDATA assignment.
Listing 23.5. Reading XDATA.
Listing 23.6. Obtaining a list of all the dictionaries.
Listing 23.7. Data entry function.
Listing 23.8. Function that creates a dictionary, or retrieves its ENAME in case it already existed.
Listing 23.9. Adding new entries to the dictionary.
Listing 23.10. Main function C:TOPONYMS.
Listing 23.11. Function that queries the linked data.
Listing 23.12. LIST->LDATA function.
Listing 23.13. Function LIST->LDATA including VLAX-LDATA-TEST.
Listing 23.14. Function to associate entities using LDATA.
Listing 23.15. Function that searches the libraries paths.
Chapter 24
Listing 24.1. Function that inserts a table in the drawing.
Listing 24.2. Function that changes a row's text height.
Listing 24.3. Function that changes a column's text height.
Listing 24.4. Selection of the block to process and extraction of its attributes as a list.
Listing 24.5. Function that calculates the approximate relative widths for columns.
Listing 24.6. Main function C:ATTRIB-TABLE.
Chapter 25
Listing 25.1. CONNECT-EXCEL function.
Listing 25.2. DISCONNECT-EXCEL function.
Listing 25.3. APP-ERR function.
Listing 25.4. LIST->EXCEL function.
Listing 25.5. PROCESS-TABLE function.
Listing 25.6. PROCESS-ROW function.
Listing 25.7. DATA->CELL auxiliary function.
Listing 25.8. Code for the dialog box.
Listing 25.9. Main function C:EXCEL-ATTRIBUTES.
Listing 25.10. FILL-LIST function.
Listing 25.11. CHECK-ATTRIBUTES function.
Listing 25.12. EXTRACT function.
Listing 25.13. READ-BLOCKS function.
Listing 25.14. HAS-ATTRIBUTES? function.
Chapter 27
Listing 27.1. Function that implements the new XLAYER command.
Listing 27.2. Template function created by OpenDCL Studio.
Listing 27.3. Function that retrieves the list of attached drawings.
Listing 27.4. Function that retrieves the list of Layers.
Listing 27.5. Function that creates the drawings and Layers association list.
Listing 27.6. Function that creates the Tree's nodes.
Listing 27.7. Auxiliary function DRAW-BUTTONS.
Listing 27.8. CHECK-STATE function.
Listing 27.9. Function to change the btnACT button's image.
Listing 27.10. Function that saves the Layer State.
Listing 27.11. Function that restores a previous Layer State.
Listing 27.12. Layer states initializing function.
Listing 27.13. Function that populates the tree view.
Listing 27.14. Final code for the form's Initialize event.
Listing 27.15. Tree control SelChanged event's callback function.
Listing 27.16. DocActivated event's callback function.
Listing 27.17. Code for the EnteringNoDocState event.
Listing 27.18. Event handler for the btnACT button's Clicked event.
Listing 27.19. Function that sets Layers On or Off.
Listing 27.20. Event handler function for the btnCOLOR button's Clicked event.
Listing 27.21. Event handler for the btnISOLATE button's Clicked event.
Listing 27.22. Event handler for the btnUNDO button's Clicked event.
Listing 27.23. Event handler for the btnUPDATE button's Clicked event.
Functions library.
These functions from previous Volumes may be called in programs defined in the present text.
(defun replace (new old string / pos len)
(setq pos 0
len (strlen new))
(while (setq pos (vl-string-search old string pos))
(setq string (vl-string-subst new old string pos)
pos (+ pos len)))
string)
Listing 11.15. Function to detect and remove objects that belong to a Group.
(defun ax-add-group
(name obj-list / groups-coll group)
(setq groups-coll (vla-get-Groups *aevl:drawing*)
group (vl-catch-all-apply
'vla-Item
(list groups-coll name)))
(cond
((vl-catch-all-error-p group)
(setq group (vla-Add groups-coll name))
(vla-AppendItems
group
(ax-list->variant obj-list))
group)
(t
(if (setq objects
(ax-no-group obj-list group))
(vla-AppendItems
group
(ax-list->variant objects)))
group)))
(pragma
'((unprotect-assign *aevl:acad* *aevl:drawing*)))
(setq *aevl:acad* (vlax-get-acad-object)
*aevl:drawing* (vla-get-ActiveDocument
*aevl:acad*))
(pragma
'((protect-assign *aevl:acad* *aevl:drawing*)))
(vl-load-com)
(load "aevl-globals"
"AEVL-GLOBALS not found")
(load "./Utility/vlisp-library"
"VLISP-LIBRARY not found")
(autoload "./Numera/FAS/numera.fas"
'("NUMERA" "NUM-OPTIONS"))
;;; Vector A to B
;;; Arguments: A, B, lists of three real numbers.
(defun vec (A B) (mapcar '- B A))
Listing 13.15. Function that adds a new UCS to the current document.
Listing 13.16. Function that returns the current UCS transformation matrix.
Listing 13.17. Function that sets the view direction and visual style.
(defun var-vis ()
(if (= (getvar "BLOCKEDITOR") 0)
(progn (setvar "VSMONOCOLOR" " RGB:211,76,3")
(setvar "VSFACECOLORMODE" 1)
(setvar "VSEDGES" 1)
(setvar "VSEDGECOLOR" "RGB:255,212,82")
(setvar "VSISOONTOP" 0)
(setvar "VSFACESTYLE" 2)
(setvar "VSOBSCUREDEDGES" 0)
(setvar "VSOCCLUDEDEDGES" 0)
(setvar "PERSPECTIVE" 1))))
Listing 15.12. UCS from the Z axis direction vector using ActiveX.
P ART 1. INTRODUCTION.
Chapter 1. AutoLISP/Visual LISP.
1.1 Visual LISP.
1.2 New LISP functions.
2.3. Summary.
Volume 2
Volume 3.