[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