Given that this page has been quite for some time, I decided to put the finished evolved immediately code below. Below that further you can see the development history. I think this is most useful if one can grab the code and work with it in one shot. Kevin Buchs
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.
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]
# the following two are suggestions as more platform independent
set a(font) [eval font create fontNormal [font actual [defaultfont]]]
set a(boldfont) [eval font create fontBold [font actual [defaultfont]] -weight 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) {}
# lines to add saving by jima
set a(TyS,$id) box
set a(NaS,$id) $name
set a(ClS,$id) $class
set a(FiS,$id) $fill
set a(XS,$id) $x
set a(YS,$id) $y
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]
#jima added lines.
set ::UML::a(XS,$tag) [%W canvasx %X]
set ::UML::a(YS,$tag) [%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
if {[array names ::UML::a -exact edges] != ""} {
foreach i $::UML::a(edges) {
eval UML::edge %W $i
}
}
}
}
}
proc save c {
variable a
set ThRes {}
append ThRes "\n"
append ThRes "#Setting canvas...\n"
append ThRes "$c configure -width [$c cget -width] -height [$c cget -height]\n"
append ThRes "\n"
if {[array names ::UML::a -regexp {ClS,node[1-9]}] != ""} {
foreach i [array names ::UML::a -regexp {NaS,node[1-9]}] {
set ThNode [string range $i 4 end]
set ThTy $UML::a(TyS,$ThNode)
set ThNa $UML::a(NaS,$ThNode)
set ThCl $UML::a(ClS,$ThNode)
set ThFi $UML::a(FiS,$ThNode)
set ThX $UML::a(XS,$ThNode)
set ThY $UML::a(YS,$ThNode)
append ThRes "\n"
append ThRes "#Setting node ($ThTy) $ThNa\n"
append ThRes "set $ThNa \[UML::$ThTy $ThX $ThY $ThCl $ThFi\]\n"
append ThRes "\n"
}
}
if {[array names ::UML::a -exact edges] != ""} {
foreach i $::UML::a(edges) {
set ThTy [lindex $i 0]
set ThA $::UML::a(NaS,[lindex $i 1])
set ThB $::UML::a(NaS,[lindex $i 2])
append ThRes "\n"
append ThRes "#Setting edge ($ThTy) $ThA $ThB\n"
append ThRes "UML::edge $c $ThTy $ThA $ThB\n"
append ThRes "\n"
}
}
return $ThRes
}
#------- self-test code, and demo
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
}
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.
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, and demo
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
}
}
}jima(30/10/2005): Thanks RS for this toy...I tried to add saving capabilities.
For the moment I just wanted to save box objects so I devised a save proc that would pass the widget re-creation script to a string.
Just in case anyone is interested here I post my modifications:
On proc box I added some state saving lines:
set a(TyS,$id) box set a(NaS,$id) $name set a(ClS,$id) $class set a(FiS,$id) $fill set a(XS,$id) $x set a(YS,$id) $y
On proc makeMovable I added some lines to update the coordinates saved:
$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]
#jima added lines.
set ::UML::a(XS,$tag) [%W canvasx %X]
set ::UML::a(YS,$tag) [%W canvasx %Y]
}
}And, finally, the save proc:
proc save c {
variable a
set ThRes {}
append ThRes "\n"
append ThRes "#Setting canvas...\n"
append ThRes "$c configure -width [$c cget -width] -height [$c cget -height]\n"
append ThRes "\n"
if {[array names ::UML::a -regexp {ClS,node[1-9]}] != ""} {
foreach i [array names ::UML::a -regexp {NaS,node[1-9]}] {
set ThNode [string range $i 4 end]
set ThTy $UML::a(TyS,$ThNode)
set ThNa $UML::a(NaS,$ThNode)
set ThCl $UML::a(ClS,$ThNode)
set ThFi $UML::a(FiS,$ThNode)
set ThX $UML::a(XS,$ThNode)
set ThY $UML::a(YS,$ThNode)
append ThRes "\n"
append ThRes "#Setting node ($ThTy) $ThNa\n"
append ThRes "set $ThNa \[UML::$ThTy $ThX $ThY $ThCl $ThFi\]\n"
append ThRes "\n"
}
}
if {[array names ::UML::a -exact edges] != ""} {
foreach i $::UML::a(edges) {
set ThTy [lindex $i 0]
set ThA $::UML::a(NaS,[lindex $i 1])
set ThB $::UML::a(NaS,[lindex $i 2])
append ThRes "\n"
append ThRes "#Setting edge ($ThTy) $ThA $ThB\n"
append ThRes "UML::edge $c $ThTy $ThA $ThB\n"
append ThRes "\n"
}
}
return $ThRes
}Despite the horror names for the variables should more or less do it.
Add:
%W delete withtag edge
foreach i $::UML::a(edges) {eval UML::edge %W $i}to the end of the
"$c bind mv <B1-Motion> {" proc and your lines will rubber band No idea if this breaks the save/load issue.