AutoLISP Again: Blocks and Attributes

Image: AutoCAD icon.

This third exercise takes a look at block guts.

We learned about writing AutoLISP to select an area in model space. We learned about using those coordinates to do something else, specifically to draw four lines making a rectangle.

Then we learned about using the 'mapcar' function to generate lists from the output of other functions.

Now it's time to go another step closer to actually writing useful AutoLISP code. We are going to select a block, and then play with it a bit to see what we can learn.

This codeblock (below) is a complete AutoLisp program. Load it into a running instance of AutoCAD and issue the command BLK_ATT_LIST.

You will see a dialog box pop on the screen asking for an output filename. Type in any valid filename and end it with .txt . You don't have to, of course, but I recommend it.

Press the 'Okay' button and then touch any block in your model. The Lisp routine will generate some output to the console, and will generate the file you named in the current working directory. The current working directory is where the open .dwg lives.

A Note:

I've noticed over the years that most Lisp routines don't provide much user feedback. After writing these three .lsp files, I think I know why.

It takes a lot of extra work to give feedback, and I suspect most of the people .lsp files are written for are fine with that as long as it does what it's supposed to do.

However, in the case of our learning exercises, I need to provide enough output to the console to link what is happening in the code with some tangible real-world data.

Another Note:

These blog posts are not the exercise. The code is the exercise, and that is where most of the useful information is to be found. If you only read my post and skip over the code block, you won't really learn anything of value.

The comments in the code combined with the output in the console window will tell you lots of stuff. Copy the codeblock below into a blank Visual Studio Code file so you can see everything at once.

Read each line of code end to end. The line comment is there to give you insights into what that line is doing.

Run the code in a test .dwg and see what it does. When you can read the code and comprehend the results, I'd say we are making good progress.

The Code

