Print Area Selector

Keith Vetter 2004-03-31 : Recently I added printing capabilities to a large tcl project using the excellent GDI package by Michael I. Schwartz [L1 ]. Initially the user could only print the entire visible part of a canvas window, but then I wanted the user to be able to select a portion of the window to print.

Thus, I needed, what I call for the lack of a better phrase, a print area selector. This is a control that lets the user select a portion of canvas that should get printed. It is a stippled rectangle which the user can resize by grabbing a corner or an edge, and move by grabbing in the middle.

The trickiest part was handling the cursor: when the mouse is on or just inside an edge or corner--in the grab zone so to speak--it should change shape. To do this required using invisible rectangles (which are made visible if you turn on debug in the demo). Also, according to the tk cursors man page [L2 ], Unix doesn't have the double headed diagonal arrow cursor (called size_nw_se and size_ne_sw on Windows).

I'm surprised that this type of control hadn't already been written--I guess this is because printing is such a pain in tcl/tk that people just don't do it. Anyway, this control, may hopefully help ease some of that pain.


##+##########################################################################
 #
 # PrintBox -- demonstrates a print area selection control
 # by Keith Vetter, March 29, 2004
 #
 # Usage:
 #   ::PrintBox::Create <canvas widget>
 #   set xy [::PrintBox::Done <canvas widget>
 #
 
 package require Tk
 
 catch {namespace delete ::PrintBox}
 namespace eval ::PrintBox {
    variable xy {}                              ;# Coordinates of print box
    variable CURSORS                            ;# Cursors to use while resizing
    variable bxy {}                             ;# Button down location
    variable bdown 0                            ;# Button is down flag
    variable minSize 150                        ;# Minimum size of print box
    variable grabSize 10                        ;# Size of "grab" area
    variable debug 0
 
    if {$::tcl_platform(platform) == "windows"} {
        array set CURSORS {
            L size_we      R size_we
            B size_ns      T size_ns
            TL size_nw_se  BR size_nw_se
            TR size_ne_sw  BL size_ne_sw
        }
    } else {
        array set CURSORS {
            L sb_h_double_arrow      R sb_h_double_arrow
            B sb_v_double_arrow      T sb_v_double_arrow
            TL top_left_corner       BR bottom_right_corner
            TR top_right_corner      BL bottom_left_corner
        }
    }
 }
 ##+##########################################################################
 # 
 # ::PrintBox::Create -- creates the print box on top of canvas W
 # 
 proc ::PrintBox::Create {W} {
    variable xy
    variable CURSORS
    variable bdown 0
 
    # Get initial location
    set w [winfo width $W]
    set h [winfo height $W]
 
    set x0 [$W canvasx 0]
    set y0 [$W canvasy 0]
    set x1 [expr {int($x0 + $w - $w / 8)}]
    set y1 [expr {int($y0 + $h - $h / 8)}]
    set x0 [expr {int($x0 + $w / 8)}]
    set y0 [expr {int($y0 + $h / 8)}]
    set xy [list $x0 $y0 $x1 $y1]
 
    # Create stubs items that ::PrintBox::Resize will size correctly
    $W delete pBox
    $W create line 0 0 1 1 -tag {pBox diag1} -width 2 -fill red
    $W create line 0 1 1 $y0 -tag {pBox diag2} -width 2 -fill red
    $W create rect 0 0 1 1 -tag {pBox pBoxx} -width 2 -outline red \
        -fill red -stipple gray25
    $W bind pBoxx <Enter> [list $W config -cursor hand2]
    $W bind pBoxx <ButtonPress-1> [list ::PrintBox::PBDown $W box %x %y]
    $W bind pBoxx <B1-Motion> [list ::PrintBox::PBMotion $W box %x %y]
 
    foreach {color1 color2} {{} {}} break
    if {$::PrintBox::debug} {
        foreach {color1 color2} {yellow blue} break
    }
 
    # Hidden rectangles that we bind to for resizing
    $W create rect 0 0 0 1 -fill $color1 -stipple gray25 -width 0 -tag {pBox L}
    $W create rect 1 0 1 1 -fill $color1 -stipple gray25 -width 0 -tag {pBox R}
    $W create rect 0 0 1 0 -fill $color1 -stipple gray25 -width 0 -tag {pBox T}
    $W create rect 0 1 1 1 -fill $color1 -stipple gray25 -width 0 -tag {pBox B}
    $W create rect 0 0 0 0 -fill $color2 -stipple gray25 -width 0 -tag {pBox TL}
    $W create rect 1 0 1 0 -fill $color2 -stipple gray25 -width 0 -tag {pBox TR}
    $W create rect 0 1 0 1 -fill $color2 -stipple gray25 -width 0 -tag {pBox BL}
    $W create rect 1 1 1 1 -fill $color2 -stipple gray25 -width 0 -tag {pBox BR}
    
    foreach tag [array names CURSORS] {
        $W bind $tag <Enter> [list ::PrintBox::PBEnter $W $tag]
        $W bind $tag <Leave> [list ::PrintBox::PBLeave $W $tag]
        $W bind $tag <B1-Motion> [list ::PrintBox::PBMotion $W $tag %x %y]
        $W bind $tag <ButtonRelease-1> [list ::PrintBox::PBUp $W $tag]
        $W bind $tag <ButtonPress-1> [list ::PrintBox::PBDown $W $tag %x %y]
    }
    
    ::PrintBox::Resize $W
 }
 ##+##########################################################################
 # 
 # ::PrintBox::Done -- kills the print box and returns its coordinates
 # 
 proc ::PrintBox::Done {W} {
    variable xy
    $W delete pBox
    return $xy
 }
 ##+##########################################################################
 # 
 # ::PrintBox::Resize -- resizes the print box to ::PrintBox::xy size
 # 
 proc ::PrintBox::Resize {W} {
    variable xy
    variable grabSize
 
    foreach {x0 y0 x1 y1} $xy break
    $W coords pBoxx $x0 $y0 $x1 $y1
    $W coords diag1 $x0 $y0 $x1 $y1
    $W coords diag2 $x1 $y0 $x0 $y1
 
    set w1 [$W itemcget pBoxx -width]           ;# NB. width extends outward
    set w2 [expr {-1 * ($w1 + $grabSize)}]
    
    foreach {x0 y0 x1 y1} [::PrintBox::GrowBox $x0 $y0 $x1 $y1 $w1] break
    foreach {x0_ y0_ x1_ y1_} [::PrintBox::GrowBox $x0 $y0 $x1 $y1 $w2] break
    $W coords L $x0 $y0_ $x0_ $y1_
    $W coords R $x1 $y0_ $x1_ $y1_
    $W coords T $x0_ $y0 $x1_ $y0_
    $W coords B $x0_ $y1 $x1_ $y1_
    $W coords TL $x0 $y0 $x0_ $y0_
    $W coords TR $x1 $y0 $x1_ $y0_
    $W coords BL $x0 $y1 $x0_ $y1_
    $W coords BR $x1 $y1 $x1_ $y1_
 
 }
 ##+##########################################################################
 # 
 # ::PrintBox::GrowBox -- grows (or shrinks) rectangle coordinates
 # 
 proc ::PrintBox::GrowBox {x0 y0 x1 y1 d} {
    list [expr {$x0-$d}] [expr {$y0-$d}] [expr {$x1+$d}] [expr {$y1+$d}]
 }
 ##+##########################################################################
 # 
 # ::PrintBox::PBDown -- handles button down in a print box
 # 
 proc ::PrintBox::PBDown {W tag x y} {
    variable bxy [list $x $y]
    variable bdown 1
 }
 ##+##########################################################################
 # 
 # ::PrintBox::PBUp -- handles button up in a print box
 # 
 proc ::PrintBox::PBUp {W tag} {
    variable bdown 0
 }
 ##+##########################################################################
 # 
 # ::PrintBox::PBEnter -- handles <Enter> in a print box
 # 
 proc ::PrintBox::PBEnter {W tag} {
    $W config -cursor $::PrintBox::CURSORS($tag)
 }
 ##+##########################################################################
 # 
 # ::PrintBox::PBLeave -- handles <Leave> in a print box
 # 
 proc ::PrintBox::PBLeave {W tag} {
    variable bdown
    if {! $bdown} {
        $W config -cursor {}
    }
 }
 ##+##########################################################################
 # 
 # ::PrintBox::PBMotion -- handles button motion, moving or resizing as needed
 # 
 proc ::PrintBox::PBMotion {W tag x y} {
    variable bxy
    variable xy
    variable minSize
 
    foreach {x0 y0 x1 y1} $xy break
    foreach {dx dy} $bxy break
    set dx [expr {$x - $dx}]
    set dy [expr {$y - $dy}]
    
    set w [winfo width $W]
    set h [winfo height $W]
    set wx0 [$W canvasx 0]
    set wy0 [$W canvasy 0]
    set wx1 [$W canvasx $w]
    set wy1 [$W canvasy $h]
 
    if {$tag eq "box"} {                        ;# Move the print box
        if {$x0 + $dx < $wx0} {set dx [expr {$wx0 - $x0}]}
        if {$x1 + $dx > $wx1} {set dx [expr {$wx1 - $x1}]}
        if {$y0 + $dy < $wy0} {set dy [expr {$wy0 - $y0}]}
        if {$y1 + $dy > $wy1} {set dy [expr {$wy1 - $y1}]}
 
        set x0 [expr {$x0 + $dx}]
        set x1 [expr {$x1 + $dx}]
        set y0 [expr {$y0 + $dy}]
        set y1 [expr {$y1 + $dy}]
        
        set xy [list $x0 $y0 $x1 $y1]
        set bxy [list $x $y]
    } else {                                    ;# Resize the print box
        if {$tag eq "L" || $tag eq "TL" || $tag eq "BL"} {
            set x0_ [expr {$x0 + $dx}]
            if {$x0_ < $wx0} {
                lset xy 0 $wx0
                lset bxy 0 0
            } elseif {$x1 - $x0_ >= $minSize} {
                lset xy 0 $x0_
                lset bxy 0 $x
            }
        }
        if {$tag eq "R" || $tag eq "TR" || $tag eq "BR"} {
            set x1_ [expr {$x1 + $dx}]
            if {$x1_ > $wx1} {
                lset xy 2 $wx1
                lset bxy 0 $w
            } elseif {$x1_ - $x0 >= $minSize} {
                lset xy 2 $x1_
                lset bxy 0 $x
            }
        }
        if {$tag eq "T" || $tag eq "TR" || $tag eq "TL"} {
            set y0_ [expr {$y0 + $dy}]
            if {$y0_ < $wy0} {
                lset xy 1 $wy0
                lset bxy 1 0
            } elseif {$y1 - $y0_ >= $minSize} {
                lset xy 1 $y0_
                lset bxy 1 $y
            }
        }
        if {$tag eq "B" || $tag eq "BR" || $tag eq "BL"} {
            set y1_ [expr {$y1 + $dy}]
            if {$y1_ > $wy1} {
                lset xy 3 $wy1
                lset bxy 1 $h
            } elseif {$y1_ - $y0 > $minSize} {
                lset xy 3 $y1_
                lset bxy 1 $y
            }
        }
    }
    ::PrintBox::Resize $W
 }
 
 ################################################################
 #
 # DEMO CODE
 #
 
 wm title . "Print Box Demo"
 wm resizable . 0 0
 canvas .c -width 500 -height 500 -bg lightyellow
 pack [frame .ctrl -relief ridge -bd 2 -padx 5 -pady 5] \
    -side right -fill both -ipady 5
 pack .c -side left -fill both -expand 1
 
 for {set i 0} {$i < 20} {incr i} {
    set xy {}
    foreach _ {1 2 3 4} { lappend xy [expr {rand() * 700 - 100}] }
    set color [format "\#%06x" [expr {int(rand() * 0xFFFFFF)}]]
    set type [expr {rand() < .5 ? "oval" : "rect"}]
    set width [expr {rand() * 8 + 2}]
    .c create $type $xy -fill $color -width $width
 }
 
 checkbutton .ctrl.onoff -text "Print Box" -variable S(onoff) -anchor w \
    -command OnOff 
 checkbutton .ctrl.debug -text "Debug" -variable S(debug) -anchor w \
    -command DebugToggle
 label .ctrl.lxy -text "\nCoordinates"
 label .ctrl.xy -textvariable ::PrintBox::xy -bd 2 -bg white -relief sunken \
    -width 15
 eval pack [winfo child .ctrl] -side top -fill x -anchor w
 button .ctrl.about -text About -command \
    [list tk_messageBox -message "Print Box Demo\nby Keith Vetter, March 2004"]
 pack .ctrl.about -side bottom
 
 
 proc OnOff {} {
    if {$::S(onoff)} {
        ::PrintBox::Create .c
    } else {
        ::PrintBox::Done .c
    }
 }
 proc DebugToggle {} {
    set xy $::PrintBox::xy
    set ::PrintBox::debug $::S(debug)
    if {$::S(onoff)} {
        ::PrintBox::Done .c
        ::PrintBox::Create .c
        set ::PrintBox::xy $xy
        ::PrintBox::Resize .c
    }
 }
 
 update
 set S(onoff) 1
 ::PrintBox::Create .c
 
 return