Version 13 of Playing UML

Updated 2005-07-21 11:20:18 by ctasada

Richard Suchenwirth 2001-10-02 - UML, the Unified Modeling Language, is a convention for drawing diagrams in object-oriented analysis and design. After an OOA/D course (without a CASE tool at hand), I felt tempted to draw UML graphic objects on a canvas.

http://mini.net/files/uml.jpg

Here's a set of graphic elements used in UML diagrams: boxes that represent classes, with class name in bold, attribute and method names each in a sub-box; ovals that represent use cases, and stylized human figures that represent actors, both with the name below; plus edges connecting such nodes without running into their bounding boxes.

In the graph rendered on a canvas, you may move nodes with the mouse, and all edges are updated accordingly. }

 proc defaultfont {} {
    set f [[button ._] cget -font]
    destroy ._
    set f
 }
 proc ladd {_list what} {
    upvar $_list list
    if {![info exists list] || [lsearch $list $what] == -1} {
        lappend list $what
    }
 }
 namespace eval UML {
    variable a
    set a(font)     [defaultfont]
    set a(boldfont) [concat $a(font) bold]

    proc box {c x y class {fill white}} {
        variable a
        foreach {name atts meths} $class break
        if {"$atts$meths"==""} {append name \n} ;# don't make box too low
        set id0 [$c create text $x $y -text $name -anchor nw -font $a(boldfont)]
        $c itemconfig $id0 -tag [set id node$id0]
        set y1 [lindex [$c bbox $id] 3]
        if {$atts!=""} {
            $c create text $x $y1 -text [join $atts \n] -anchor nw\
                -font $a(font) -tag $id
        }
        set y2 [lindex [$c bbox $id] 3]
        if {$meths!=""} {
            $c create text $x $y2 -text [join $meths ()\n]() -anchor nw\
                -font $a(font) -tag $id
        }
        foreach {x0 y0 x3 y3} [$c bbox $id] break
        set x0 [expr {$x0-2}]
        set x3 [expr {$x3+2}]
        if {$meths!=""} {set y3 [expr {$y3+2}]}
        set id1 [$c create rect $x0 $y0 $x3 $y3 -tag $id \
            -fill grey -outline grey]
        $c move $id1 4 4
        set id2 [$c create rect $x0 $y0 $x3 $y3 -tag $id -fill $fill]
        $c lower $id2; $c lower $id1
        $c create line $x0 $y1 $x3 $y1 -tag $id
        $c create line $x0 $y2 $x3 $y2 -tag $id
        foreach {x0 y0 x1 y1} [$c bbox $id] break
        $c move $id [expr {($x0-$x1)/2}] [expr {($y0-$y1)/2}]
        set a(edges,$id) {}
        return $id
    }
    proc oval {c x y text {fill white}} {
        variable a
        set id0 [$c create oval [expr {$x-20}] [expr {$y-20}] \
            [expr {$x+20}] $y -fill grey -outline grey]
        $c move $id0 4 4
        set id1 [$c create oval [expr {$x-20}] [expr {$y-20}] \
            [expr {$x+20}] $y -fill $fill]
        $c itemconfig $id1 -tag [set id node$id1]
        $c itemconfig $id0 -tag $id
        $c create text $x [expr {$y+5}] -text $text \
            -font $a(font) -tag $id -anchor n
        set a(edges,$id) {}
        return $id
    }
    proc actor {c x y {text ""}} {
        variable a
        set id0 [$c create oval [expr {$x-5}] [expr {$y-5}] \
            [expr {$x+5}] [expr {$y+5}] -fill white]
        $c itemconfig $id0 -tag [set id node$id0]
        $c create line [expr {$x-15}] [expr {$y+7}] \
            [expr {$x+15}] [expr {$y+7}] -tag $id
        $c create line $x [expr {$y+5}] $x [expr {$y+15}] -tag $id
        $c create line $x [expr {$y+15}] [expr {$x-10}] [expr {$y+25}] -tag $id
        $c create line $x [expr {$y+15}] [expr {$x+10}] [expr {$y+25}] -tag $id
        $c create text $x [expr {$y+25}] -text $text \
            -font $a(font) -tag $id -anchor n
        set a(edges,$id) {}
        return $id
    }
    proc rawEdge {c from to {dash ""}} {
        foreach {x0 y0 x2 y2} [$c bbox $from] break
        set x1 [expr {($x0+$x2)/2.}]
        set y1 [expr {($y0+$y2)/2.}]
        if {$from==$to} {
            set x3 [expr {$x2+5}]
            set y4 [set y3 [expr {$y2+5}]]
            set x4 $x2
            $c create line $x2 $y1 $x3 $y1 $x3 $y3 $x1 $y3 $x1 $y2 \
                -tag edge -dash $dash
        } else {
            foreach {x3 y3 x5 y5} [$c bbox $to] break
            set x4 [expr {($x3+$x5)/2.}]
            set y4 [expr {($y3+$y5)/2.}]
            if {$x1<$x2 && $x4>$x2} {set x1 $x2} ;# crop coordinates
            if {$x4>$x3 && $x1<$x3} {set x4 $x3}
            if {$y1<$y2 && $y4>$y2} {set y1 $y2}
            if {$y4>$y3 && $y1<$y3} {set y4 $y3}
            if {$x1>$x0 && $x4<$x0} {set x1 $x0}
            if {$x4<$x5 && $x1>$x5} {set x4 $x5}
            if {$y1>$y0 && $y4<$y0} {set y1 $y0}
            if {$y4<$y5 && $y1>$y5} {set y4 $y5}
            $c create line $x1 $y1 $x4 $y4 -tag edge -dash $dash
        }
        decorationPoints $x1 $y1 $x4 $y4
    }
    proc decorationPoints {x1 y1 x4 y4 {r 12}} {
        set a [expr {atan2($x4-$x1,$y4-$y1)}]
        set a1 [expr {$a-atan(1.)}]
        set x2 [expr {round($x4-cos($a1)*$r)}]
        set y2 [expr {round($y4+sin($a1)*$r)}]
        set a2 [expr {$a+atan(1.)}]
        set x3 [expr {round($x4+cos($a2)*$r)}]
        set y3 [expr {round($y4-sin($a2)*$r)}]
        set a3 [expr {$a+2*atan(1)}]
        set r2 [expr {$r*sqrt(2.)}]
        set x5 [expr {round($x4+cos($a3)*$r2)}]
        set y5 [expr {round($y4-sin($a3)*$r2)}]
        list $x2 $y2 $x4 $y4 $x3 $y3 $x5 $y5 ;# use 6 for a triangle        
    }
    proc edge {c type from to} {
        variable a
        ladd a(edges) [list $type $from $to]
        set dash [expr {$type=="depend"||$type=="dotted"? ".": ""}]
        set deco [rawEdge $c $from $to $dash]
        switch -- $type {
          aggreg   {$c create poly $deco -fill white -outline black -tag edge}
          assoc    {}
          compose  {$c create poly $deco -fill black -outline black -tag edge}
          uniAssoc -
          depend   {$c create line [lrange $deco 0 5] -tag edge}
          dotted   {}
          inherit  {$c create poly [lrange $deco 0 5] -fill white \
            -outline black -tag edge}
        }
    }
    proc makeMovable c {
        variable a
        foreach i [$c find all] {
            if [regexp {node[1-9]} [$c itemcget $i -tags]] {
                $c addtag mv withtag $i
            }
        }
        $c bind mv <1> {
            set tag ""
            foreach i [%W itemcget current -tags] {
                if [regexp node $i] {set tag $i; break}
            }
            if {$tag!=""} {
                set ::UML::a(tag) $tag
                set ::UML::a(x) [%W canvasx %X]
                set ::UML::a(y) [%W canvasx %Y]
            }
        }
        $c bind mv <B1-Motion> {
            set x [%W canvasx %X]
            set y [%W canvasx %Y]
            %W move $::UML::a(tag) \
                [expr {$x-$::UML::a(x)}] [expr {$y-$::UML::a(y)}]
            set ::UML::a(x) $x
            set ::UML::a(y) $y
        }
        $c bind mv <B1-ButtonRelease> {
            %W delete withtag edge
            foreach i $::UML::a(edges) {eval UML::edge %W $i}
        }
    }
 }
 #------- self-test code
 if {[file tail [info script]]==[file tail $argv0]} {
   set c [canvas .c -bg white]
   pack .c -fill both -expand 1
   set b1 [UML::box   $c  50  50 {Class {att0 att1 att2} {method1 method2}}]
   set b2 [UML::box   $c 200  50 {AnotherClass {attrib} {method1 method2 method3}}]
   set o1 [UML::oval  $c 350  50 "Use a CASE tool"]
   set a1 [UML::actor $c 350 200 badActor]
   set b3 [UML::box   $c  50 200 {NoAttributes {} {butHas methods}} yellow]
   set b4 [UML::box   $c 200 200 {NoMethods {butHas attributes} {}}]
   set b5 [UML::box   $c 200 125 {{UML demo}} green]
   UML::edge $c aggreg   $b1 $b2
   UML::edge $c uniAssoc $b1 $b3
   UML::edge $c depend   $b1 $b4
   UML::edge $c dotted   $b4 $o1
   UML::edge $c assoc    $o1 $a1
   UML::edge $c assoc    $b2 $a1
   UML::edge $c inherit  $b3 $b4
   UML::edge $c compose  $b2 $b3
   UML::edge $c assoc    $b4 $b4
   UML::makeMovable $c
 }