; BLK_ATT_LIST.LSP - Saves a list of the Attribute names and current values of a selected block
; to a user named tab-delimited text file in the current working directory.
; (The current working directory is the directory where the opened and active .dwg file resides.)
; Developed 2022 by Greg Sanders
;
; This .lsp file focuses on grasping some concepts related to what we commonly call 'blocks'.
; - Blocks are entities, not all entities are blocks.
; Blocks are called 'INSERT' type entities.  The entity type is found in the DXF
; group codes (see link below), which is a dictionary of key.value pairs.
; The entity type string is the value associated with the 0 key. (0."INSERT")
;
;;; IMPORTANT DISTINCTION: In the DXF format, the definition of 'objects' differs from
;;;   'entities': objects have no graphical representation and entities do. 
;;;   For example, dictionaries and lists are objects, not entities.
;;;   Read more here: https://help.autodesk.com/view/ACD/2016/ENU/?guid=GUID-3F0380A5-1C15-464D-BC66-2C5F094BCFB9
; 
; NEW IN THIS EXERCISE:
;     getstring
;     getvar
;     open
;     entsel
;     entget
;     entnext
;     assoc
;     progn
;     The use of a .dcl file to create a dialog.
;
(defun c:BLK_ATT_LIST ( / ent en entName effName primEntList blkHandle blkType  ; Start the main function definition
             outPut en2 entlist2 outFile2 outPath2 cur_dir ddiag blkAttrFn)     ; Make all the variables local so they will show up in vscode debug.
  (princ "\n--- Running BLK_ATT_LIST : 2022 - Gregory Sanders 
\n--- Create a tab-delimited .txt file of the attribute names and values in a single selected block.")
  (print)                                                                       ; Print a new blank line on the console.
;;; --==  What follows is two ways of getting user input.
;;; --==  The first makes a dialog box, but requires an external .dcl file described below. It is commented out.
  ; (setq blkAttrFN (C:FN_GETTER))                                                ; Run the dialog .dcl file and return the filename here.
;;; --==  The second is simpler requiring no external .dcl file.
  (setq blkAttrFN (getstring "Provide output filename: "))                      ; The easier way of providing the filename.  No .dcl file needed.
  (setq ddiag 2)                                                                ; Cheat the ddiag test by providing the answer here.
;;; --==  Once the filename has been acquired, we can move on.
  (if (= ddiag 2)                                                               ; 'ddiag = 2' tells us we pushed the Okay button.
    (progn                                                                      ; 'progn' evaluates each line and returns the result of the last one.
      (setq cur_dir (getvar "dwgprefix"))                                       ; Remember the folder where the .dwg is.
      (setq outPath2 (strcat cur_dir blkAttrFN))                                ; Join the current folder to the output filename.
      (princ outPath2)
      (setq outFile2 (open outPath2 "a"))                                       ; Open the output file for appending.
      (setq ent (entsel "\nSelect block: "))                                    ; Prompt user to select a block. Return entity definition.
      (if (/= ent nil)                                                          ; Make sure we actually selected a block.
        (progn                                                                  ; 'progn' evaluates each line and returns the result of the last one.
          (setq entName (car ent))                                              ; Get the <Entity name> of 'ent'.
          (setq primEntList (entget entName))                                   ; Get the DXF codes of 'ent'.
          (setq blkHandle (cdr(assoc 5 primEntList)))                               ; Get the block's Handle (unique ID number).
          (setq effName (vla-get-effectivename (vlax-ename->vla-object entName)))   ; Remember the human-readable name.
          ;;; The long 'vla-' string above is required due to the way AutoCAD handles naming copied blocks.
          ;;; They get assigned names like '*U29' instead of the normal block name.  No idea why.
          ;;; So two more modern Visual Lisp functions are called to ferret out the name.
          (princ (strcat "\n" effName " was selected."))                        ; Show human-readable block name.
          (princ (strcat "\nBlock handle: " blkHandle))                         ; Show the block handle value.
          (princ "\nThe following is the 'entity definition', a dictionary of key.value pairs.\n")
          (print primEntList)                                                   ; Show the contents of ent's DXF code dictionary.
          (princ (strcat "Block Name:\t" effName) outFile2)                     ; Log that name to our output file.
          (close outFile2)                                                      ; Never forget to close the file.
              (setq enlist (entget entName))                                    ; Get the DXF group codes
              (setq blkType (cdr(assoc 0 enlist)))                              ; Save the value from the 0 key.value pair. The entity type.
              (if (= blkType "INSERT")                                          ; Test if the entity type is an 'INSERT' entity
                (progn                                                          ; 'progn' evaluates each line and returns the result of the last one.
                  (if(= (cdr(assoc 66 enlist)) 1)                               ; See if 'key 66' (the attribute flag) = 1. (if so, attributes follow)
                    (progn                                                      ; 'progn' evaluates each line and returns the result of the last one.
                      (setq en2(entnext entName))                               ; Get the next sub-entity
                      (setq enlist2(entget en2))                                ; Get the DXF group codes
                      (while (/= (cdr(assoc 0 enlist2)) "SEQEND")               ; Start the while loop and keep looping until SEQEND is found.
                        (setq outFile2 (open outPath2 "a"))                     ; Open the output file in 'append' mode ("a").
                        (princ "\n " outFile2)                                  ; Print a blank line.
                        (setq thisAttrName (cdr(assoc 2 enlist2)))              ; Get the value from the '2.' key.value pair.
                        (setq thisAttrVal (cdr(assoc 1 enlist2)))               ; Get the value from the '1.' key.value pair.
                        (print)                                                 ; Gimme a blank line on the console.
                        (princ thisAttrName outFile2)                           ; Write the attribute name to the output file.
                        (princ (strcat "\t" thisAttrVal) outFile2)              ; Write a tab, followed by the attribute value to the output file.
                        (close outFile2)                                        ; NEVER forget to close the file.
                        (setq en2(entnext en2))                                 ; Get the next sub-entity
                          (if (/= en2 nil)                                      ; Check to make sure there actually was a 'next entity'.
                            (setq enlist2(entget en2)))                         ; Get the DXF group codes
                      )                                                         ; Close the while loop.
                    )                                                           ; Close fourth progn 
                  )                                                             ; Close the if group code 66 = 1 statement
                )                                                               ; close third progn
              )                                                                 ; Close the if blkType = "INSERT" statement
        )                                                                       ; Close the second progn.
      )                                                                         ; Close the (if (/= ent nil) section.
      (if (= ent nil)(princ "\nNothing was selected."))                         ; Let the user know they missed the block.
      (princ "\n --- BLK_ATT_LIST is Finished. ---")                            ; Final console output.
    )                                                                           ; Close the first progn
  )                                                                             ; Close the if((= ddiag 2))
  (if (= ddiag 1) (princ "\nThe BLK_ATT_LIST command was cancelled."))          ; If the 'Cancel' button is pressed, show me this.
  (princ)                                                                       ; Suppress the last echo for a clean exit
)                                                                               ; Close the defun c:BLK_ATT_LIST (

; Standalone function to save the filename to a variable.
(defun saveFN()
  (setq blkAttrFN (get_tile "outFileName"))                                     ; Save the input from the dialog box
)

; Dialog box function to get user provided filename.
(defun C:FN_GETTER()
  (setq cur_dir (getvar "dwgprefix"))                                           ; Get the current working directory.
  (setq dcl_id (load_dialog (strcat cur_dir "fn_getter.dcl")))                  ; Load the dcl file. (A GUI definition file.)
  ;;; The dcl_id has nothing to do with Disney Cruise Lines.  Totally unrelated.
  (if (not (new_dialog "FN_GETTER" dcl_id) ) (exit))                            ; Load the dialog definition if it is not already loaded
  (action_tile "cancel" "(setq ddiag 1)(done_dialog)")                          ; If an action event occurs, do this function
  (action_tile "accept" "(setq ddiag 2)(saveFN)(done_dialog)")
  (start_dialog)                                                                ; Display the dialog box
  (if (= blkAttrFN nil) (princ "\n FN_GETTER Command cancelled."))
  (unload_dialog dcl_id)                                                        ; Unload the dialog
  (princ)                                                                       ; Suppress the last echo for a clean exit)
  blkAttrFN                                                                     ; Return the blkAttrFN value.
)                                                                               ; Close (defun C:FN_GETTER

The file you specified will be in the same folder as the .dwg.

The output file is a tab-delimited file. If you open Excel, then drag/drop the file in the Excel sheet, Excel will import it, and after adjusting the columns, you'll see the attribute names in column A and the values (if they exist) in column B.

You will get this printout on the console:

Image: AutoCad's console printout.
Image: If you code it right, the console can help you learn this stuff.

The .dcl File:

Before we quit here, I need to show you the contents of the fn_getter.dcl file that the FN_GETTER function uses to present the dialog for the Output filename.

FN_GETTER : dialog 
{
  audit_level = 3;
  label = "Block Attribute List Saver";
  initial_focus="outFileName";
  : column {
    : row {
      : boxed_column {
        : edit_box {
          key = "outFileName";
          label = "Filename for Output";
          edit_width = 30;
          value = "";
        }
      }
    }
    : row {
      : boxed_row {
        : button {
          key = "accept";
          label = " Okay ";
          is_default = true;
        }
        : button {
          key = "cancel";
          label = " Cancel ";
          is_default = false;
          is_cancel = true;
        }
      }
    }
  }
}

You might be able to see this defines a dialog with one column and two rows. The first row has an edit box labeled, "Filename for Output".

The second row has two buttons, one 'Okay' and one 'Cancel'.

This .dcl file must be somewhere AutoCAD can find it. It's recommended you put files like this in a folder that is included along one of the paths defined in the Options menu of AutoCAD. For the purpose of these exercises, I simply copy the .dcl file into the same folder as the .dwg file I'm using. That makes it easy.

That does it for this exercise.

I think we are one big step closer to our understanding of AutoLISP becoming a Thing That Works!

link to home page

Prev: AutoLISP Again: mapcar Next: AutoLISP Again: Dissecting Blocks

links

-->