bsg 2015-03-30: I often need a spreadsheet to perform some quick calculations that are just a bit more complicated than can be done in my head or with a calculator. Using excel can be heavy with startup time, it's tendency to want to take over the whole screen, and the insistence on saving the work to disk. Seeing the Tiny Excel-like app in plain Tcl/Tk, I decided to make it actually useful. Here is my progress so far:
#!/bin/sh # \ exec /usr/local/bin/wish8.6 $0 $* package require Tk 8.6 set Prog(Title) "MiniSS" set Prog(Version) "v0.12" set Prog(Date) "2016-01-27" set Prog(Author) "bsg, HaJo" set Prog(Contact) "[email protected]"; # Todo set Prog(HomePage) "https://wiki.tcl-lang.org/41318" set Prog(Options) "7 7" set Prog(About) {MiniSS: a mini-spreadsheet with excel-like formulas that evolved from a 'Tiny Excel-like app in plain Tcl/Tk'. } # delete all data in worksheet # Todo: check if there is an easier way, e.g. using unset proc clear {} { global rows cols for {set row 0} {$row <= $rows} {incr row} { set columnName "" for {set column 0} {$column <= $::cols} {incr column; inca columnName} { set cell $columnName$row set ::formula($columnName$row) [set ::$cell ""] } } } # # Main program builds GUI # optional arguments are ?#rows ?#columns?? # proc main {args} { global rows cols Prog lassign {7 7} rows cols if {[llength $args] > 0} { set rows [lindex $args 0] } if {[llength $args] > 1} { set cols [lindex $args 1] } for {set row 0} {$row <= $rows} {incr row} { set columnName "" for {set column 0} {$column <= $cols} {incr column; inca columnName} { set cell $columnName$row set widget [if {$column == 0 || $row == 0} { ::ttk::label .label$cell -text [expr {$row ? $row : $columnName}] } else { set ::formula($columnName$row) [set ::$cell ""] trace add variable ::$cell read recalc ::ttk::entry .cell$cell -textvar ::$cell -width 10 -validate focus \ -validatecommand [list ::reveal-formula $cell %V %s] }] grid $widget -row $row -column $column } } set ::status "$Prog(Title) $Prog(Version)" set statusbar [ttk::label .lb -textvar status -anchor w ] incr row grid $statusbar -row $row -column 0 -columnspan $column grid $statusbar -sticky ew } ## Menu: proc m+ {head name {cmd ""}} { #: Menu-creator "m+" by R.Suchenwirth and DKF, 2006-08-24, see https://wiki.tcl-lang.org/16327 # Uses the undocumented and unsupported feature ::tk::UnderlineAmpersand if {![winfo exists .m.m$head]} { foreach {l u} [::tk::UnderlineAmpersand $head] break .m add cascade -label $l -underline $u -menu [menu .m.m$head -tearoff 0] } if {[regexp ^-+$ $name]} { .m.m$head add separator } else { foreach {l u} [::tk::UnderlineAmpersand $name] break .m.m$head add command -label $l -underline $u -comm $cmd } } # # Define menu # proc initMenu {} { . configure -menu [menu .m] # m+ &File &Open { Dummy "Open" } # m+ &File &Save { Dummy "Save" } # m+ &File ----- # m+ &File &New { clear $rows $cols } m+ &File &New { clear } m+ &File ----- m+ &File &Demo1 { demo1; focus -f .cellB2 } m+ &File &Demo2 { demo2; focus -f .cellB2 } m+ &File &Demo3 { demo3; focus -f .cellB1 } m+ &File &Demo4 { demo4; focus -f .cellB3 } m+ &File &Demo5 { demo5; focus -f .cellB2 } m+ &File ----- m+ &File &Exit exit m+ Edit &EditCell { Dummy "Edit" } m+ Edit &Insert { Dummy "Insert" } m+ Edit &Goto { Dummy "Goto" } m+ Options &ColumnWidth { Dummy "ColumnWidth" } m+ Options &Protection { Dummy "Protection" } m+ &Help &About About m+ &Help &Help About; # Todo: Help m+ &Help ----- m+ &Help &Debug { console show } } #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+ proc About {} { #: Short info about the program / using tk_messageBox global Prog set txt "$Prog(Title) $Prog(Version) - " append txt "$Prog(Date)\nby $Prog(Author) - $Prog(Contact)\n\n$Prog(About)" tk_messageBox -icon info -message $txt -title "About $Prog(Title) $Prog(Version)" } # Todo: Help() proc Dummy {txt} { #: Alert: "Function X not implemented yet" bell tk_messageBox -icon warning -title "Demo" \ -message "Function '$txt' not implemented yet." } #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+ # # Input-Dialog: (from https://wiki.tcl-lang.org/8692 "A little value dialog" by R.Suchenwirth) # proc edit_val {string } { set w [toplevel .edit] wm resizable $w 0 0 wm title $w "Edit cell" label $w.l -text $string #entry $w.e -textvar $w -bg white entry $w.e -textvar ::val -width 20 #set done -1 set old $::val button $w.ok -text " OK " -command {set ::done 0} button $w.clear -text "Clear " -command "set ::val {}" button $w.cancel -text "Cancel" -command "set ::val $old; set ::done 1" bind $w.e <Return> {set done 1} bind $w.e <Escape> "$w.cancel invoke" grid $w.l - - -sticky news grid $w.e - - -sticky news #grid $w.ok $w.clear $w.cancel grid $w.cancel $w.clear $w.ok raise $w . focus $w.e vwait ::done destroy $w return $::done } #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+ # # Program the Arrow keys to move about the sheet # proc cell-arrow {dir w args} { set column [dict get [grid info $w] -column] set row [dict get [grid info $w] -row] switch $dir { Left { lassign [list [incr column -1] -row $row] index axis axis-value } Right { lassign [list [incr column 1] -row $row] index axis axis-value } Up { lassign [list [incr row -1 ] -column $column] index axis axis-value } Down { lassign [list [incr row 1 ] -column $column] index axis axis-value } } set x [lindex [lsort -dictionary [grid slaves . $axis ${axis-value}]] [expr {$index -1}]] if {[string match {*cell*} $x]} { focus $x } puts "Current cell: $row $column" set ::status "Current cell: $column $row"; # Todo: convert columns to A..G } bind TEntry <Key-Left> {cell-arrow %K %W} bind TEntry <Key-Right> {cell-arrow %K %W} bind TEntry <Key-Up> {cell-arrow %K %W} bind TEntry <Key-Down> {cell-arrow %K %W} bind TEntry <Key-Escape> {bell} bind TEntry <Key-Return> { set val "abc"; # Todo: get value from current cell set res [edit_val "Enter value or formula:"] puts "Edit:$res/$val" } # # inca - increment letter (column) sequence # A -> B -> C ... AA -> AB ... AZ -> BA -> BB # set atab [split {ABCDEFGHIJKLMNOPQRSTUVWXYZ} {}] proc inca {avar {by 1}} { upvar $avar a if {$a eq ""} {set a A; return} global atab set i $by foreach d [lreverse [split [string toupper $a] {}]] { set nxt [expr {([lsearch $atab $d] + $i) % 26}] set i [expr {($i>0 && !$nxt) ? 1 : 0}] lappend n [lindex $atab $nxt] } if {$i>0} { lappend n [lindex $atab 0] } set a [join [lreverse $n] ""] } proc recalc {cell args} { if {$::formula($cell) ne ""} { catch {set ::$cell [uplevel #0 [list \ expr [regsub -all {([A-Z]+[1-9])} [expand-range $::formula($cell)] {$\1}]]]} } } proc reveal-formula {cell event value} { if {$event eq "focusin"} { if {$::formula($cell) ne ""} { set ::$cell =$::formula($cell) } .cell$cell selection range 0 end .cell$cell icursor end } else { ;# focusout if {![regexp {^=(.*)} $value -> ::formula($cell)]} { set ::formula($cell) "" } foreach otherCell [array names ::formula] { recalc $otherCell } } return 1 } proc expand-range {arg} { while {[regexp {(([A-Z]+)([0-9]+)\.\.([A-Z]+)([0-9]+))} $arg -> pat leftcol leftrow rghtcol rghtrow]} { set l [list] for {set col $leftcol} {$col <= $rghtcol} {inca col} { for {set row $leftrow} {$row <= $rghtrow} {incr row} { lappend l ${leftcol}${row} } } set arg [regsub $pat $arg [join $l ,]] } return $arg } # Add excel like functions here: proc ::tcl::mathfunc::sum {args} { ::tcl::mathop::+ {*}$args } proc ::tcl::mathfunc::average {args} { expr {[llength $args] ? [::tcl::mathop::+ {*}$args] / double([llength $args]) : "!ERR"} } # Demos: # setFormula proc setFo {cell value} { set ::formula($cell) $value; recalc $cell } proc demo1 {} { clear wm title . "MiniSpreadSheet - Demo1" set ::A1 "**Demo1**" set ::C1 "use TAB- &" set ::D1 "cursor-keys" set ::E1 "to move" set ::F1 "around" set ::C2 "ENTER-key" set ::D2 "to edit a cell" set ::C3 "ESC-key" set ::D3 "to abort or" set ::E3 "undo" set ::A3 " +"; # Todo: right-align set spc " " set ::A4 "$spc +" set ::B2 "17" set ::B3 "4" set ::B4 "0.25" set ::B5 "====" set ::A6 "Sum:" set ::A7 "$spc *" set ::formula(B6) "B2+B3+B4"; recalc B6 set ::C6 "Fieldnames" set ::D6 "must be" set ::E6 "uppercase !" set ::B7 "0.1" set ::C7 " =" set ::formula(D7) "B7*B6"; recalc D7 return "B2" } proc demo2 {} { clear wm title . "MiniSpreadSheet - Demo2" set ::A1 "**Demo2**" set ::A2 "Article#001"; set ::B2 "55.5" set ::A3 "Article#002"; set ::B3 "44.5" set ::A4 " Sum:"; setFo B4 "B2+B3" set ::A5 ".16"; setFo B5 "B4*A5" set ::A6 " Total:"; setFo B6 "B4+B5" } proc demo3 {} { clear wm title . "MiniSpreadSheet - Demo3" set ::G1 "**Demo3**" set ::A1 "Article#001"; set ::B1 "3"; set ::C1 "50"; setFo D1 "B1*C1" set ::A2 "Article#002"; set ::B2 "1"; set ::C2 "123.75"; setFo D2 "B2*C2" set ::A3 "Article#003"; set ::B3 "5"; set ::C3 "25.25"; setFo D3 "B3*C3" set ::A4 " Sum:"; setFo B4 "B1+B2+B3"; #setFo D4 "D1+D2+D3" setFo D4 "sum(D1..D3)" set ::A5 " Tax:"; set ::B5 ".16"; setFo D5 "D4*B5" set ::C6 " Total:"; setFo D6 "D4+D5" } proc demo4 {} { clear wm title . "MiniSpreadSheet - Demo4" set ::A1 "**Demo4**" # Todo - Testdata for sum() } proc demo5 {} { clear wm title . "MiniSpreadSheet - Demo5" set ::G1 "**Demo5**" set ::A1 "Date:" set ::B1 "2016week01" set ::A2 "Day:"; set ::B2 "Mo"; set ::C2 "Di"; set ::D2 "Mi"; set ::E2 "Do"; set ::F2 "Fr"; set ::A3 "Start:"; set ::B3 " 8.0"; set ::C3 " 7.75"; set ::D3 " 8.0"; set ::E3 " 8.25"; set ::F3 " 8.0"; set ::A4 "End:"; set ::B4 "17.0"; set ::C4 "17.25"; set ::D4 "16.0"; set ::E4 "16.25"; set ::F4 "15.0"; set ::A5 "Hours:"; setFo B5 "B4-B3"; setFo C5 "C4-C3"; setFo D5 "D4-D3"; setFo E5 "E4-E3"; setFo F5 "F4-F3"; set ::A6 "Pause:"; set ::B6 "1.0"; set ::C6 "0.5"; set ::D6 "1.0"; set ::E6 "0.5"; set ::F6 "0"; set ::A7 "Total:"; setFo B7 "B5-B6"; setFo C7 "C5-C6"; setFo D7 "D5-D6"; setFo E7 "E5-E6"; setFo F7 "F4-F3"; setFo G3 "sum(B3..F3)"; # ?? bug in sum() ?? setFo G4 "sum(B4..F4)"; # ?? setFo G5 "sum(B5..F5)"; # ?? setFo G5 "sum(B6..F6)"; # ?? setFo G6 "B6+C6+D6+E6+F6" setFo G2 "sum(B7..F7)"; # ?? setFo G7 "B7+C7+D7+E7+F7" } main {*}$argv; focus -f .cellA1 initMenu demo1; focus -f .cellB2 #set cell1 [demo1]; focus -f .cell$cell1 #demo2; focus -f .cellB2 #demo3; focus -f .cellB1 #demo4; focus -f .cellB2 #demo5; focus -f .cellB3
AMG: Please considering using the [sum] and [average] procs presented at the bottom of [L1 ]. These are more efficient, avoid expr injection attacks, and work correctly when averaging integers.
AMG: To anyone who may be curious, the code has been updated per my suggestion.
HJG 2016-01-27 - Added some simple demos, from TinyExcel.
This format for the data looks much more suitable for a possible load/save-procedure.
Nice work so far, and this would be my wishlist:
HJG 2016-01-28: A first step to 'mainstream' this program: I added a menu, edit-dialog, statusbar and About-box.
A Help-dialog could be done just like the About-box, but something with a text-widget would be better. See also demo1, for a 'hands-on introduction.
The canned demos can be loaded from the file-menu (as a sort-of-replacement for load).
Demo5 is an attempt to provide something small but useful out-of-the-box, a timesheet for one week. It uses decimal time for now.
Maybe we can come up with some more of such small worksheets.