Removed an extra square bracket in the defaultfont proc. Ro Tuesday, May 7th, 2002 - RS: thanks! I test scripts before wikifying them (by copy&paste), but occasionally I have to edit stuff that prevents its acceptance, and there this glitch may have happened. Ro: Wednesday, July 10th, 2002: You're welcome... I love to read your posts ;)


MRS on 2005-06-26

Any *nix user out there? I allways have problems with scripts posted here at the wiki that set the font. In all cases (that I have problem) the screenshots showed Windows as the running OS. Here the error I got from this script (running on Linux):

 Error in startup script: expected integer but got "bold"
     (processing "-font" option)
     invoked from within
 "$c create text $x $y -text $name -anchor nw -font $a(boldfont)"
     (procedure "UML::box" line 5)
     invoked from within
 "UML::box   $c  50  50 {Class {att0 att1 att2} {method1 method2}}"
     invoked from within
 "if {[file tail [info script]]==[file tail $argv0]} {
   set c [canvas .c -bg white]
   pack .c -fill both -expand 1
   set b1 [UML::box   $c  50  50 {Cla..."
     (file "playUML.tcl" line 167)

Shouldn't Tcl/Tk scripts be platform independent?

MG If you replace the lines:

    set a(font)     [defaultfont]
    set a(boldfont) [concat $a(font) bold]

with

    set a(font)     [eval font create fontNormal [font actual [defaultfont]]]
    set a(boldfont) [eval font create fontBold   [font actual [defaultfont]] -weight bold]

then I think it'll work OK.

RS Hmm.. this can only be if you still have the old X *-*-*-* pattern for default fonts.. I thought that grew obsolescent after Tcl 8.1 :) You can find out in an interactive wish by typing

 % [text .t] cget -font

But I agree that MG's solution is more robust than my original.

MRS It worked! Thanks!

And here is my output from RS suggestion:

 % [text .t] cget -font # running wish on console
 fixed

 % [text .t] cget -font # running TkCon
 NormalFont

ctasada 2005-07-21 I found a dummy problem in the script. If you draw boxes, but don't create any "edge" between them, the script crashes when trying to move the boxes. To solve that I modified the makeMovable proc. Replace the <B1-ButtonRelease> bind by the next piece of code:

        $c bind mv <B1-ButtonRelease> {
            %W delete withtag edge
            if {[array names ::UML::a -exact edges] != ""} {
               foreach i $::UML::a(edges) {
                  eval UML::edge %W $i
               }
            }
        }

Arts and crafts of Tcl-Tk programming