[AMG]: Here is a Tcl/Tk calculator application I developed. [calc.png] **Prerequisites** * Tcl 8.6 * Tk * [yeti] * [struct] (part of [tcllib], required by yeti) * [wcb] (part of [tklib]) **Design and Features** At first I thought I'd just wrap around [[[expr]]], but then I realized I wanted the operations to work a little bit differently. For example, I want "1/2" to evaluate to "0.5" and not be truncated to "0". Thus, I decided to use [yeti] to implement my own expression parser which can call custom math operation and function procedures having my desired behaviors. I left out a lot of operations not typically found in a calculator, mainly comparisons and logical operations. I also decided to hide and rename a few of the standard math functions. Given my redesigned division operator, [double] isn't useful, so get rid of it. [entier], on the other hand, is very useful, but most folks expect it to be named [int], so rename it. And so forth. As for the user interface, having to click on the buttons of a simulated calculator face is not so great when you have a mouse and keyboard, so I went with a [text] widget instead. This gave me room to show history as well as the current computation. Though, to make the user interface look good, I made heavy use of tags to colorize and otherwise format everything. The standard bindings for text aren't really appropriate. Mainly, the user should only be allowed to type in the entry field, not overwrite history. The easiest way to filter and customize widget bindings is [wcb], so I used that. Speaking of history, I automatically store each result value into a new variable that can be accessed just by typing its name into the expression (no $ needed). For extra convenience, the most recent result is always called "ans" in addition to its permanent variable name, which is shown in the log. Since I do a lot of GIS work, I thought it would be cool to support sexagesimal numbers, i.e. angles divided into degrees, minutes, and/or seconds. Just type DD:MM or DD:MM:SS, with an optional sign prefix and fractional suffix. The sign prefix can be any of [[+NnEe]] for positive or [[-SsWw]] for negative. For documentation, start the program and press F1. **Rejected Ideas** ***Omitting parentheses from function calls*** Some prior versions of this page have a [BNF] allowing parentheses to be omitted from function calls, a la [Perl], since I found the reduction in parentheses to be convenient during interactive use. However, this resulted in an ambiguous BNF because of the collision between a parenthesized single-argument function call and a non-parenthesized single-argument function call whose argument is itself surrounded by parentheses. Confused about the distinction? So is the computer. Consider `ln(2**999)/ln(2)`. The argument to the first `ln` could be either `2**999` or `(2**999)/ln(2)`. My attempted fix was to have the non-parenthesized single-argument function call BNF descend into a second copy of the BNF that omits grouping, but this causes `ln a+b` to be `ln(a+b)`, whereas `ln a+(b)` becomes `ln(a)+b`. The real fix would be not using parentheses both for grouping and for function calls. If function call argument lists were surrounded with brackets or braces rather than parentheses, there would be no ambiguity. However, this syntax is mildly unconventional, though not completely unheard-of, so I'm leaving it out until actually needed. **Code** ====== #!/usr/bin/env tclsh # Tcl/Tk calculator program. # This program is released under BSD license without any warranties. # Copyright (C) 2019 Andy Goth # Source: https://wiki.tcl-lang.org/page/calc # Load required packages. lappend auto_path [file join [file dirname [info script]] lib] package require Tcl 8.6 package require Tk package require yeti package require ylex package require wcb # Create the ::calc namespace. namespace eval ::calc { variable Count 0 ;# Number of items in the history. } # ::calc::EncodeDms -- # Encodes a degrees-minutes or degrees-minutes-seconds value. proc ::calc::EncodeDms {enableSec a {digits 6}} { # Decode the input. set a [DecodeNumber $a] if {![string is integer -strict $digits] || $digits < 0} { return -code error "digit count must be a nonnegative integer" } # Perform common processing. if {$a < 0} { set result - set a [expr {-$a}] } else { set result {} } set deg [expr {int($a)}] set scalar [expr {10 ** $digits}] if {$enableSec} { # Encode degrees-minutes-seconds. set fix [expr {int(($a - $deg) * 3600 * $scalar + 0.5)}] set min [expr {$fix / (60 * $scalar) % 60}] set sec [expr {$fix / $scalar % 60}] append result [format %d:%02d:%02d $deg $min $sec] } else { # Encode degrees-minutes. set fix [expr {int(($a - $deg) * 60 * $scalar + 0.5)}] set min [expr {$fix / $scalar % 60}] append result [format %d:%02d $deg $min] } # Append fractional digits if enabled. if {$digits} { append result [format .%0*d $digits [expr {$fix % $scalar}]] } return $result } # ::calc::DecodeDms -- # Decodes a degrees-minutes-seconds value. The seconds field is optional. proc ::calc::DecodeDms {a} { # Split the value into fields. regexp {^([NnSsEeWw]?)0*(\d+)(?::0*(\d+))(?::0*(\d+))?(\.\d*)?$} $a _\ hemi deg min sec frac # Treat degenerate fractions as zero. if {$frac in {{} .}} { set frac 0 } # Add the fraction to the least-significant place. if {$min >= 60} { return -code error "minutes field too large" } elseif {$sec eq {}} { set min [expr {$min + $frac}] set sec 0 } elseif {$sec >= 60} { return -code error "seconds field too large" } else { set sec [expr {$sec + $frac}] } # Compute the result. Negate the southern and western hemispheres. if {$hemi in {S s W w}} { expr {-$deg - $min / 60.0 - $sec / 3600.0} } else { expr {+$deg + $min / 60.0 + $sec / 3600.0} } } # ::calc::DecodeNumber -- # Attempts to decode any number in a recognized format. proc ::calc::DecodeNumber {a} { if {[string is double -strict $a]} { return $a } else { ::calc::DecodeDms $a } } # ::calc::Call -- # Calls a function with all arguments decoded as numbers. proc ::calc::Call {func args} { {*}$func {*}[lmap arg $args {DecodeNumber $arg}] } # Create the ::calc::math namespace to contain all math functions and operators. namespace eval ::calc::math { # Create selected math functions, renaming a few of them along the way. foreach {from to} { abs abs acos acos asin asin atan atan atan2 atan2 ceil ceil cos cos cosh cosh int entier isqrt isqrt exp exp floor floor hypot hypot ln log log log10 max max min min round round sqrt sqrt sin sin sinh sinh tan tan tanh tanh } { interp alias {} ::calc::math::$from {} ::calc::Call ::tcl::mathfunc::$to } # Create functions to format in degrees-minutes and degrees-minutes-seconds. interp alias {} ::calc::math::dm {} ::calc::EncodeDms 0 interp alias {} ::calc::math::dms {} ::calc::EncodeDms 1 # Create functions to convert between degrees and radians. proc deg {a} {variable pi; expr {$a * 180 / $pi}} proc rad {a} {variable pi; expr {$a * $pi / 180}} # Create trigonometric functions that work in degrees. proc acosd {a} {deg [expr {acos($a)}]} proc asind {a} {deg [expr {asin($a)}]} proc atand {a} {deg [expr {atan($a)}]} proc atan2d {a b} {deg [expr {atan2($a, $b)}]} proc cosd {a} {expr {cos([rad $a])}} proc sind {a} {expr {sin([rad $a])}} proc tand {a} {expr {tan([rad $a])}} # Create functions to format numbers in binary, octal, and hexadecimal. # When given a negative number, truncate the two's-complement. foreach {name code} {bin b oct o hex x} { proc $name {a {size 32}} [string map [list %CODE% [list $code]] { if {$a < 0} { set a [expr {$a & (1 << $size) - 1}] } format 0%CODE%%ll%CODE% $a }] } # Create math operations that recognize DMS inputs as well as numbers. foreach op {+ - * / % ** << >> & ^ |} { interp alias {} ::calc::math::$op {} ::calc::Call ::tcl::mathop::$op } # ::calc::math::/ -- # Custom division operation that avoids truncation. proc / {a b} { if {[string is integer -strict $a] && [string is integer -strict $b] && !($a % $b)} { expr {$a / $b} } else { expr {double([::calc::DecodeNumber $a]) / [::calc::DecodeNumber $b]} } } # ::calc::math::% -- # Custom modulo operation that works with integers and real numbers. proc % {a b} { if {[string is integer -strict $a] && [string is integer -strict $b]} { expr {$a % $b} } else { expr {fmod([::calc::DecodeNumber $a], [::calc::DecodeNumber $b])} } } # Clean up temporary variables. unset from to name code op # Create constants. set pi [expr {acos(-1)}] set e [expr {exp(1)}] set inf Inf } # Create the scanner. yeti::ylex ::calc::ScannerBuilder -name ::calc::ScannerClass ::calc::ScannerBuilder code error {} ::calc::ScannerBuilder code private { method NameType {name} { if {[info exists ::calc::math::$name]} { return VAR } elseif {[info commands ::calc::math::$name] ne {}} { return FUNC } else { return ERR } } } ::calc::ScannerBuilder macro { SPACE {[ \f\n\r\t\v]+} END {(?!\w)} OP {<<|>>|\*\*|[-(),&|^+*/%]} DEC {\d+} BIN {0[Bb][01]+} OCT {0[Oo][0-7]+} HEX {0[Xx][[:xdigit:]]+} EXP {[eE][+-]?\d+} REAL {(?:\d+|\d*\.\d+?|\d+\.\d*?)} DMS {[NnSsEeWw]?\d+(?::\d+){1,2}(?:\.\d*)?} NAME {[a-zA-Z_]\w*} } ::calc::ScannerBuilder add { {} {} {} {return [list $yytext]} {} {return [list VAL [scan $yytext %lld]]} {} {return [list VAL [scan $yytext %llb]]} {} {return [list VAL [scan [regsub {..} $yytext {}] %llo]]} {} {return [list VAL [scan $yytext %llx]]} {} {return [list VAL [scan $yytext %f]]} {} {return [list VAL [::calc::DecodeDms $yytext]]} {} {return [list [NameType $yytext] $yytext]} {.} {error "invalid character \"$yytext\""} } eval [::calc::ScannerBuilder dump] ::calc::ScannerClass ::calc::Scanner ::calc::ScannerBuilder destroy # Create the parser. yeti::yeti ::calc::ParserBuilder -name ::calc::ParserClass -start ior ::calc::ParserBuilder code error {} ::calc::ParserBuilder add { ior xor {return $1} | {ior | xor} {return [::calc::math::| $1 $3]} xor and {return $1} | {xor ^ and} {return [::calc::math::^ $1 $3]} and shf {return $1} | {and & shf} {return [::calc::math::& $1 $3]} shf add {return $1} | {shf << add} {return [::calc::math::<< $1 $3]} | {shf >> add} {return [::calc::math::>> $1 $3]} add mul {return $1} | {add + mul} {return [::calc::math::+ $1 $3]} | {add - mul} {return [::calc::math::- $1 $3]} mul exp {return $1} | {mul * exp} {return [::calc::math::* $1 $3]} | {mul / exp} {return [::calc::math::/ $1 $3]} | {mul % exp} {return [::calc::math::% $1 $3]} exp una {return $1} | {una ** exp} {return [::calc::math::** $1 $3]} una num {return $1} | {+ una} {return [::calc::math::+ $2]} | {- una} {return [::calc::math::- $2]} num {( ior )} {return $2} | VAL {return $1} | VAR {return [set ::calc::math::$1]} | {FUNC ( )} {return [::calc::math::$1]} | {FUNC ( arg )} {return [::calc::math::$1 {*}$3]} arg ior {return [list $1]} | {arg , ior} {return [list {*}$1 $3]} } eval [::calc::ParserBuilder dump] ::calc::ParserClass ::calc::Parser -scanner ::calc::Scanner ::calc::ParserBuilder destroy # ::calc::eval -- # Evaluates the math expression. proc ::calc::eval {expr} { Parser reset Scanner start $expr Parser parse } # ::calc::help -- # Writes help text into a text widget. proc ::calc::help {win} { # Configure tags. $win configure -wrap word set font [font actual [$win cget -font]] $win tag configure normal -font $font $win tag configure header -font [list {*}$font -weight bold -underline 1] $win tag configure fixed -font [list {*}$font -family monospace]\ -background palegreen # Build reference table. set row 0 set col(2) \u2502 set col(5) \u2502 foreach {col(0) col(1)} { Expression Result {} {} "a | b" "Bitwise inclusive OR" "a ^ b" "Bitwise exclusive OR" "a & b" "Bitwise AND" "a << b" "Shift left" "a >> b" "Shift right" {} {} "a + b" "Addition" "a - b" "Subtraction" "a * b" "Multiplication" "a / b" "Division" "a % b" "Remainder" "a ** b" "Exponentiation" {} {} +a "Passthrough" -a "Negation" (a) "Grouping" {} {} pi "Archimedes's constant" e "Euler's number" inf "Infinity" ans "Most recent result" "a1 a2 ... aN" "Numbered result" } {col(3) col(4)} { Expression Result {} {} bin(a[,bits]) "Binary format" oct(a[,bits]) "Octal format" hex(a[,bits]) "Hexadecimal format" {} {} dm(a[,digits]) "Deg:min format" dms(a[,digits]) "Deg:min:sec format" {} {} deg(a) "Radians to degrees" rad(a) "Degrees to radians" {} {} abs(a) "Absolute value" ceil(a) "Ceiling" floor(a) "Floor" int(a) "Round toward zero" round(a) "Round to nearest integer" {} {} exp(a) "e**a" ln(a) "Natural logarithm" log(a) "Base-10 logarithm" {} {} max(a,b,...) "Maximum" min(a,b,...) "Minimum" } {col(6) col(7)} { Expression Result {} {} isqrt(a) "Integer square root" sqrt(a) "Real square root" hypot(a,b) "Hypotenuse length" {} {} cos(a) "Cosine" sin(a) "Sine" tan(a) "Tangent" acos(a) "Inverse cosine" asin(a) "Inverse sine" atan(a) "Inverse tangent" atan2(a,b) "Argument" cosh(a) "Hyperbolic cosine" sinh(a) "Hyperbolic sine" tanh(a) "Hyperbolic tangent" {} {} cosd(a) "Cosine (deg)" sind(a) "Sine (deg)" tand(a) "Tangent (deg)" acosd(a) "Inverse cosine (deg)" asind(a) "Inverse sine (deg)" atand(a) "Inverse tangent (deg)" atan2d(a,b) "Argument (deg)" } { foreach {tag i} { fixed 0 normal 1 normal 2 fixed 3 normal 4 normal 5 fixed 6 normal 7 } { if {!$row && $col($i) ne "\u2502"} { set tag header } set w [font measure [$win tag cget $tag -font] $col($i)] if {![info exists width($i)] || $w > $width($i)} { set width($i) $w } if {$i} { $win insert end \t } $win insert end $col($i) $tag } $win insert end \n incr row } $win configure -tabs [lmap i {0 1 2 3 4 5 6 7} { incr accum [expr {5 + $width($i)}] }] # Configure widget size according to table size. set unit [font measure $font 0] $win configure -width [expr {($accum + $unit - 1) / $unit}]\ -height [expr {$row + 2}] # Output the main help text. set sep {} foreach {header body} { "Basic Operation" "Simply type the math expression, and the displayed result will update continuously. If there is an error (e.g., incomplete input), the result will be blank until the input is valid. Pressing Enter moves the current entry and result to the history. Old result values can be recalled via the `ans` variable or the numbered `a` variables, e.g., `a1` for the first result. Old entries and results can be copied into the entry field by moving the cursor upward (or by clicking on them) and pressing Enter. Ctrl+C and Ctrl+V can be used to copy and paste between applications. Press Esc to clear the entry field." "Number Formats" "Integers and real numbers are normally entered in decimal. Integers can also be entered in binary, octal, or hexadecimal using `0b`, `0o`, or `0x` prefixes, e.g. `0b1101` for `13` in binary. The `bin`, `oct`, and `hex` functions convert to binary, octal, and hexadecimal. For example, `oct(255)` evaluates to `0o377`. The inputs to these functions need not be written in decimal and can be the result of further computation, so `hex(0b10100101)` returns `0xa5`, and `bin(int(acosd(sqrt(1/2))))` returns `0b101101`. By default, these functions truncate negative numbers to 32 bits, but an optional second argument can be used to specify the bit count. For example, `hex(-100)` yields `0xffffff9c`, whereas `hex(-100,8)` yields `0x9c`." "Sexagesimal Notation" "Real numbers can be entered in sexagesimal (base-60) notation, which subdivides degrees into minutes and (optionally) seconds, with the places separated by colons. There are sixty minutes per degree and sixty seconds per minute. A sign prefix may be used, where \[`+NnEe`\] are positive and \[`-SsWw`\] are negative. Examples: `N32:26:06.630720` is `32.4351752` and `W097:00.252` is `-97.0042`. The `dm` and `dms` functions convert to deg:min and deg:min:sec, so `dm(-12.3456789)` returns `-12:20.740734`, `dms(45.5+59.123456/3600)` returns `45:30:59.123456`, and `dm(1:2:3.4)` returns `1:02.056667`. These functions output six fractional digits by default, though this is controlled by the optional second argument. For example, `dm(1:2:3,2)` gives `1:02.05` and `dms(22.22,0)` gives `22:13:12`." "Degrees and Radians" "The normal trigonometry functions use radians due to the equivalence between the unit circle's arc angle and arc length. However, degrees are easier to conceptualize and communicate and are therefore more popular in practical applications. To use degrees instead of radians, add `d` to the name of the trigonometric function, for instance `sind` rather than `sin`. (The degree versions of hyperbolic functions are omitted.) `sind(90)` is the same as `sin(pi/2)`, and both evaluate to `1.0`. To directly convert to degrees from radians, use the `deg` function, or use `rad` to convert to radians from degrees. For example, `deg(pi/4)` returns `45.0`, and `rad(180)` returns `3.141592653589793`." "Precision" "Arbitrary-precision integers are used, so integer computations have no upper limit besides that imposed by memory and CPU time constraints. Some extremely large computations may effectively lock up the program. For real numbers, double-precision values are used. On most platforms, this gives 53 bits of significand, or about 16 decimal digits, with an exponent range of about \u00b1308 decimal." } { $win insert end $sep\n$header\n header foreach {_ normal fixed} [regexp -all -inline {([^`]*)(?:`([^`]*)`)?}\ [regsub -all {\n *} $body " "]] { $win insert end $normal normal $fixed fixed } set sep \n } } # ::calc::setup -- # Configures a text widget to be a calculator. proc ::calc::setup {win} { # Create entry and result fields. .calc insert end \n entry " " result .calc mark set insert entry.first # Create a tab stop at the right edge of the text widget. bind $win { set box [%W bbox "1.0 lineend"] %W configure -tabs [list\ [expr {[lindex $box 0] + [lindex $box 2] - 2}] right] } # Delete the entry when escape is pressed. bind $win { %W delete entry.first entry.last-1char } # Create input callback. wcb::callback $win before insert {apply {{win index args} { variable Count # Loop over each part being inserted. set i 1 set cancel 1 foreach {string tags} $args { if {$string eq "\n" && [$win compare $index < entry.first]} { # Interpret a bare newline before the entry field as a command # to replace the entry with the old entry or result. set index [$win index "$index lineend"] foreach tag {oldResult oldEntry} { if {[set match [$win tag prevrange $tag $index]] ne {} && [$win compare $index >= "[lindex $match 0] linestart"] && [$win compare $index < "[lindex $match 1] lineend"]} { lset match 1 [lindex $match 1]-1char $win delete entry.first entry.last-1char wcb::replace 0 end\ entry.first [$win get {*}$match] entry $win tag remove sel sel.first sel.last $win mark set insert entry.last-1char set cancel 0 break } } } elseif {$string eq "\n"} { # Interpret a bare newline as a command to store the result. catch { # Get and evaluate the input expression. set in [$win get entry.first entry.last-1char] set out [calc::eval $in] # Store the result into the history. incr Count set ::calc::math::ans $out set ::calc::math::a$Count $out # Display the result. $win insert entry.first $in\n oldEntry\ "a$Count =\t" oldVar $out\n oldResult # Clear the input. $win replace result.first result.last " " result $win delete entry.first entry.last-1char $win see end } wcb::replace $i [expr {$i + 1}] } elseif {$string in {\f \r \t \v}} { # Disallow entering whitespace control characters by themselves. wcb::replace $i [expr {$i + 1}] } else { # Simplify input whitespace. wcb::replace $i [expr {$i + 1}] [string map\ {\f {} \n " " \r {} \t " " \v {}}\ [string trim $string \n\t]] entry # Clamp the insert position to the entry field. if {[$win compare $index < entry.first]} { catch {$win tag remove sel sel.first sel.last} $win mark set insert entry.first } elseif {[$win compare $index >= entry.last]} { catch {$win tag remove sel sel.first sel.last} $win mark set insert entry.last-1char } # Do not cancel the command if there is any good input. set cancel 0 } incr i 2 } # Cancel the command if all parts have been omitted. if {$cancel} { wcb::cancel } } ::calc}} # Create delete callback. wcb::callback $win before delete {apply {{win from {to {}}} { # Check if the selection is being deleted. set sel [expr {$from eq "sel.first" && $to eq "sel.last"}] # Fill in the default end index. if {$to eq {}} { set to $from+1char } # Clamp the start and/or end indexes to the entry field. set clampFrom [$win compare $from < entry.first] set clampTo [$win compare $to >= entry.last] if {$clampFrom || $clampTo} { # Clamp the indexes, or resolve the selection indexes. if {$clampFrom} { set from entry.first } elseif {$sel} { set from [$win index $from] } if {$clampTo} { set to entry.last-1char } elseif {$sel} { set to [$win index $to] } # Now that the indexes have been updated, take action. if {!$sel} { # If the selection is not being deleted, simply apply clamping. wcb::replace 0 1 $from $to } elseif {[$win compare $from >= $to]} { # Abort if clamping results in an empty range. wcb::cancel {} } else { # If the selection is being deleted, clamp the selection itself. $win tag remove sel sel.first sel.last $win tag add sel $from $to } } }}} # Create motion callbacks. wcb::callback $win before motion {apply {{win index} { if {($index eq "insert-1displayindices" && [$win compare insert == entry.first]) || ($index eq "insert+1displayindices" && [$win compare insert == entry.last-1chars]) || [$win compare $index >= result.first] && [$win get result.first result.last] eq " "} { # Cancel attempts to leave the entry field using left or right, as # well as attempts to move the cursor to the result when empty. wcb::cancel {} } }}} wcb::callback $win after motion {apply {{win index} { if {[$win compare insert < entry.first]} { # Automatically select the whole entry or result from the log. set index [$win index "insert lineend"] foreach tag {oldResult oldEntry} { if {[set match [$win tag prevrange $tag $index]] ne {} && [$win compare $index >= "[lindex $match 0] linestart"] && [$win compare $index < "[lindex $match 1] lineend"]} { catch {$win tag remove sel sel.first sel.last} after idle [list $win tag add sel\ [lindex $match 0] [lindex $match 1]-1char] break } } } elseif {[$win compare insert >= result.first]} { # Automatically select the whole result. catch {$win tag remove sel sel.first sel.last} after idle [list $win tag add sel result.first result.last] } }}} # Create post-update callbacks. foreach event {insert delete} { wcb::callback $win after $event {apply {{win args} { try { # Attempt to evalute the input field. set out [calc::eval [$win get entry.first entry.last-1char]] } on error {} { # On failure, force the result to a single space. set out " " } # Put the result onscreen. if {[set match [$win tag ranges result]] eq {}} { $win insert end $out result } else { $win replace result.first result.last $out result } $win see end }}} } } # Create user interface. wm title . Calculator text .calc -highlightthickness 0 -background palegreen\ -yscrollcommand {.scroll set} .calc tag configure oldEntry -font {-family monospace -size 14}\ -selectforeground chartreuse -selectbackground forestgreen\ -foreground olivedrab .calc tag configure oldVar -font {-family monospace -size 10}\ -selectforeground chartreuse -selectbackground forestgreen\ -foreground seagreen .calc tag configure oldResult -font {-family monospace -size 14}\ -selectforeground chartreuse -selectbackground forestgreen\ -foreground green .calc tag configure entry -font {-family monospace -size 18}\ -borderwidth 2 -relief groove\ -foreground darkslategray -background greenyellow\ -selectforeground chartreuse -selectbackground darkgreen\ -lmargin1 4 -lmargin2 4 -rmargin 4 -spacing1 2 -spacing3 2 .calc tag configure result -font {-family monospace -size 18 -weight bold}\ -selectforeground chartreuse -selectbackground forestgreen\ -foreground green -justify right ttk::scrollbar .scroll -command {.calc yview} grid .calc .scroll -sticky nsew grid columnconfigure . .calc -weight 1 grid rowconfigure . .calc -weight 1 focus .calc calc::setup .calc # Create help interface. ttk::label .hint -text "Press F1 for help" -background palegreen place .hint -anchor sw -relx 0 -x 1 -rely 1 -y -1 bind . {destroy .hint; bind . {}; continue} bind . {wm state .help normal} toplevel .help wm title .help "Calculator Help" wm withdraw .help wm protocol .help WM_DELETE_WINDOW {wm withdraw .help} bind .help {wm withdraw .help} wm resizable .help 0 1 text .help.text -highlightthickness 0 -font TkDefaultFont\ -yscrollcommand {.help.scroll set} ttk::scrollbar .help.scroll -command {.help.text yview} grid .help.text .help.scroll -sticky nsew grid columnconfigure .help .help.text -weight 1 grid rowconfigure .help .help.text -weight 1 calc::help .help.text .help.text configure -state disabled # vim: set sts=4 sw=4 tw=80 et ft=tcl: ====== <> Tk | Application