Dictionaries and XRecords.
Written by Stig
Madsen.
(Published here on AfraLisp with kind permission)
Since the dawn of times we've had at least 10 options to save private data
with an AutoCAD drawing. Those were and are the system variables USERI1-5
and USERR1-5 that holds integers and real numbers, respectively. The idea
is OK but the variables are exposed for everyone to use and you shouldn't
assume that your private data will stay intact.
Somewhere around release 10 came XData (Extended Entity Data) which is a
rather clever invention. With XData you can attach intelligent information
to entities, and it works flawlessly. There aren't really drawbacks to the
technique, but there are limitations that has to do with the amount and
type of data you can attach.
During release 13 new forms of entities hit the deck in order to keep and
maintain all sorts of data. Among those were Dictionaries and XRecords.
Like XData, Dictionaries can be attached to any kind of entity and you can
even attach XData to them. Additionally, with Dictionaries/Xrecords you
can tell the drawing itself to keep your data because the drawing
maintains a dictionary in which you can save all the data you want.
So what is a Dictionary and how does it work? Instead of lengthy
explanations, it'll be much easier to look at one of the many Dictionaries
that AutoCAD itself uses. Later we''ll briefly look at how to make our own
simple Dictionary, add an Xrecord to it and save them both with the
drawing.
So, fire up AutoCAD and pay attention. As mentioned, the drawing maintains
a Dictionary that is always present. This is known as the 'named object
dictionary' (although I prefer the term 'main dictionary'). In VBA lingo
it's a collection object and like all other collections it just holds a
series of other objects. In VBA it is accessed through the document object
and in AutoLISP it's accessed by one function only, NAMEDOBJDICT:
Command: (setq mainDict (namedobjdict))
<Entity name: 16a9860>
Command: (entget mainDict)
((-1 . <Entity name: 16a9860>) (0 .
"DICTIONARY")
(330 . <Entity name: 0>) (5 . "C") (100 . "AcDbDictionary")
(280 . 0) (281 . 1) (3 . "ACAD_GROUP")(350 . <Entity name:
16a9868>)
(3 . "ACAD_LAYOUT") (350 . <Entity name: 16a98d0>)
(3 . "ACAD_MLINESTYLE") (350 . <Entity name: 16a98b8>)
(3 . "ACAD_PLOTSETTINGS") (350 . <Entity name: 16a98c8>)
(3 . "ACAD_PLOTSTYLENAME") (350 . <Entity name: 16a9870>))
By looking at the entity list of the main Dictionary, the most important
thing about Dictionaries becomes clear: they are complete entities by
themselves. They are not fragments of data attached to other entities like
XData are.
However, the Dictionary itself is not where you will store your raw data,
- it is merely a container for other objects that in turn can hold the
data. The main dictionary above shows 5 such objects. Its components are
given by a unique name in group 3. Corresponding entity names are given by
groups 350 that follow, but when referencing an object in a Dictionary it
should be done by name only. For example, to reference the "ACAD_MLINESTYLE"
object, use DICTSEARCH:
Command: (setq mlineDict (dictsearch (namedobjdict)
"ACAD_MLINESTYLE"))
((-1 . <Entity name: 16a98b8>) (0 .
"DICTIONARY") (5 . "17")
(102 . "{ACAD_REACTORS") (330 . <Entity name: 16a9860>)
(102 . "}")
(330 . <Entity name: 16a9860>) (100 . "AcDbDictionary")
(280 . 0)
(281 . 1) (3 . "Standard") (350 . <Entity name: 16a98c0>))
This member of the main Dictionary is a Dictionary itself. It holds all
the mline styles that are available. Any style that is created with
MLSTYLE is added to the "ACAD_MLINESTYLE" Dictionary. To explore
a specific style we have to dig deeper and because we are dealing with
Dictionaries we can use DICTSEARCH again - this time by searching the
recently returned Dictionary:
Command: (setq mlineStd (dictsearch (cdr (assoc -1
mlineDict)) "Standard"))
((-1 . <Entity name: 16a98c0>) (0 . "MLINESTYLE")
(5 . "18")
(102 . "{ACAD_REACTORS") (330 . <Entity name: 16a98b8>)
(102 . "}")
(330 . <Entity name: 16a98b8>) (100 . "AcDbMlineStyle") (2
. "STANDARD")
(70 . 0) (3 . "") (62 . 256) (51 . 1.5708) (52 . 1.5708) (71 .
2) (49 . 0.5)
(62 . 256) (6 . "BYLAYER") (49 . -0.5) (62 . 256) (6 . "BYLAYER"))
Now we are getting somewhere! All properties of the mline style
"Standard" are exposed in all their glory. Feel free to look up
all properties in the DXF Reference. Want to change the color of
multilines? Just SUBSTitute group 62 and ENTMOD the style as usual:
Command: (entmod (subst (cons 62 2)(assoc 62 mlineStd)
mlineStd))
((-1 . <Entity name: 16a98c0>) ..etc.. (62 .
2) ..etc.. (62 . 2) ..etc..)
Ok, so a Dictionary is a container that can hold a number of objects. Why
not use existing structures like symbol tables instead of complicating
things? Many reasons, but two reasons come to mind. Symbol tables are
maintained by the people who implemented them and to expand them to hold
every possible custom object is not an option. Secondly, Dictionaries can
be customized in a way that is not possible with symbol tables, and that
opens a range of possibilities only limited by imagination.
Dictionaries and XRecords go hand in hand. Like Dictionaries, XRecords are
handled as named objects and can be manipulated by the same functions that
handle Dictionaries. In the following, we'll try to add our own Dictionary
to the main dictionary. We will also create an XRecord to hold various
informations and add it to our Dictionary.
When dealing with Dictionaries, at one point you will have to consider
ownership. Which object is going to own the Dictionary? Will it hold
generic data for your application or will it hold data that is specific
for some entity or entities? In the first case you will probably use the
main dictionary to save your data with the drawing. If your application is
maintaining data for linetypes, you will probably add an extension
dictionary to the linetype symbol table.
Whatever the ownership, the Dictionary is initially created without
ownership and for that purpose we'll use the function ENTMAKEX. It works
like ENTMAKE, but it creates the entity without an owner - and it returns
an entity name instead of an entity list. Let's make a function that adds
our own Dictionary to the main dictionary. In this example we will name it
"OUR_DICT":
(defun get-or-create-Dict (/ adict)
;;test if "OUR_DICT" is already present in the main dictionary
(if (not (setq adict (dictsearch (namedobjdict) "OUR_DICT")))
;;if not present then create a new one and set the main
;; dictionary as owner
(progn
(setq adict (entmakex '((0 . "DICTIONARY")(100 . "AcDbDictionary"))))
;;if succesfully created, add it to the main dictionary
(if adict (setq adict (dictadd (namedobjdict) "OUR_DICT" adict)))
)
;;if present then just return its entity name
(setq adict (cdr (assoc -1 adict)))
)
)
|
If you want to see what happens to the dictionary when
added to an owner, then stop the routine right after ENTMAKEX and use
ENTGET to investigate the newly created entity. Notice that the owner in
group code 330 will not be specified. After using DICTADD the owner in
group 330 will be the main dictionary.
Right now we have placed a Dictionary named "OUR_DICT" in the
main dictionary. To check if it succeeded we can investigate the main
dictionary:
Command: (entget (namedobjdict))
((-1 . <Entity name: 16a9860>) (0 .
"DICTIONARY") ..etc...
(3 . "ACAD_PLOTSTYLENAME") (350 . <Entity name: 16a9870>)
(3 . "OUR_DICT") (350 . <Entity name: 16a8da0>))
And there it is! But what good does it do us? It just sits there and
doesn't hold any data. Well, let's say we want to save data for a routine
that creates annotations - for example a text style, a layer name and a
text height. Simple stuff, but it'll suffice for the purpose of
illustration. With XRecords we can create an entity that can hold any
possible data within the range of defined data types. Unlike XData it uses
regular group codes to save data.
All group codes (except internal data like code 5 and the negative codes)
can be used. Of course, the data types that are defined for the specific
group codes have to be respected. This means that, for example, a code 70
cannot hold anything else than a 16-bit integer and so on. Code values can
be examined in the DXF Reference.
An XRecord is created in much the same way as a Dictionary. First we'll
see if it already exists, then create it without an owner with ENTMAKEX
and lastly add it to our custom Dictionary. Again, we will name it in
order to retrieve it by name. In this example it will be called "OUR_DICT".
Both name and initial values are hardcoded into the function - in the real
world we would probably make this a generic function and specify name and
values as arguments.
(defun get-or-make-Xrecord (/ adict anXrec)
(cond
;;first get our dictionary. Notice that "OUR_DICT" will be
;;created here in case it doesn't exist
((setq adict (get-or-create-Dict))
(cond
;;if "OUR_DICT" is now valid then look for "OUR_VARS" Xrecord
((not (setq anXrec (dictsearch adict "OUR_VARS")))
;;if "OUR_VARS" was not found then create it
(setq anXrec (entmakex '((0 . "XRECORD")
(100 . "AcDbXrecord")
(7 . "Arial")
(8 . "A09--T-")
(40 . 2.0)
)
)
)
;;if creation succeeded then add it to our dictionary
(if anXrec (setq anXrec (dictadd adict "OUR_VARS" anXrec)))
)
;;if it's already present then just return its entity name
(setq anXrec
(cdr (assoc -1 (dictsearch adict "OUR_VARS")))
)
)
)
)
)
|
Now we have an XRecord that contains three different data:
a text style name in group code 7, a layer name in group code 8 and a text
height in group code 40. All codes are chosen with respect to normal
convention, but any code that can be associated with the data type in
question can be used. The structure from which to access our data will now
be like this:
Named object dictionary = Dictionary (owner = the drawing)
> OUR_DICT = Dictionary (owner = named object dictionary)
> OUR_VARS = Xrecord (owner = OUR_DICT)
(7 . "Arial") = Egenskab i Xrecord
(8 . "A09-T-") = Egenskab i Xrecord
(40 . 2.0) = Egenskab i Xrecord
The only thing that remains is to read the data:
(defun getvars (/ vars varlist)
;;retrieve XRecord "OUR_VARS" from dictionary "OUR_DICT"
;;which in turn calls both functions above
(setq vars (get-or-make-Xrecord))
;;if our Xrecord is found, then get values in group code 7, 8 and 40
(cond (vars
(setq varlist (entget vars))
(setq txtstyle (cdr (assoc 7 varlist)))
(setq txtlayer (cdr (assoc 8 varlist)))
(setq txtsize (cdr (assoc 40 varlist)))
)
;;otherwise return nil
(T nil)
)
)
|
Because of the naming scheme, Dictionaries work much like symbol tables
in terms of accessing entries. In addition to DICTSEARCH there's also a
function, DICTNEXT, to iterate through all entries in a Dictionary. It
works like TBLNEXT - here shown by iterating through the main dictionary:
(defun C:LISTDICTS (/ maindict adict)
(setq maindict (namedobjdict))
(while (setq adict (dictnext maindict (not adict)))
(princ (cdr (assoc -1 adict)))
(princ (strcat "\t(type = " (cdr (assoc 0 adict)) ")\n")
)
)
(princ)
)
|
Command: listdicts
<Entity name: 185c0d0> (type = DICTIONARY)
<Entity name: 185f4b8> (type = DICTIONARY)
<Entity name: 185c0d8> (type = DICTIONARY)
<Entity name: 185f4a8> (type = ACDBDICTIONARYWDFLT)
<Entity name: 18c4320> (type = DICTIONARY)
<Entity name: 18c3d10> (type = XRECORD)
There're also functions to rename a Dictionary, DICTRENAME, and to remove
a Dictionary, DICTREMOVE. The latter simply removes its entry from the
owner, or in other words: detaches it from the owner. It doesn't delete it
unless the owner is "ACAD_GROUP" or "ACAD_MLINESTYLE".
Sometimes when updating a Dictionary it's easier to remove/delete it and
replace it with a new entry, but that will be for your pleasure to
explore.
If you gained some understanding of Dictionaries and XRecords by now then
I'll throw in a little assignment: Figure out how you can add the XRecord
directly to the main dictionary without first creating a Dictionary!
|