Mini windows (AKA 'multiple document interface')

Difference between version 2 and 7 - Previous - Next
[Mini windows example picture 2]

For an application I'm working on, I needed to implement mini windows inside other windows, in the style of "MDI" that used to be common in Microsoft Windows applications in the 90s. Here's what I came up with.

======
package require Tk

namespace eval miniwin {
 # Set the title of a 'miniwindow'
 proc setWindowTitle {window title} {
  $window.inside.title.l configure -text $title
 }
 # Show a miniwindow (if it was previously hidden)
 proc showWindow {window} {
  place $window
 }
 # Hide a miniwindow
 proc hideWindow {window} {
  place forget $window
 }
 # Set the x,y position of a miniwindow
 proc setWindowPosition {window x y} {
  place configure $window -x $x -y $y 
 }
 # Set the width and height of a miniwindow
 proc setWindowSize {window w h} {
  place configure $window -width $w -height $h 
 }
 # Maximise a miniwindow
 proc maximiseWindow {window} {
  raise $window
  set root [string range $window 0 [string last "." $window]]
  set w [winfo width $root]
  set h [winfo height $root]
  ::miniwin::setWindowPosition $window 0 0
  ::miniwin::setWindowSize $window $w $h
 }
 # Minimise a miniwindow
 proc minimiseWindow {window} {
  set root [string range $window 0 [string last "." $window]]
  set w [winfo width $root]
  set h [winfo height $root]
  set titleHeight [winfo reqheight $window.inside.title.l]
  incr titleHeight 17
  ::miniwin::setWindowPosition $window 0 [expr {$h - $titleHeight}]
  ::miniwin::setWindowSize $window [expr {64+$titleHeight}] $titleHeight
  lower $window
 }
 # Maximise, or restore if already maximised
 proc maxRestore {window} {
  set root [string range $window 0 [string last "." $window]]
  set w [winfo width $root]
  set h [winfo height $root]  
  maximiseWindow $window
  if { $w == [winfo width $window] || $h == [winfo height $window] } {
   ::miniwin::fitWindow $window
   ::miniwin::setWindowPosition $window \
    [expr {$w/2-[::miniwin::getWindowWidth $window]/2} ] \
    [expr {$h/2-[::miniwin::getWindowHeight $window]/2} ]
  }
 }
 # Fit a window to its requested size (roughly)
 proc getWindowWidth {w} {expr {[winfo reqwidth $w.inside.contents] + 12 }}
 proc getWindowHeight {w} {expr {[winfo reqheight $w.inside.contents] + 12 + [winfo reqheight $w.inside.title] }}
 proc fitWindow {w} {
  ::miniwin::setWindowSize $w \
   [::miniwin::getWindowWidth $w] \
   [::miniwin::getWindowHeight $w]
 }
 # Cascade all the miniwindows in a root window
 proc cascade {root} {
  set xy 0
  foreach w [winfo children $root] {
   if { [info exists ::miniwin::$w.exists] } {
    raise $w
    ::miniwin::setWindowPosition $w $xy $xy
    incr xy 32
   }
  }
 }
 # Fit all miniwindows to their requested size (roughly)
 proc fitAllWindows {root} {
  foreach w [info vars ::miniwin::$root*.exists] {
   set w [set $w]
   fitWindow $w
  }
 }
 # Destroy a miniwindow
 proc destroyWindow {window} {
  if { ! [info exists ::miniwin::$window.exists] } {
   error "destroyWindow: '$window' is not a valid path or not a 'miniwindow'"
  }
  unset ::miniwin::$window.exists
  unset ::miniwin::$window.resizing
  unset ::miniwin::$window.dragging
  destroy $window
 }
 # Create a miniwindow
 proc makeWindow {window args} {
  # ---------------------------
  # Information about the structure of a 'miniwindow'
  # ---------------------------
  # $window                        The window frame itself. This is visible as the border around the window which allows resizing
  # $window.inside                Everything inside the window
  # $window.inside.title        The title bar
  # $window.inside.title.l         The label showing the title of the window
  # $window.inside.title.b         This frame contains the window control buttons
  # $window.inside.contents        The contents, where you put your stuff
  # ---------------------------

  # ---------------------------
  # Values supported by 'args'
  # ---------------------------
  # buttons=111                where '111' is a bit mask that enables or disables the '_OX' control buttons
  # ---------------------------
  set buttonEnableMask {1 1 1}
  set index [lsearch $args "buttons=*"]
  if { $index != -1 } {
   set buttonEnableMask [lmap i [split [lindex [split [lindex $args $index] "="] 1] ""] {if {$i ne "0"} {set i 1}; set i}]
   if { [llength $buttonEnableMask] != 3 } {
    error "bad button enable mask: '[lindex $args $index]'"
   }
   lset args $index "_ARG_USED_"
  }
  # ---------------------------
  # title=text                where 'text' is the title of the window
  # ---------------------------
  set titleString ""
  set index [lsearch $args "title=*"]
  if { $index != -1 } {
   set titleString [lindex [split [lindex $args $index] "="] 1]
   lset args $index "_ARG_USED_"
  }
  # check all args were valid
  foreach i $args {
   if {$i ne "_ARG_USED_"} {error "unknown argument '$i'"}
  }
  # ---------------------------

  # Create the window
  frame $window -borderwidth 1 -relief raised -background white
 
  # Create the inside of the window
  set inside $window.inside
  pack [frame $inside -padx 6 -pady 6] -fill both -expand 1

  # Create the title bar
  set title [frame $inside.title -relief sunken -borderwidth 1]
  set n 0
  text ._temporary_
  set titleFG [._temporary_ cget -selectforeground]
  set titleBG [._temporary_ cget -selectbackground]
  destroy ._temporary_
  # Create and mount the titlebar components.
  # Text label to display window title
  grid configure [label $title.l -text $window -background $titleBG -foreground $titleFG -borderwidth 1 -relief raised] -column 0 -row 0 -sticky nsew
  grid columnconfigure $title $n -weight 1
  grid rowconfigure $title 0 -weight 1
  # Frame to contain the window control buttons
  grid configure [frame $title.b -borderwidth 1 -relief sunken] -column 1 -row 0
  # Creation of the window control buttons
  foreach {item buttonLabel command} [list \
   min "_" "::miniwin::minimiseWindow $window" \
   max "O" "::miniwin::maxRestore $window" \
   clo "X" "::miniwin::destroyWindow $window" \
  ] enable $buttonEnableMask {
   set thisItem $title.b.$item
   label $thisItem -text "$buttonLabel"
   if {$enable} {pack $thisItem -side left}
   bind $thisItem <ButtonRelease-1> $command
  }
  # Create the contents frame
  set contents [frame $inside.contents -relief sunken -borderwidth 1]
  # Mount all the components
  pack $title -side top -fill x 
  pack $contents -side bottom -fill both -expand 1
  pack propagate $inside 0
  place $window -x 0 -y 0 -width 100 -height 100


  set ::miniwin::x 0
  set ::miniwin::y 0

  # --------------------------
  # Window management bindings
  # --------------------------

  # -----------------
  # Titlebar bindings
  # -----------------
  set stringMap [list WIN $window]
  foreach i [list $title.l $title] {
 
   # Middle click title or frame to lower
   bind $i <ButtonPress-2> [string map $stringMap {
    lower WIN
   }]
  
   # Left click title to start dragging (moving)
   # Also, left click title or frame to raise
   bind $i <ButtonPress-1> [string map $stringMap {
    set ::miniwin::x %X
    set ::miniwin::y %Y
    set ::miniwin::WIN.dragging 1
    raise WIN
   }]
   # Release left click to stop dragging (moving)
   bind $i <ButtonRelease-1> [string map $stringMap {
    set ::miniwin::WIN.dragging 0
   }]

   # titlebar drag to move
   bind $i <Motion> [string map $stringMap {
    if {[set ::miniwin::WIN.dragging]} {
     set ::miniwin::ox $::miniwin::x
     set ::miniwin::oy $::miniwin::y
     set ::miniwin::x %X
     set ::miniwin::y %Y
     set dx [expr {$::miniwin::x - $::miniwin::ox}]
     set dy [expr {$::miniwin::y - $::miniwin::oy}]
     place configure WIN -x [expr {[lindex [place configure WIN -x ] end]+$dx}] -y [expr {[lindex [place configure WIN -y] end]+$dy}]
    }
   }]

  }
  # -----------------

  # ----------------------------------
  # Window frame middle click to lower
  # ----------------------------------
  bind $inside <ButtonPress-2> [string map $stringMap {
   lower WIN
  }]
  # ----------------------------------

  # ----------------------------------
  # Window frame drag to resize
  # ----------------------------------
  set ::miniwin::x2 0
  set ::miniwin::y2 0
  bind $inside <ButtonPress-1> [string map $stringMap {
   set cx1 [lindex [place configure WIN -x] end]
   set cy1 [lindex [place configure WIN -y] end]
   set cx2 [lindex [place configure WIN -width] end]
   set cy2 [lindex [place configure WIN -height] end]
   incr cx2 $cx1
   incr cx2 -8
   incr cy2 $cy1
   incr cy2 -8

   set xx %x
   incr xx $cx1
   set yy %y
   incr yy $cy1

   set ::miniwin::x2 %X
   set ::miniwin::y2 %Y

   set ::miniwin::WIN.resizing [expr {
    8 * (  abs($xx-$cx1)<8  )
     |
    4 * (  abs($yy-$cy1)<8 )
     |
    2 * (  abs($xx-$cx2)<8 )
     |
    1 * (  abs($yy-$cy2)<8 )
   }]

   raise WIN
  }]
  # ----------------------------------

  # ------------------------------------------
  # Stop dragging on left mouse button release
  # ------------------------------------------
  bind $inside <ButtonRelease-1> [string map $stringMap {
   set ::miniwin::WIN.resizing 0
  }]
  # ------------------------------------------

  # ------------------------------------------------
  # Dragging the edges of the window frame to resize
  # ------------------------------------------------
  bind $inside <Motion> [string map $stringMap {
   if {[set ::miniwin::WIN.resizing]} {
    set ::miniwin::ox2 $::miniwin::x2
    set ::miniwin::oy2 $::miniwin::y2
    set ::miniwin::x2 %X
    set ::miniwin::y2 %Y

    set flags [set ::miniwin::WIN.resizing]
    if {$flags & 8} {
     place configure WIN -x [expr {[lindex [place configure WIN -x] end]-($::miniwin::ox2-$::miniwin::x2)}] -w [expr {[lindex [place configure WIN -w] end]+($::miniwin::ox2-$::miniwin::x2)}]
    }
    if {$flags & 4} {
     place configure WIN -y [expr {[lindex [place configure WIN -y] end]-($::miniwin::oy2-$::miniwin::y2)}] -h [expr {[lindex [place configure WIN -h] end]+($::miniwin::oy2-$::miniwin::y2)}]
    }
    if {$flags & 2} {
     place configure WIN -w [expr {[lindex [place configure WIN -w] end]-($::miniwin::ox2-$::miniwin::x2)}]
    }
    if {$flags & 1} {
     place configure WIN -h [expr {[lindex [place configure WIN -h] end]-($::miniwin::oy2-$::miniwin::y2)}]
    }
    if {[lindex [place configure WIN -w] end] < 8} {
     place configure WIN -w 8
    }
    if {[lindex [place configure WIN -h] end] < 8} {
     place configure WIN -h 8
    }

   }
  }]
  # ------------------------------------------------

  # Final setup
  if {$titleString ne {}} {::miniwin::setWindowTitle $window $titleString}
  # Create the variables for this miniwindow
  set ::miniwin::$window.exists $window
  set ::miniwin::$window.dragging 0
  set ::miniwin::$window.resizing 0
  return $contents
 }

}; #end of namespace eval




