Version 2 of 3-D Boxes (Support for Canvas Buttons in 3-D)

Updated 2010-08-23 17:32:19 by AKgnome

These are the support routines for Canvas Buttons in 3-D.

These routines can also be used to make other things. The corners and sides are all separate, so one could build anything rectangular and 3-D on a canvas. A combobox would be interesting.

Here's a link to the Canvas Buttons in 3-D, that use these routines.

Canvas Buttons in 3-D


 proc CreateTopLeftCorner {theCanvas x y height args} {

     global tlcorner

     set id [$theCanvas create rectangle 0 0 1 1]

     set tlcorner($id,theCanvas) $theCanvas
     set tlcorner($id,adjustedHeight) [expr round(0.707 * $height)]
     # I'd rather deal with integers. 
     set tlcorner($id,x) $x
     set tlcorner($id,y) $y

     set tlcorner($id,color) white
     set tags ""
     set state normal

     foreach {option value} $args {

         switch -- $option {

             -color {

                 set tlcorner($id,color) $value

                 # No checking at present that color value is valid. 

             }

             -tag - 
             -tags {

                 set tags $value

             }

             -state {

                 switch -- $value {

                     normal {

                         set state normal

                     }

                     hidden {

                         set state hidden

                     }

                     default {

                         tk_messageBox -message "CreateTopLeftCorner: \
                          Unrecognized state value:  $value" -type ok

                     }

                 }

             }

             default {

                 tk_messageBox -message "CreateTopLeftCorner:  Unrecognized\
                  option:  $option" -type ok

             }

         }

     }

     set ah $tlcorner($id,adjustedHeight)
     set ax [expr $x + $ah]  ;# Altered x. 
     set ay [expr $y + $ah]  ;# Altered y. 

     $theCanvas coords $id $x $y $ax $ay

     set c $tlcorner($id,color)

     $theCanvas itemconfig $id -fill $c -outline $c -tags $tags -state $state
     # Both the fill and the outline need to be set, for rectangle 
     # to match polygon. 

     return $id

     # Hopefully, the caller won't keep the id long, but will assign 
     # a tag to the assemblage. 

 }

 # Since I don't want to make a separate routine to set the state for each 
 # piece, and I don't want the rectangle to have to directly twiddle variables 
 # belonging to a piece, I don't make the state of the canvas item, hidden or 
 # normal, part of the state of the piece. 


 proc CreateLeftSide {theCanvas x ty by height args} {

     global lside

     set id [$theCanvas create rectangle 0 0 1 1]

     set lside($id,theCanvas) $theCanvas
     set lside($id,adjustedHeight) [expr round(0.707 * $height)]
     set lside($id,x) $x
     set lside($id,ty) $ty
     set lside($id,by) $by

     set lside($id,color) white
     set tags ""
     set state normal

     foreach {option value} $args {

         switch -- $option {

             -color {set lside($id,color) $value}

             -tag - -tags {set tags $value}

             -state {
                 switch -- $value {
                     normal {set state normal}
                     hidden {set state hidden}
                     default {tk_messageBox -message "CreateLeftSide: \
                      Unrecognized state value:  $value" -type ok}
                 }
             }

             default {tk_messageBox -message "CreateLeftSide: \
              Unrecognized option:  $option" -type ok}

         }

     }

     set ah $lside($id,adjustedHeight)
     set ax [expr $x + $ah]
     set aty [expr $ty + $ah]
     set aby [expr $by - $ah]

     $theCanvas coords $id $x $aty $ax $aby

     set c $lside($id,color)

     $theCanvas itemconfig $id -fill $c -outline $c -tags $tags -state $state

     return $id

 }


 proc CreateBottomLeftCorner {theCanvas x y height args} {

     global blcorner

     set id [$theCanvas create polygon 0 0 1 0 1 1]

     set blcorner($id,theCanvas) $theCanvas
     set blcorner($id,adjustedHeight) [expr round(0.707 * $height)]
     set blcorner($id,x) $x
     set blcorner($id,y) $y

     set blcorner($id,color) white
     set tags ""
     set state normal

     foreach {option value} $args {

         switch -- $option {

             -color {set blcorner($id,color) $value}

             -tag - -tags {set tags $value}

             -state {
                 switch -- $value {
                     normal {set state normal}
                     hidden {set state hidden}
                     default {tk_messageBox -message "CreateBottomLeftCorner: \
                      Unrecognized state value:  $value" -type ok}
                 }
             }

             default {tk_messageBox -message "CreateBottomLeftCorner: \
              Unrecognized option:  $option" -type ok}

         }

     }

     set ah $blcorner($id,adjustedHeight)
     set ax [expr $x + $ah]
     set by [expr $y + $ah]
     set ty [expr $y - $ah]

     $theCanvas coords $id $x $y $ax $by $ax $ty $x $ty

     set c $blcorner($id,color)

     $theCanvas itemconfig $id -fill $c -outline $c -tags $tags -state $state

     return $id

 }

 # We let the corner manage the territory a little way up the side, 
 # so if we ever decide to allow the customer to change the perspective 
 # so that he is to the left and below the button, instead of to the left 
 # and above, we can. 
 # 
 # This seems as good a place as any to mention that if the height of the 
 # button or whatever gets to be an appreciable fraction of the width and 
 # breadth of the button, these routines will fail. 


 proc CreateTopSide {theCanvas lx rx y height args} {

     global tside

     set id [$theCanvas create rectangle 0 0 1 1]

     set tside($id,theCanvas) $theCanvas
     set tside($id,adjustedHeight) [expr round(0.707 * $height)]
     set tside($id,lx) $lx
     set tside($id,rx) $rx
     set tside($id,y) $y

     set tside($id,color) white
     set tags ""
     set state normal

     foreach {option value} $args {

         switch -- $option {

             -color {set tside($id,color) $value}

             -tag - -tags {set tags $value}

             -state {
                 switch -- $value {
                     normal {set state normal}
                     hidden {set state hidden}
                     default {tk_messageBox -message "CreateTopSide: \
                      Unrecognized state value:  $value" -type ok}
                 }
             }

             default {tk_messageBox -message "CreateTopSide: \
              Unrecognized option:  $option" -type ok}

         }

     }

     set ah $tside($id,adjustedHeight)
     set alx [expr $lx + $ah]
     set arx [expr $rx - $ah]
     set ay [expr $y + $ah]

     $theCanvas coords $id $alx $y $arx $ay

     set c $tside($id,color)

     $theCanvas itemconfig $id -fill $c -outline $c -tags $tags -state $state

     return $id

 }


 proc CreateTopRightCorner {theCanvas x y height args} {

     global trcorner

     set id [$theCanvas create polygon 0 0 1 0 1 1]

     set trcorner($id,theCanvas) $theCanvas
     set trcorner($id,adjustedHeight) [expr round(0.707 * $height)]
     set trcorner($id,x) $x
     set trcorner($id,y) $y

     set trcorner($id,color) white
     set tags ""
     set state normal

     foreach {option value} $args {

         switch -- $option {

             -color {set trcorner($id,color) $value}

             -tag - -tags {set tags $value}

             -state {
                 switch -- $value {
                     normal {set state normal}
                     hidden {set state hidden}
                     default {tk_messageBox -message "CreateTopRightCorner: \
                      Unrecognized state value:  $value" -type ok}
                 }
             }

             default {tk_messageBox -message "CreateTopRightCorner: \
              Unrecognized option:  $option" -type ok}

         }

     }

     set ah $trcorner($id,adjustedHeight)
     set arx [expr $x + $ah]
     set alx [expr $x - $ah]
     set ay  [expr $y + $ah]

     $theCanvas coords $id $x $y $alx $y $alx $ay $arx $ay

     set c $trcorner($id,color)

     $theCanvas itemconfig $id -fill $c -outline $c -tags $tags -state $state

     return $id

 }


 proc CreateBottomSide {theCanvas lx rx y height args} {

     global bside

     set id [$theCanvas create rectangle 0 0 1 1]

     set bside($id,theCanvas) $theCanvas
     set bside($id,adjustedHeight) [expr round(0.707 * $height)]
     set bside($id,lx) $lx
     set bside($id,rx) $rx
     set bside($id,y) $y

     set bside($id,color) white
     set tags ""
     set state normal

     foreach {option value} $args {

         switch -- $option {

             -color {set bside($id,color) $value}

             -tag - -tags {set tags $value}

             -state {
                 switch -- $value {
                     normal {set state normal}
                     hidden {set state hidden}
                     default {tk_messageBox -message "CreateBottomSide: \
                      Unrecognized state value:  $value" -type ok}
                 }
             }

             default {tk_messageBox -message "CreateBottomSide: \
              Unrecognized option:  $option" -type ok}

         }

     }

     set ah $bside($id,adjustedHeight)
     set arx [expr $rx - $ah]
     set ay [expr $y - $ah]

     $theCanvas coords $id $lx $y $arx $ay

     set c $bside($id,color)

     $theCanvas itemconfig $id -fill $c -outline $c -tags $tags -state $state

     return $id

 }

 # Logically, the corner owns the territory out an adjustedHeight distance 
 # from the vertex.  But I can't bring myself to make a separate little 
 # corner when it won't be visible.  So I extend this side, and the right 
 # side, all the way to the vertex. 


 proc CreateBottomRightCorner {theCanvas x y height args} {

     global brcorner

     set id [$theCanvas create rectangle 0 0 1 1]

     set brcorner($id,theCanvas) $theCanvas
     set brcorner($id,adjustedHeight) [expr round(0.707 * $height)]
     set brcorner($id,x) $x
     set brcorner($id,y) $y

     set brcorner($id,color) white
     set tags ""
     set state normal

     foreach {option value} $args {

         switch -- $option {

             -color {set brcorner($id,color) $value}

             -tag - -tags {set tags $value}

             -state {
                 switch -- $value {
                     normal {set state normal}
                     hidden {set state hidden}
                     default {tk_messageBox -message "CreateBottomRightCorner: \
                      Unrecognized state value:  $value" -type ok}
                 }
             }

             default {tk_messageBox -message "CreateBottomRightCorner: \
              Unrecognized option:  $option" -type ok}

         }

     }

     set ah $brcorner($id,adjustedHeight)
     set ax [expr $x - $ah]
     set ay [expr $y - $ah]

     $theCanvas coords $id $x $y $ax $ay

     set c $brcorner($id,color)

     $theCanvas itemconfig $id -fill $c -outline $c -tags $tags -state $state

     return $id

 }


 proc CreateRightSide {theCanvas x ty by height args} {

     global rside

     set id [$theCanvas create rectangle 0 0 1 1]

     set rside($id,theCanvas) $theCanvas
     set rside($id,adjustedHeight) [expr round(0.707 * $height)]
     set rside($id,x) $x
     set rside($id,ty) $ty
     set rside($id,by) $by

     set rside($id,color) white
     set tags ""
     set state normal

     foreach {option value} $args {

         switch -- $option {

             -color {set rside($id,color) $value}

             -tag - -tags {set tags $value}

             -state {
                 switch -- $value {
                     normal {set state normal}
                     hidden {set state hidden}
                     default {tk_messageBox -message "CreateRightSide: \
                      Unrecognized state value:  $value" -type ok}
                 }
             }

             default {tk_messageBox -message "CreateRightSide: \
              Unrecognized option:  $option" -type ok}

         }

     }

     set ah $rside($id,adjustedHeight)
     set ax [expr $x - $ah]
     set aby [expr $by - $ah]

     $theCanvas coords $id $x $aby $ax $ty

     set c $rside($id,color)

     $theCanvas itemconfig $id -fill $c -outline $c -tags $tags -state $state

     return $id

 }


 proc DestroyTopLeftCorner id {

     global tlcorner

     # I thought about converting from a tag to an id, if the value passed 
     # is not numeric, but how are you going to get the canvas name? 

     $tlcorner($id,theCanvas) delete $id

     unset tlcorner($id,theCanvas)
     unset tlcorner($id,adjustedHeight)
     unset tlcorner($id,x)
     unset tlcorner($id,y)
     unset tlcorner($id,color)

 }


 # Rather than have a procedure "ShowTopLeftCorner" and one "HideTopLeftCorner" 
 # I think I'll have the orchestrator configure state using a tag that refers 
 # to all the parts. 


 proc DestroyLeftSide id {

     global lside

     $lside($id,theCanvas) delete $id

     unset lside($id,theCanvas)
     unset lside($id,adjustedHeight)
     unset lside($id,x)
     unset lside($id,ty)
     unset lside($id,by)
     unset lside($id,color)

 }


 proc DestroyBottomLeftCorner id {

     global blcorner

     $blcorner($id,theCanvas) delete $id

     unset blcorner($id,theCanvas)
     unset blcorner($id,adjustedHeight)
     unset blcorner($id,x)
     unset blcorner($id,y)
     unset blcorner($id,color)

 }


 proc DestroyTopSide id {

     global tside

     $tside($id,theCanvas) delete $id

     unset tside($id,theCanvas)
     unset tside($id,adjustedHeight)
     unset tside($id,lx)
     unset tside($id,rx)
     unset tside($id,y)
     unset tside($id,color)

 }


 proc DestroyTopRightCorner id {

     global trcorner

     $trcorner($id,theCanvas) delete $id

     unset trcorner($id,theCanvas)
     unset trcorner($id,adjustedHeight)
     unset trcorner($id,x)
     unset trcorner($id,y)
     unset trcorner($id,color)

 }


 proc DestroyBottomSide id {

     global bside

     $bside($id,theCanvas) delete $id

     unset bside($id,theCanvas)
     unset bside($id,adjustedHeight)
     unset bside($id,lx)
     unset bside($id,rx)
     unset bside($id,y)
     unset bside($id,color)

 }


 proc DestroyBottomRightCorner id {

     global brcorner

     $brcorner($id,theCanvas) delete $id

     unset brcorner($id,theCanvas)
     unset brcorner($id,adjustedHeight)
     unset brcorner($id,x)
     unset brcorner($id,y)
     unset brcorner($id,color)

 }


 proc DestroyRightSide id {

     global rside

     $rside($id,theCanvas) delete $id

     unset rside($id,theCanvas)
     unset rside($id,adjustedHeight)
     unset rside($id,x)
     unset rside($id,ty)
     unset rside($id,by)
     unset rside($id,color)

 }


 # --------------------- Above are the pieces of a rectangle -------------------
 #                       to be assembled in different ways. 


 # Some options that might be confusing are: 
 # 
 # dx, dy - If you have something written on the surface of your box 
 # (for example if the box is a button and the top of it says "Quit") 
 # as the box is raised and recessed, the text or image should be moved 
 # with a canvas move command.  Dx and dy tell you how much to move your 
 # image over.  They are relative to if you had written your image directly 
 # in the surface, without being raised above it or lowered below it.  
 # The names of the variables where you would like the values placed are 
 # passed using these options. 

 proc Create3DBox {theCanvas lx ty rx by height args} {

     global box3d

     set id [$theCanvas create rectangle 0 0 1 1]

     set box3d($id,theCanvas) $theCanvas
     set box3d($id,adjustedHeight) [expr round(0.707 * $height)]
     set box3d($id,lx) $lx
     set box3d($id,ty) $ty
     set box3d($id,rx) $rx
     set box3d($id,by) $by

     set box3d($id,outSideColor) white
     set box3d($id,inSideColor) white
     set box3d($id,topFillColor) gray
     set box3d($id,topOutlineColor) black
     set box3d($id,relief) raised
     set dxname ""
     set dyname ""
     set tags ""

     foreach {option value} $args {

         switch -- $option {

             -outsidecolor {

                 set box3d($id,outSideColor) $value

                 # No checking that color value is valid. 

             }

             -insidecolor {

                 set box3d($id,inSideColor) $value

             }

             -topfillcolor {

                 set box3d($id,topFillColor) $value

             }

             -topoutlinecolor {

                 set box3d($id,topOutlineColor) $value

             }

             -relief {

                 switch -- $value {

                     raised {

                         set box3d($id,relief) raised

                     }

                     sunken - 
                     recessed {

                         set box3d($id,relief) recessed

                         # Tk users are likely to say sunken. 

                     }

                     flat {

                         set box3d($id,relief) flat

                     }

                     default {

                         tk_messageBox -message "Create3DBox: \
                          Unrecognized relief value:  $value" -type ok

                     }

                 }

             }

             -dx {

                 set dxname $value

             }

             -dy {

                 set dyname $value

             }

             -tag - 
             -tags {

                 set tags $value

             }

             default {

                 tk_messageBox -message "Create3DBox:  Unrecognized\
                  option:  $option" -type ok

             }

         }

     }

     set osc $box3d($id,outSideColor)
     set isc $box3d($id,inSideColor)
     set tfc  $box3d($id,topFillColor)
     set toc  $box3d($id,topOutlineColor)

     lappend tags box3d$id

     set box3d($id,blcid) [CreateBottomLeftCorner $theCanvas $lx $by \
      $height -color $osc -tags $tags]

     set box3d($id,lsid) [CreateLeftSide $theCanvas $lx $ty $by $height \
      -color $osc -tags $tags]

     set box3d($id,tlcid) [CreateTopLeftCorner $theCanvas $lx $ty $height \
      -color $osc -tags $tags]

     set box3d($id,tsid) [CreateTopSide $theCanvas $lx $rx $ty $height \
      -color $osc -tags $tags]

     set box3d($id,trcid) [CreateTopRightCorner $theCanvas $rx $ty $height \
      -color $osc -tags $tags]

     set box3d($id,bsid) [CreateBottomSide $theCanvas $lx $rx $by $height \
      -color $isc -tags $tags]

     set box3d($id,brcid) [CreateBottomRightCorner $theCanvas $rx $by \
      $height -color $isc -tags $tags]

     set box3d($id,rsid) [CreateRightSide $theCanvas $rx $ty $by $height \
      -color $isc -tags $tags]

     $theCanvas itemconfig $id -outline $toc -fill $tfc -tags $tags

     Draw3DBox $id localdx localdy

     if {$dxname != ""} {

         upvar $dxname dx

         set dx $localdx

     }

     if {$dyname != ""} {

         upvar $dyname dy

         set dy $localdy

     }

     return $id

 }

 # First we set tags to an empty string, then set it with any list of tags, 
 # then appended our required tag.  Thus we kept it a one level list. 
 # 
 # Typically, Create3DBox will be called with only one tag, but it it becomes 
 # part of a larger assemblage later, this may be useful. 


 proc Destroy3DBox id {

     global box3d

     DestroyBottomLeftCorner $box3d($id,blcid)
     DestroyLeftSide $box3d($id,lsid)
     DestroyTopLeftCorner $box3d($id,tlcid)
     DestroyTopSide $box3d($id,tsid)
     DestroyTopRightCorner $box3d($id,trcid)
     DestroyRightSide $box3d($id,rsid)
     DestroyBottomRightCorner $box3d($id,brcid)
     DestroyBottomSide $box3d($id,bsid)

     $box3d($id,theCanvas) delete $id

     unset box3d($id,theCanvas)
     unset box3d($id,adjustedHeight)
     unset box3d($id,lx)
     unset box3d($id,ty)
     unset box3d($id,rx)
     unset box3d($id,by)

     unset box3d($id,outSideColor)
     unset box3d($id,inSideColor)
     unset box3d($id,topFillColor)
     unset box3d($id,topOutlineColor)
     unset box3d($id,relief)

     unset box3d($id,blcid)
     unset box3d($id,lsid)
     unset box3d($id,tlcid)
     unset box3d($id,tsid)
     unset box3d($id,trcid)
     unset box3d($id,rsid)
     unset box3d($id,brcid)
     unset box3d($id,bsid)

 }

 # Seems best to always have all the parts of a rectangle exists, if the 
 # rectangle exists.  Some of the parts may have the state "hidden".  
 # Otherwise, we have to create and delete things when the button pops up, 
 # or create things only when they are first called for, and keep track 
 # of whether the piece exists yet. 
 # 
 # I'll put this here arbitrarily.  It seems unfortunate to calculate the 
 # adjusted height from the passed height of the rectangle in multiple 
 # different places.  What if one calculates it slightly differently?  
 # Also, it seems silly to put the same code to handle an option like 
 # -state in each routine.  If I were using something like XOTcl, I would 
 # handle the options the specialized object knows about, and then finish 
 # my routine in the parent object with the next command. 


 proc Draw3DBox {id dxname dyname} {

     global box3d

     set c   $box3d($id,theCanvas)
     set lx  $box3d($id,lx)
     set ty  $box3d($id,ty)
     set rx  $box3d($id,rx)
     set by  $box3d($id,by)
     set ah  $box3d($id,adjustedHeight)
     set osc $box3d($id,outSideColor)
     set isc $box3d($id,inSideColor)
     set tfc $box3d($id,topFillColor)
     set toc $box3d($id,topOutlineColor)
     set r   $box3d($id,relief)

     upvar $dxname dx
     upvar $dyname dy

     if {$r == "raised"} {
         set ulstate normal
         set lrstate hidden
     } elseif {$r == "recessed"} {
         set ulstate hidden
         set lrstate normal
     } else {  ;# Flat
         set ulstate hidden
         set lrstate hidden
     }

     $c itemconfig $box3d($id,blcid) -state $ulstate
     $c itemconfig $box3d($id,lsid) -state $ulstate
     $c itemconfig $box3d($id,tlcid) -state $ulstate
     $c itemconfig $box3d($id,tsid) -state $ulstate
     $c itemconfig $box3d($id,trcid) -state $ulstate
     $c itemconfig $box3d($id,rsid) -state $lrstate
     $c itemconfig $box3d($id,brcid) -state $lrstate
     $c itemconfig $box3d($id,bsid) -state $lrstate

     if {$r == "raised"} {

         $c coords $id [expr $lx + $ah] [expr $ty + $ah] [expr $rx + $ah] \
          [expr $by + $ah]

         $c lower $id box3d$id
         # Probably could just lower below everything, but if sometime in 
         # the future the 3d box is part of a more complicated assemblage, 
         # it will be good to stay with my group. 
         # 
         # Lowered so that outline will only be visible where top meets 
         # background, not where it meets sides. 

         set dx $ah
         set dy $ah

     } elseif {$r == "recessed"} {

         $c coords $id $lx $ty [expr $rx - $ah] [expr $by - $ah]

         $c lower $id box3d$id

         set dx -$ah
         set dy -$ah

     } else {  ;# Flat

         $c coords $id $lx $ty $rx $by

         set dx 0
         set dy 0

     }

 }


 proc Set3DBoxRelief {id relief args} {

     global box3d

     if {![info exists box3d($id,theCanvas)]} {

         tk_messageBox -message "Set3DBoxRelief:  no such id:  $id" -type ok

     }

     # I'm not going to check that the relief value is legal each time.  
     # The user may be waiting for his button to go down. 

     if {$relief == "sunken"} {set relief recessed}

     if {$box3d($id,relief) == $relief} {

         set ah $box3d($id,adjustedHeight)

         if {$relief == "raised"} {
             set localdx $ah
             set localdy $ah
         } elseif {$relief == "recessed"} {
             set localdx -$ah
             set localdy -$ah
         } else {
             set localdx 0 
             set localdy 0 
         }

     } else {

         set box3d($id,relief) $relief

         Draw3DBox $id localdx localdy

     }

     # Set3DBoxRelief really shouldn't know things about drawing, like that 
     # if the relief is raised, any label on top of the button should be 
     # moved down and right by adjustedHeight amount.  I could always call 
     # Draw3DBox.  I guess I'll regard this knowledge on Set3DBoxRelief's 
     # part as an optimization. 

     set dxname ""
     set dyname ""

     foreach {option value} $args {

         switch -- $option {

             -dx {set dxname $value}

             -dy {set dyname $value}

             # Not checking for unrecognized option. 

         }

     }

     if {$dxname != ""} {

         upvar $dxname dx

         set dx $localdx

     }

     if {$dyname != ""} {

         upvar $dyname dy

         set dy $localdy

     }

 }


 # It might be desired to bind to the whole box, not to each of its subparts. 
 # One way to accomplish this might be to put a polygon over the top of the box. 
 # You can give the polygon a fill color and outline color of the empty string. 

 proc Get3DBoxPolygon {id} {

     global box3d

     set coords {}

     set ah $box3d($id,adjustedHeight)
     set lx $box3d($id,lx)
     set ty $box3d($id,ty)
     set rx $box3d($id,rx)
     set by $box3d($id,by)

     lappend coords $lx $ty $rx $ty [expr $rx + $ah] [expr $ty + $ah]

     lappend coords [expr $rx + $ah] [expr $by + $ah]

     lappend coords [expr $lx + $ah] [expr $by + $ah] $lx $by

     return $coords
 }