# ------------------------------------------------------------
# ------------------------------------------------------------
# --------------------- Demonstration ------------------------
# ------------------------------------------------------------
# ------------------------------------------------------------




# Demo window 1
set thisWindow ".ourTestWindow"
set insideThisWindow [::miniwin::makeWindow $thisWindow "title=test window"]
pack [canvas $insideThisWindow.c -width 100 -height 100 -background black] -fill both -expand 1
$insideThisWindow.c create oval 0 0 1 1 -outline white -fill blue -tag blueOval
bind $insideThisWindow.c <Configure> {
 set w [winfo width $insideThisWindow.c]
 set h [winfo height $insideThisWindow.c]
 incr w -4; incr h -4
 $insideThisWindow.c coords blueOval 4 4 $w $h
}

# Demo window 2
set w [miniwin::makeWindow .w]
pack [text $w.m -width 40 -height 10] -fill both -expand 1
$w.m insert 0.0 "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident,\nsunt in culpa qui officia deserunt mollit anim id est laborum."

bind $w.m <Destroy> "puts {this window $w.m was destroyed}"

# Demo window 4
set w [miniwin::makeWindow .names "title=Random name generator"]
proc randomname {} {
 set specialFunction {{lists} {
  set out ""
  foreach i $lists {
   append out [lindex $i [expr {int(rand()*[llength $i])}]]
  }
  return $out
 }}
 append name [apply $specialFunction {
  {Mr. Ms. Mrs. Mx. Master}
 }]
 append name " "
 append name [apply $specialFunction {
  {Pa Ha Da Ro Ste Ru Wi Si Ja Theo A Ai Le Te To Ma Ry Bea}
  {tt tr v n b m l p d rr me {} dd mm }
  {y ick yn id ve iel ik ert ian son on ore in lia o an ice} 
 }]
 append name " "
 append name [apply $specialFunction {
  {Be Wi Co Wo Ma Po Ho Ha Ru}
  {nn l ck dgr ckb {} w p rr ls v ss}
  {ett en oft ove urn y ell er is on}
 }]
 return $name
}
pack [button $w.b -text "Generate" -command { set nameResultVariable [randomname] } ] -side right
pack [entry $w.e -textvariable nameResultVariable -font {-size 20}] -side left -fill x -expand 1

# Demo window 4
set w [miniwin::makeWindow .cas buttons=110 "title=Window controls"]
pack [button $w.b  -text "Cascade" -command "::miniwin::cascade ."] -fill x
pack [button $w.b2 -text "Fit windows to size" -command "::miniwin::fitAllWindows ."] -fill x

# Final setup of the demonstration
update idletasks
miniwin::fitAllWindows .
miniwin::cascade .
wm geometry . 550x440
======
----

[m.n.summerfield] 2026-09-11. Suggest changing the control buttons to these (including changed order):

======
  # Creation of the window control buttons
  foreach {item buttonLabel command} [list \
   max "\U0001F5D6" "::miniwin::maxRestore $window" \
   min "\U0001F5D5" "::miniwin::minimiseWindow $window" \
   clo "❌" "::miniwin::destroyWindow $window" \
======

Also suggest changing the mouse cursor to "fleur" when moving and to "sizing" when resizing.

<<categories>> GUI