Tcl and Tk
- last revision
- 21 November 2015, 2:37pm
- book quality
- stub
This is a booklet about the Tool Command Language and the Tk widget
set. The booklet is oriented towards using tcl/tk on a debian linux
computer, even though it is simple to write cross platform
applications using tcl/tk. Tcl and Tk is a large topic with an
interesting history. Tcl was originally designed as an 'embeddable'
language to be used with existing commands written in c.
INSTALLATION
* choose which tk version you want to activate (8.4 8.5 etc)
>> sudo update-alternatives --config wish
and then choose the version from the list provided. Version 8.5
of Tk has nice anti-aliased fonts.
* search for packages relating to tcl/tk on a debian-type linux system
>> apt-cache search tcl tk | less
* search for extensions for tk
>> apt-cache search libtk | less
* get lots of useful libraries and packages for tcl/tk
>> sudo apt-get install tcllib
* show the list of packages that are in tcllib
>> apt-cache show tcllib
SSL is useful for lots of things, but one is connecting to imap servers.
Although national security agencies may have broken the encryption.
* install the ssl secure sockets library for tcl
>> sudo apt-get install tcl-tls
EXECUTING TCL
* execute a tcl script (s.tcl doesnt have to be executable)
>> tclsh s.tcl
The technique below also works for multiline inputs
* execute a one line script in tcl
>> echo "puts hello" | tclsh
* execute a tcl script and pipe a file listing to it
>> ls | tclsh s.tcl
* a shebang line in the script
-----
#!/usr/bin/tclsh
Then... chmod +x s.tcl
Then... ./s.tcl
Command Line Options ‹↑›
how to use command line options to a tcl script
puts "There are $argc arguments to this script"
puts "The name of this script is $argv0"
if {$argc > 0} {puts "The other arguments are: $argv" }
puts "You have these environment variables set:"
foreach index [array names env] {
puts "$index: $env($index)"
}
Executing Tk ‹↑›
execute a tk script (s.tcl doesnt have to be executable)
wish s.tcl
a shebang line at the top of the script
#!/usr/bin/wish
Gotchas Executing Tcl Tk ‹↑›
If the file has dos line endings, then on linux wish may
complain about a "bad interpreter #!/usr/bin/wish". The solution
is "dos2unix file.tcl"
install the documentation for tcl and tk (this provides 'man' pages)
sudo apt-get install tk-doc
install the documentation for tk version 8.5
sudo apt-get install tk8.5-doc
view the documentation for the 'button' tk widget
man 3tk button
view the documentation for the file 'globbing' command
man 3tcl glob
view some short syntax documentation for tcl/tk
man 3tcl Tcl
give a bad option to a command in order to get a list of real options
frame .g -relief sss
use "$widget configure" to see current configuration for a widget
frame .g ; .g configure
The code below could be much more useful with a search
box and better formatting of the text and some good navigation
keystrokes
display a tcl/tk man page in a text box
--------
set tt [exec man 3tk listbox 2>/dev/null]
text .t -font {-size 12}
.t insert end $tt
pack .t
focus .t
bind . x exit
,,,
Proceedures Or Functions ‹↑›
No language can prosper with functions or proceedures.
If there is no explicit return statement, then the
result of the last command in the procedure is the
returned value
In Tcl proceedures begin (by convention) with an upper case letter.
Proceedures seem to have to be defined before where they are used
determine the name of the current proceedure
-------
proc Do {args} {
puts "proc = [lindex [info level 0] 0]"
}
Do
,,,
Default Arguments Or Parameter Values ‹↑›
a proceedure with default parameter values
proc Test {a {b 7} {c -2}} {
expr $a / $b + $c
}
puts [Test 4.0]
Note the double curly braces below. If they are not
used then the Tcl interpreter thinks that the
procedure has 2 arguments.
a proceedure with one default parameter
proc Message {{m "default"}} {
puts "Argument was: $m"
}
Message
Message "hi"
a proceedure with one argument and (empty) default
proc Message {{m ""}} {
puts "Argument was: $m"
}
Message
Message "hi"
Global Variables And Procedures ‹↑›
Also look at the upvar command, which appears to be
important in this context.
a proceedure using global variables
proc Test {} {
global n m
return [expr $n/$m]
}
set n 20
set m 4
puts "[Test]"
Function Pointers ‹↑›
The name of a proceedure can be held in a variable. Which
is not quite function pointers, but seems similar
a simple example of a function name in variable
---------
proc addTax {x} {expr $x*1.16}
set fn addTax; set amount 10
puts "$amount with tax is: [$fn $amount]"
,,,
A tcl array ? of pseudo function pointers
------
proc f(1) {} {puts hello}
proc f(2) {} {puts world}
proc f(3) {} {puts again}
for {set i 1} {$i<=3} {incr i} {f($i)}
,,,
determine what "pointers" are defined
info proc f(*) => f(1) f(2) f(3)
I think built in list mappings are tcl/tk 8.6
using a map function
j -----
proc fn {x} {expr $x*1.16}
set l1 {1 2 3 4}
set l2 [map fn $l1]
,,,
Recursive Proceedures ‹↑›
the factorial function written recursively
------
proc fac x {expr {$x<2? 1: $x*[fac [incr x -1]]}}
puts [fac 4]
,,,
Widget Procedures ‹↑›
Brent Welch uses an important technique in his book about tcl/tk.
This is a procedure which takes an arbitrary number of arguments
and returns a widget. This is a way to "extend" widgets, or a
kind of non-object oriented inheritance.
The example below is like a masterclass in tcl/tk programming.
Notice that the procedure returns a text area not a frame.
It also takes care of laying out the scrolled text widget.
The eval command is important. It allows us to supply all the
same options to the ScrollText widget as we can to the normal
text widget. In this sense it is reminiscent of inheritance.
create a scrolled text area
proc ScrollText { f args } {
frame $f
eval {text $f.text -wrap none \
-xscrollcommand [list $f.xscroll set] \
-yscrollcommand [list $f.yscroll set]} $args
scrollbar $f.xscroll -orient horizontal \
-command [list $f.text xview]
scrollbar $f.yscroll -orient vertical \
-command [list $f.text yview]
grid $f.text $f.yscroll -sticky news
grid $f.xscroll -sticky news
grid rowconfigure $f 0 -weight 1
grid columnconfigure $f 0 -weight 1
return $f.text
}
set t [ScrollText .f -width 40 -height 6 -font {courier 12}]
pack .f -side top -fill both -expand true
#set in [open [file join {~/sf/htdocs/books} tcl tcl-book.txt]
set in [open "$env(HOME)/sf/htdocs/books/tcl/tcl-book.txt"]
$t insert end [read $in]
close $in
Libraries And Packages ‹↑›
The source command reads and evaluates all tcl commands in
a file. This is a simple way to include a file containing
procedures in the current script
load all tcl files in a dir
---------
foreach script [glob lib/*.tcl] {
source $script
}
,,,
Running Scripts ‹↑›
run a tk graphical script
wish script.tk
We can also run a tk graphical script using tclsh as long as
the line "require package Tk" is present. This has the great advantage
on windows of writing puts statements to the console (which doesnt
seem to happen with wish)
run a tk script using tclsh
tclsh script.tk
Useful Libraries ‹↑›
tcllib - this seems to do all sorts of useful stuff.
Ternary Operator ‹↑›
compressed if, then, else
-----
set a 2
puts [expr {$a<2? "a<2": "a>=2"}]
,,,
an example of a compressed 'if/then/else' structure
[proc fac x {expr {$x<2? 1: $x*[fac [incr x -1]]}}]
Control Flow ‹↑›
If Then Etc ‹↑›
an if then example
-------
if { test1 } {
body1
} elseif { test2 } {
body2
} else {
bodyn
}
,,,
the if text doesnt always need braces ...
if [file isdirectory $file] {
an if clause with a logical AND operator
-------
set a 3; set b 4
if { $a>2 && $b>3 } {
puts "a>2 and b>3"
}
,,,
an if clause with a NOT operator
-------
set a 3; set b 4
if { ![expr $a>6] } {
puts "a is not > 6"
}
,,,
logical or
if { $a>2 || $b>3 } {
testing equality
-------
set a 3; set b 4
if { [expr {$a/1} ] == [expr {$b/double(1)}] } {
puts "a==b"
} else { puts "!=" }
,,,
For Counted Loop ‹↑›
Is there a language which doesnt have a foreach loop?
Probably Lisp and Tcl/Tk may have been inspired by Lisp
partly but it has a counted for loop.
a for loop
--------
for {set i 0} {$i < 10} {incr i} {
puts "I inside first loop: $i"
}
,,,
a for loop using hexadecimal
--------
for {set i 0} {$i < 0x100} {incr i} {
puts "I : $i"
}
,,,
While Loops ‹↑›
an incrementing while loop
puts "Start"
set i 0
while {$i < 10} {
puts "i: $i"
incr i
}
loop through a file line by line
set f [open /etc/passwd r]
while {[gets $f line] >= 0} {
puts $line
}
loop through standard input and print each line out
while {[gets stdin line] >= 0} {
puts $line
}
Foreach Loops ‹↑›
foreach with explicit lists and 2 index vars
foreach {a b} {red green yellow blue} {
puts "a:$a b:$b"
}
loop through lines of a file
foreach line [split [read $f] \n] {
puts $line
}
test speed of a foreach loop with filter
set start [clock milliseconds]
set list [split [exec cat /usr/share/dict/words] \n]
set newlist {}
foreach word $list {
if {[string match *et* $word]} {
lappend newlist $word
#puts $word
}
}
set end [clock milliseconds]
puts \
"Filtered [llength $list] items in [expr {$end-$start}] ms"
prompt a user for input at the terminal
puts -nonewline "enter something:"
flush stdout
gets stdin response
,,,
Arithmetic And Mathematics In Tcl ‹↑›
get help for tcl math functions
man 3tcl mathfunc
for speed put expression in braces
puts "12/3=[expr {12/3}]"
do integer division and display the result
----
set a 21; set b 10
puts "$a/$b=[expr {$a/$b}]"
,,,
do decimal (non integer) division and display the result
----
set a 21; set b 10
puts "$a/$b=[expr $a/double($b)]"
,,,
another way to do non integer division
puts "14/3=[expr 14/3.0]"
some basic arithmetic using expr
---------
set a 21; set b 10
set c [expr $a + $b]
,,,
A problem: find 'd' where
x/d < y
So find the least value of d so that x/d < y
The problem below is relevant when scaling images to
fit into a screen or widget size. The ceil function can
be used instead of a loop
find the least divisor of a number etc
---------
set x 2345; set y 1024
for {set i 2} {$i < 1000} {incr i} {
set q [expr {$x/$i}]
if {$q < $y} {
puts "$x/$i=$q < $y";
break
}
}
,,,
Subtraction ‹↑›
simple subtraction
--------
set q 4
puts [expr {12-$q}]
#puts [expr {12/double($q)}]
,,,
Double Or Decimal Numbers ‹↑›
I wonder if 2.0==2 ??
double division
--------
set q 4
puts [expr {12/$q}]
puts [expr {12/double($q)}]
,,,
is a double the same as an integer
--------
set q 4
if {[expr {12/$q}] == [expr {12/double($q)}]} {
puts "yes 12/$q == 12/4.0"
}
,,,
Integer Techniques ‹↑›
check if a division is an integer
{[expr {12/$q}] == [expr {12/double($q)}]}
how ceil works
-------
puts "ceil(2.1) = [expr {ceil(2.1)}]"
puts "ceil(2.9) = [expr {ceil(2.9)}]"
puts "ceil(2.0) = [expr {ceil(2.0)}]"
puts "ceil(5/2.0) = [expr {ceil(5/2.0)}]"
,,,
use ceil to round up
-------
set a 5; set b 2
puts "[expr {ceil($a/double($b))}]"
,,,
Math Functions ‹↑›
In the example below we place a new function into the
'mathfunc' namespace which allows us to use it easily with
the expr command.
get help for all tcl built in mathematics functions
man 3tcl mathfunc
define a new math function and use it
------
proc tcl::mathfunc::calc {x y} {
expr { ($x**2 - $y**2) / exp($x**2 + $y**2) }
}
puts [expr {calc(3,4)}]
,,,
The way to evaluate mathematical functions and operations
in tcl/tk is to use the expr command
eg
----
if { [expr {1000/double(4)}] > [expr {200/double(5)}] } {
puts yes
}
,,,
eg
----
if { [expr 1000/double(4) > 200/double(5)] } {
puts yes
}
,,,
Gotchas For Expr ‹↑›
If you forget a closing curly brace for and expr expression
then tcl does not complain, but doesnt do what you want it to.
no error, no output
puts "[expr {ceil(2.5)]"
Random Numbers ‹↑›
get help for the 'rand()' mathematics functions
man 3tcl mathfunc
use rand() with expr
srand() resets the seed of the random generator
but is not really necessary
generate 30 random numbers between 0 and 1 inclusive
-----
for {set i 0} {$i < 30} {incr i} {
puts "$i: [expr {rand()}]"
}
,,,
The following maybe useful for doing random shuffles of lists
using the lsort -command ...
generate 30 random numbers between -1 and 1
-----
for {set i 0} {$i < 30} {incr i} {
puts "$i: [expr {rand()*2 - 1}]"
}
,,,
generate 30 random numbers between 0 and 99 inclusive
-----
for {set i 0} {$i < 30} {incr i} {
puts "$i: [expr { int(100 * rand()) }]"
}
,,,
generate 1 pseudo random number 0<n<10 using the clock
------
puts [expr {[clock microseconds] % 10}]
,,,
This seems to get better results than using the rand function!
get a random element from a list
---------
set ll {seed grain pebble book paper sheet}
for {set i 0} {$i < 10} {incr i} {
set f [lindex $ll [expr {[clock microseconds]%[llength $ll]}]]
puts $f
}
,,,
Powers Of Numbers ‹↑›
In Tcl expressions x^2 is not x*x, use x**2 instead
raise 4 to power 3
------
set x 4
puts "x**3 = [expr {$x**3}]"
,,,
Modulus Arithmetic ‹↑›
do a modulus calculation and display the result
----
set a 21; set b 10
puts "$a % $b = [expr $a % $b]"
,,,
Pythagoras Theorem ‹↑›
Or just use the hypot function
Find the length of the hypotenuse of a rectangle
-------
proc Hypot {x1 y1 x2 y2} {
return [expr {sqrt(($x2-$x1)**2 + ($y2-$y1)**2)}]
}
puts "Length of Hypotenuse: [Hypot 0 0 3 4]"
,,,
use the hypot function
----
puts "Hypotenuse of 3,4 is [expr {hypot(3,4)}]"
,,,
mathfunc
Trigonometry ‹↑›
find the sin of x+y
set x 1; set y 0; puts [expr {sin($x+$y)}]
Comparisons ‹↑›
compare 2 numbers in tcl
-----
set y 2; set x 1
if {$y > $x} {
puts "y ($y) is greater than x ($x)"
}
,,,
compare using 'expr'
set x 1; set y 3; puts [expr $y > $x]
test if 4*2 < 7
expr {4*2 < 7}
Geometry ‹↑›
We could simplify this slightly by using the
hypot math function.
Determine if a point is within a circle. That is, if it
proc InCircle {point center radius} {
set x [lindex $point 0]
set y [lindex $point 1]
set cx [lindex $center 0]
set cy [lindex $center 1]
puts "x=$x,y=$y,cx=$cx,cy=$cy"
if {[expr {sqrt(($cx-$x)**2 + ($cy-$y)**2)}] <= $radius} {
return true
} else {
return false
}
}
puts [InCircle {3 4} {0 0} 5]
slightly simplified
proc InCircle {point center radius} {
set x [expr {[lindex $point 0] - [lindex $center 0]}]
set y [expr {[lindex $point 1] - [lindex $center 1]}]
if {[expr {hypot($x,$y)}] <= $radius} {
return true
} else {
return false
}
}
puts [InCircle {3 4} {0 0} 5]
Variables ‹↑›
Tcl/Tk does not have variable assignment syntax, so a=2 does
not work. Use 'set' instead. In general tcl/tk has a very minimalist
syntax.
assign a value to a variable
set x 1;
Global Variables ‹↑›
It is not necessary in bind functions to make global
just in proceedures.
declare variables as global within a procedure.
global size ll .lb tt
Unicode ‹↑›
Unicode needs its own section it is such a big topic.
print the rain kanji character
puts "雨"
print a kanji using its unicode code point
puts "\u2F72"
convert an integer to a unicode character
------
set value 0x2f72
set char [format %c $value]
puts $char
,,,
print a list of kanji
-------
for {set i 0x2F52} {$i < 0x2F62} {incr i} {
puts [format %c $i]
}
,,,
When the list gets big, the text box gets very slow
to scroll
a list of kanji in a text box
-------
text .t -font {-size 14}
for {set i 0x2F00} {$i < 0x2FFF} {incr i} {
.t insert end " [format %c $i] "
}
pack .t; focus .t; bind . x exit
,,,
a label with the rain kanji character
--------
label .l -text "雨" -font {-size 22}
pack .l -padx 20 -pady 20
bind . x exit
,,,
a label with the rain kanji character
--------
set text [exec unicode rain]
text .t -font {-size 14}
.t insert end $text
pack .t
bind . x exit
,,,
display hebrew and cyrillic blocks
-------
set text [exec unicode 0450..0520]
puts $text
,,,
display some kanji
-------
set text [exec unicode 2F52.. ]
puts $text
,,,
Japanese writing, probably the hardest known to animal or beast
deserves its own chapter. Modern tcl/tks can display all these
characters in the tk widgets. hooray
display 1-5 stroke kanji with z/Z zooming
--------
font create ff -size 18
option add *font ff
pack [text .t]; .t insert end "
Kanji with 1-5 strokes
一人入力二十八七九刀丁大上下子小口山三女土工士万川丸千夕亡干久己
才寸弓手日中不引分水切心文内方公化月天木火止反元今予戸支少王区円
片父五太欠友仏夫毛比牛氏六午犬尺仁出生立本付目外主用代打民正白世
平半仕石市加収失末古北母台申圧四可未広庁弁布示必田処写号辺礼句去
旧左刊他玉由司右令史札包以皮冬犯幼兄永氷功矢穴冊皿央合気行地会年
自名当同先回成有全交色向死安任次団共多休好字早伝式在"
bind . z { font conf ff -size [expr [font conf ff -size]+2] }
bind . Z { font conf ff -size [expr [font conf ff -size]-2] }
bind . x exit
,,,
random colour all 1-5 stroke kanji in labels
--------
font create ff -size 34
option add *font ff
proc Colour {} {format #%06x [expr {int(rand() * 0xFFFFFF)}]}
set kk "一人入力二十八七九刀丁大上下子小口山三女土工士万川丸千夕亡干久己
才寸弓手日中不引分水切心文内方公化月天木火止反元今予戸支少王区円
片父五太欠友仏夫毛比牛氏六午犬尺仁出生立本付目外主用代打民正白世
平半仕石市加収失末古北母台申圧四可未広庁弁布示必田処写号辺礼句去
旧左刊他玉由司右令史札包以皮冬犯幼兄永氷功矢穴冊皿央合気行地会年
自名当同先回成有全交色向死安任次団共多休好字早伝式在"
set sixten "再曲老血両光肉西列米守百印存仮争件考仲衣吸毎各宅因羽虫灯危至耳糸州寺竹宇池舌灰兆羊后机見体言作身売社来車決何利足別形対返役初投近花局冷労私防声図応位住医角乱告余状走判改兵助低男折良町芸技児条赤完快否材我系求村究似束災弟囲序均志卵里君豆麦忘希困谷臣批孝貝努坂汽物学国取的事金者所実長定法性明直空受知和画表味官制夜放治苦命門店使居価雨念具例参東並歩育果青始服呼波非板林油固若券延版注効協底刻英委典河岸武供府周宝乳担易季毒京岩松舎届往承妻幸泣宙肥卒宗沿拡枚述牧垂拝忠刷泳招芽枝姉昔径妹前発食面重後風度点持海乗相品政神信思変保屋音送軍科指追計急退要飛室独草界限待南洋美活段約負客首巻建便派専省逆洗春茶単査星型栄背厚県昨故映係級染研則秋祖昼祝革奏紅皇砂城律迷紀炭看勇宣畑姿柱拾浅泉胃秒肺昭書家通高時流原員真連差特配病記消起格校料素紙党根速能院留残害造座案値容破殺帰降修師弱挙馬酒席航帯骨息射除財納夏借旅益個従笑宮将荷島梅庭純秘庫株脈展浴討候粉訓徒恩針倍胸孫倉勉朗耕桜俳俵班郡陛蚕理動産部務教転強"
regsub -all {\s+} $kk {} kk
set ll [split $kk {}]
puts "colouring [llength $ll] kanji"
set ii 0
foreach k $ll {
label .l$k -text $k -bg [Colour]
grid .l$k -row [expr {$ii/20}] -column [expr {$ii%20}]
incr ii
}
bind . z { font conf ff -size [expr [font conf ff -size]+2] }
bind . Z { font conf ff -size [expr [font conf ff -size]-2] }
bind . x exit
,,,
kanji in labels
-------
set kanji [split [exec unicode 2F62.. ] { }]
foreach k $kanji {
catch {
label .$k -text $k -font {-size 14}
pack .$k }
}
bind . x exit
,,,
Fonts For Unicode Characters ‹↑›
How do we get the right fonts for displaying, for example, kanji?
---
pack [text .t]
.t insert end "Japan \u65E5\u672C\n"
.t insert end "Athens \u0391\u03B8\u03AE\u03BD\u03B1\u03B9"
,,,
see what funny glyphs your tcl/tk can display
pack [text .t -font {Helvetica 16}]
.t insert end "
Arabic \uFE94\uFEF4\uFE91\uFEAE\uFECC\uFEDF\uFE8D\uFE94\uFEE4\uFEE0\uFEDC\uFEDF\uFE8D
Trad. Chinese \u4E2D\u570B\u7684\u6F22\u5B57
Simplified Chinese \u6C49\u8BED
Greek \u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AE\
\u03B3\u03BB\u03CE\u03C3\u03C3\u03B1
Hebrew \u05DD\u05D9\u05DC\u05E9\u05D5\u05E8\u05D9\
\u05DC\u05D9\u05D0\u05E8\u05E9\u05D9
Japanese \u65E5\u672C\u8A9E\u306E\u3072\u3089\u304C\u306A,\
\u6F22\u5B57\u3068\u30AB\u30BF\u30AB\u30CA
Korean \uB300\uD55C\uBBFC\uAD6D\uC758 \uD55C\uAE00 (\u3CA2\u3498)
Russian \u0420\u0443\u0441\u0441\u043A\u0438\u0439\
\u044F\u0437\u044B\u043A
"
bind . x exit
Comments ‹↑›
These are preceded by the #.
Notice that a ; semicolon is necessary after tcl/tk code and before
a comment on the same line.
some valid comments
------
# a comment
set a 1; # a comment
,,,
Strings ‹↑›
Comparing Strings ‹↑›
check if 2 strings are equal
if {[string equal $s $t]} { ... }
check if a string is empty
if { [ string length $nameVar ] == 0 } { }
an easier way to check if a string is empty
---
set val {}
puts [expr {$val ne "" ? "not empty" : "empty"}]
,,,
using the funny 'ni' word
------
set a "tree"; set b "re"
if { $a ni $b } {
puts "no"
}
,,,
Matching Strings ‹↑›
check a string for a simple ('glob') pattern
if {[string match *A* "1234A5678"]} { puts yes }
check for any letter (protect tcl special chars with {...})
if {[string match {*[A-Z]*} "aAa"]} { puts yes }
check if one string is contained in another, ignoring case
---------
set a "re"; set b "TREE";
if {[string match -nocase *$a* $b]} { puts yes }
,,,
another (worse) way to check if 'tree' contains 're'
-----
set a re; set b tree
if {[string first $a $b] == -1} { puts no }
,,,
Concatenating Or Joining Strings ‹↑›
The most efficient way is with 'append'
concatenate 2 strings onto the first
--------
set a "abc"; set b "xyz"; set c "ABC"
append a $b $c
puts $a
,,,
a less efficient way to join strings
------
set x "abc";
set x "$x efg"
puts $x
,,,
Formatted String Output ‹↑›
The 'format' command is based on the c 'printf' function
print the 2nd string in a list
set a 2; puts [format "%${a}\$s" one two three];
Scan extracts data from strings. It may be an alternative to regexp
for parsing text data.
get data from strings
scan etc
Substrings ‹↑›
get the first character of a string
string index $s 0
get the last character of a string
string index $s end
get the 2nd last character of a string
puts "2nd last is [string index "train" end-1]"
delete upto the first instance of ':'
---
set s "aa:bb"
puts [string range $s [string first : $s] end]
,,,
multiline strings work fine in tcl tk
puts "This is
a multiline string with
interpolation [string index "train" end-1]"
delete up to the first occurrance of the character ':'
------
set s "data: tree"
puts [lindex [split $s :] 1]
,,,
Splitting Strings ‹↑›
Splitting of strings can be done with regexp or split. Tcl
'split' has the staggering limitation that is can only split on
a character (not on a string or regular expression). A
workaround might be to use the power of awk in an exec and then
process with tcl (or else use regexp -all)
split on : character
puts [split "a:b:c" :]
get the last word of a sentence
-----
puts [lindex [split "one two tree" " "] end]
,,,
split a string into a list of characters
puts [split "abcdefgh" {}]
split on a '*' or ':' character
puts "list is: [split "ab:cde*fg*h" {*:}]"
split into words
------
set text "Some arbitrary text which might include \$ or {"
set wordlist [regexp -inline -all {\S+} $text]
for {set i 0} {$i < [llength $wordlist]} {incr i} {
puts "$i: [lindex $wordlist $i]"
}
,,,
The technique below almost works but doesnt get the last
paragraph unless it ends with a blank line
split into paragraphs
------
set text "
first para Some
2nd line
arbitrary
text which might
3rd Para
"
set wordlist [regexp -inline -all -- {.*?\n\s*\n} $text]
for {set i 0} {$i < [llength $wordlist]} {incr i} {
puts "$i: [lindex $wordlist $i]"
}
,,,
set wordlist [regexp -inline -all -- {.*?\n\s*\n} $text]
for {set i 0} {$i < [llength $wordlist]} {incr i} {
puts "$i: [lindex $wordlist $i]"
}
,,,
a simpler technique for paragraph splitting ?
set text [exec zcat /usr/share/doc/bash/INTRO.gz]
regsub -all {\n\s*\n} $text "\n<:par:>\n" text
puts $text
Substituting In Strings ‹↑›
replace multiple commas with only one comma in $tt
regsub -all {,+} $tt "," tt
a substitution with the string map function
string map {abc 1 ab 2 a 3 1 0} "1abcaababcabababc"
will return the string 01321221.
put quotes around a match
regsub -nocase {\yabc\y} "x abc y" {"&"} ss; puts $ss
get a submatch
regsub -nocase {.*(abc).*} "xyz abc xyz" {\1} ss; puts $ss
get rid of the "<" and ">" characters
----
puts [string map {< {} > {}} "<lmn>"]
,,,
Length Of Strings ‹↑›
do something if a string is longer than 1000
if { [string length $message] > 1000 } { ... }
Interpolating In Strings ‹↑›
insert a variable in a string
puts "the variable s is $s"
interpolate the result of a command into a string
puts "the variable s is [...]"
This is like the backticks or $(...) in the bash shell and it
can be nested. This is a powerful feature of tcl which may be
reminiscent of lisp...
Whitespace ‹↑›
Its interesting how often silly little bugs come down
to tabs and spaces and \n \r etc
trim whitespace from the ends of a string
set new [string trim $last_name]
Regular Expressions ‹↑›
The balm of the webprogrammer, regexs are a must. And
tcl can do them. yay...
check if an email address has an @ etc
-------
set email "bob at bigcorp"
if { ![regexp {.+@.+} $email] } {
puts "Your email address doesn't look valid."
}
,,,
search for 'abc' in string $t ignoring case
set t "catABC"; if { [regexp -nocase {abc} $t] } { puts "has 'abc'" }
save a match in a variable
set m {}; regexp {c.*g} "abcdefghi" m; puts $m
Submatches ‹↑›
A 'submatch' is a bracketed expression within a regular expression
pattern. It is a handy way to extract only the data which you are
interested in.
save a submatch into a variable
set m {}; regexp {[^:]+:(.*)} "height:1234" junk m; puts "m=$m"
Above the variable $junk is the whole matched pattern and is not used.
Sometimes -> is used, not sure why
Non Greedy Matching ‹↑›
Non greedy matching matches the shortest possible match instead of the
longest. The '?' character is used to modify other regex special characters.
save a submatch into a variable
set m {}; regexp {a.*?b} "abcabcabc" m; puts "m=$m"
Lists And Regular Expressions ‹↑›
The regexp function can also return a list of matches
split some text into an array of words
-------
set text "Some arbitrary text which might include \$ or {"
set wordlist [regexp -inline -all -- {\S+} $text]
puts $wordlist
,,,
split some multiline text into paragraphs and return as a list
set paralist [regexp -inline -all -- {.*?\n\s*\n} $text]
The line above doesnt capture the last paragraph always...
Unicode Regexp Patterns ‹↑›
You can use 'character classes' such as [[:alpha:]] to match
non latin alphabet characters.
match unicode letter characters
set m {}; regexp {[[:alpha:]]+} ":abcd..$%^" m; puts $m
Gotchas For Regular Expressions ‹↑›
If the pattern does not match the text then the match variable
may be undefined and will throw and error if accessed.
throws an error!
regexp {c.*g} "abdefghi" m; puts "m=$m"
set the variable first
set m {}; regexp {c.*g} "abdefghi" m; puts "m=$m"
Uppercase And Lowercase ‹↑›
convert a string to uppercase
set s "mIxeD"; puts [string toupper $s]
Paragraph Matching ‹↑›
open a text file and split on paragraphs
set f [open "~/ev/.ev.private.txt" r]
set text [read $f]
set thisday [clock format [clock scan "today"] -format {%d%B%Y}]
set pp [regexp -inline -all -- {.*?\n\s*\n} $text]
set datetext {}
foreach ii $pp {
set datetext xx
set curdate {}
#puts "$ii"
regexp {/date[s]?\s+([^\n]*)} $ii -> datetext
catch {set curdate [clock format [clock scan $datetext] -format {%d%B%Y}]}
#if {[string equal $thisday $curdate ]} {
set name {}; regexp {/name\s+([^\n]*)} $ii -> name
#set location {}; regexp {/location\s+([^\n]*)} $ii -> location
puts "$name \n $datetext"
#puts "$name\n $curdate"
#puts "datetext=$datetext"
#puts "thisday=$thisday"
#puts "curdate=$curdate"
#}
}
use the 'in' and 'ni' operators to see if an
element is contained in a list
append an element to a list
lappend l one two; puts $l
append an entire list to another
------
lappend ff {*}[glob *.jpg]
puts [join $ff \n]
,,,
the most succinct way to get a list from lines of a file
set ll [split [read [open "~/Pictures/fav.txt" r]] \n]
A one element list is just a value as well, confusing?
split a string into a list of characters
-------
puts [lindex [split "a" {}] 0]
puts [split "a" {}] ; # the same
,,,
Accessing Elements Of A List ‹↑›
get the 2nd element of a list
-------
set ll {star moon rain wind earth}
puts "Second Element is '[lindex $ll 1]'"
,,,
Tcl/Tk makes things easy, so we can use the special 'end' index
to get the last element of a list. This is like [llength $list] - 1
get the last element of a list
-------
set ll {star moon rain wind earth}
puts "last Element is '[lindex $ll end]'"
,,,
Random Elements Of A List ‹↑›
get a random element of a list
-------
set ll {star moon rain wind earth}
puts "Random element: [lindex $ll [expr { int([llength $ll] * rand()) }]]"
,,,
Strangely there are a suspicious number of repeated elements
when I run the code below
print 20 random elements from a list
-------
expr {srand([clock clicks])}
set ll {star moon rain wind earth water sea air}
for {set i 0} {$i < 20} {incr i}
puts "$i: [lindex $ll [expr { int([llength $ll] * rand()) }]]"
}
,,,
Lassign ‹↑›
lassign what does it do?
assign the values in {} list to variables x,y,z
lassign {a b c} x y z; puts "z is $z"
Deleting Elements ‹↑›
delete the 3rd element from a list
-----
set ll {a b c d}
set ll [lreplace $ll 2 2]
puts $ll
,,,
delete an element by value
-------
set ll {a b c d}
set idx [lsearch $ll "b"]
set ll [lreplace $ll $idx $idx]
puts $ll
,,,
Joining Lists ‹↑›
a join
set ll {leaf bark sap twig}
puts "[join $ll |] \n n"
Constructing Lists ‹↑›
append a value to a list
-------
set ll {a b c d}
lappend ll e
puts $ll
,,,
create a list with split
-------
set group_source [ split $itext [ regexp {^$} $itext ] ]
,,,
Filtering Lists ‹↑›
By filtering, I mean removing certain elements based on
a particular criteria.
get a list of unique words used in some text
---------
set text "
and if we have. and. if have:when
is all is sleep"
set words [split $text " \n\t.:"]
set words [lsort -unique -dict $words]
puts [join $words \n]
,,,
Mapping Lists ‹↑›
In tcl 8.6 'lmap' is available
transform a list
----
set nn {a b c d e f}
set mm [lmap s $nn { set s "$s:" }]
puts $mm
,,,
Sorting Lists ‹↑›
sort a list in dictionary (alphabetical ?) order
-----
set ww [list zz A a c ee dd ff]
set ww [lsort -dict $ww]
puts [join $ww \n]
,,,
The code below is definitely not the most efficient way
to do this, but demonstrates the use of the -command option
to lsort
reverse the order of a list
-----
proc Reverse {a b} {
return 1
}
set ww [list zz A a c ee dd ff]
set ww [lsort -command Reverse $ww]
puts [join $ww \n]
,,,
-command must return negative, zero, or positive integer
randomly sort a list, or shuffle it
-----
proc Shuf {x y} { expr {[clock micros]%3 - 1} }
set ww [list a b c d e f g h i j]
set ww [lsort -command Shuf $ww]
puts [join $ww \n]
,,,
trying to use rand to shuffle
-----
proc Shuf {a b} {
# rand is not working ??
#return [expr {rand()*2 - 1}]
return [expr {([clock microseconds]%3) - 1 }]
}
set ww [list a b c d e f g h i j]
set ww [lsort -command Shuf $ww]
puts [join $ww \n]
,,,
it seems the command has to be a named function ...
!!(doesnt work) lsort -command { expr {[clock micro]%3 -1}} $ww
Iterating Lists ‹↑›
Iteration is just a fancy word for looping. Use foreach or for etc
iterate through a list using an index number
------
set wordlist [list a b c dd ee ff]
for {set i 0} {$i < [llength $wordlist]} {incr i} {
puts "$i: [lindex $wordlist $i]"
}
,,,
Arrays ‹↑›
The word 'array' in the context of tcl/tk means 'associative
array' or what is sometimes called in other languages a 'hash'.
An array is like a dictionary where each word is associated with
a value (eg a definition)
See also 'dict' which is a lower level type of hash
simulate a multidimensional array in tcl
set a(1,1) 0 ;# set element 1,1 to 0
Creating Arrays ‹↑›
create an empty array
array set myArray {}
create array 'balloon' and set the color variable
set balloon(color) red
array set balloon {color red} ; # the same
create and display an array
------
array set n {size big colour blue}
puts [array get n]
,,,
set value where both array name and item are in variables
set ${key}($item) $val
Accessing Elements ‹↑›
display the color key of array 'balloon'
puts [set $balloon(color)]
get the color key of array 'balloon'
set c $balloon(color)
get the color key of array 'balloon'
set key color; set c $balloon($key)
Analysing Arrays ‹↑›
check if a key exists
info exists array(key)
Iterating Arrays ‹↑›
It is hard to get array values out in the same order that
they were put in.
loop through a tcl array using names (keys)
puts "You have these environment variables set:"
foreach index [array names env] {
puts "$index: $env($index)"
}
Dictionarys ‹↑›
Dictionaries are a new data structure introduced in
tcl/tk 8.5 Not sure how they work, but look promising.
Lets keep this simple and just use mutt (but you have to install
and configure it etc- not entirely painfree)jfk
sort the lines of the muttrc aliases
-------
set aa [exec sed -n "/^alias /s/alias *//p" $env(HOME)/.muttrc]
puts [join [lsort [split $aa \n]] \n]
,,,
select lines from the muttrc aliases using a filter function
-------
proc Filter { ll pattern } {
set rr {}
foreach item $ll {
if {[string match -nocase *$pattern* $item]} {
lappend rr $item
}
}
return $rr
}
set aa [exec sed -n "/^alias /s/alias *//p" $env(HOME)/.muttrc]
set ss [Filter [split $aa \n] "ama"]
puts [join $ss \n]
,,,
Mutt And Tcl Tk ‹↑›
A simple way to send email with tcl/tk is to use the
mutt terminal mail client. You need to configure mutt
first to be able to find and authenticate to your
email server.
a mutt example
exec mutt -s $subject -a $attachment $to << $body
To do, a subject line that appears and disappears.
Font zooming in the message area. (what is this
small screen big font bug??). Reverse colour keystroke.
Search email addresses. Show images in email addresses
An attachment box keystroke...
Using big fonts on a small screen in a text box seems to
create packing problems (widgets after the text widget
dont display)
simple but working email sending interface
wm attributes . -zoomed 1
wm title . "Send mail"
font create df -family {Courier New} -size 12
font create mFont -family {Courier New} -size 12
font create aFont -family {Courier New} -size 14
# searches a list for a string pattern
proc Filter { ll pattern } {
set rr {}
foreach item $ll {
if {[string match -nocase *$pattern* $item]} {
lappend rr $item
}
}
set rr [lsort -dictionary $rr]
return [join $rr \n]
}
set aa [exec sed -n "/^alias /s/alias *//p" $env(HOME)/.muttrc]
set addressList [lsort [split $aa \n]]
set help "z/Z zoom text | x exit | s send message
A send with attachments | g test sending with attachments
T send test message | <Enter> to type | h show help
H hide help | <Escape> or <Enter>. enter command
ml list image file names in ~/Pictures/mail.txt
l reduce message box size | L increase message box size
t enter recipient | a show mail addresses | S mail subject"
set recipient ""
set status $help
set subject "Stuff"
text .addr -font aFont -height 10 -highlightcolor blue
entry .to -textvar recipient -width 40 -font df
entry .subject -textvar subject -width 40 -font df
text .body -font mFont -height 15
label .status -textvar status -font df
pack .to -fill x -anchor w -pady 4 -padx 20
#pack .body -fill x -expand 1 -padx 20
pack .body -fill x -padx 20
pack .status
# focus .body
# dont propagate text widget events to toplevel
bindtags .body [list all Text .body]
bindtags .to [list all Entry .to]
bindtags .subject [list all Entry .subject]
bind . s {
set message [.body get 1.0 end]
if {![string match -nocase *@* $recipient]} {
set status "Strange recipient?... Mail not sent"
focus .to
break
}
if {![string match -nocase {*[a-z]*} $message]} {
set status "Strange message?... Mail not sent"
focus .body
break
}
set status "Sending message..."
update idletasks
set t1 [clock milliseconds]
exec echo "$message" | mutt -s $subject $recipient
set t2 [clock milliseconds]
set status "sent message '$subject' to $recipient
in [expr {($t2-$t1)/1000.0}] seconds"
puts "Recipient: $recipient, Subject: $subject
Message:
$message
"
}
bind . A {
set message [.body get 1.0 end]
if {![string match -nocase *@* $recipient]} {
set status "Strange recipient?... Mail not sent"
focus .to
break
}
if {![string match -nocase {*[a-z]*} $message]} {
set status "Strange message?... Mail not sent"
focus .body
break
}
set status "Sending message with attachments in ~/mail.txt..."
update idletasks
set t1 [clock milliseconds]
set in [open "$env(HOME)/Pictures/mail.txt"]
set ff [split [read $in] \n]
puts [lindex $ff 0]
puts [lindex $ff 1]
puts [lindex $ff 2]
set a1 [lindex $ff 0]
set a2 [lindex $ff 1]
set a3 [lindex $ff 1]
set attach [join $ff " "]
set attach [exec cat "$env(HOME)/Pictures/mail.txt" | head -n1]
exec echo "$message" | mutt -s $subject -a "$a1" "$a2" "$a3" -- $recipient
set t2 [clock milliseconds]
set status "sent message '$subject' to $recipient
in [expr {($t2-$t1)/1000.0}] seconds"
puts "Recipient: $recipient, Subject: $subject
Message:
$message
"
}
bind . ml {
set in [open "$env(HOME)/Pictures/mail.txt"]
set ff [split [read $in] \n]
puts [join $ff " "]
}
# reduce message lines
# this doesnt work if the text box is packed with -fill y or
# expand
bind . l {
puts [.body config -height]
set h [lindex [split [.body config -height] " "] 4]
puts $h
.body config -height [expr {$h-2}]
}
# increase message lines
bind . L {
puts [.body config -height]
set h [lindex [split [.body config -height] " "] 4]
puts $h
.body config -height [expr {$h+2}]
}
bind . t { focus .to; .to icursor end }
bind . S {
pack .subject -after .to -fill x -padx 20 -pady 4
focus .subject; .subject icursor end
}
bind .body <Return>. { focus .; set status $help }
bind .body <Escape> { focus .; set status $help }
bind . a {
.addr delete 1.0 end
.addr insert end "
Type q to hide addresses, j/k up, down
<Enter> to paste address \n\n"
.addr insert end [join $addressList \n]
pack .addr -before .to -fill both -padx 20 -pady 4
.addr mark set insert 1.0
focus .addr
}
bind .addr q { pack forget .addr }
# Make some vi keys for the address box
bind .addr j {
event generate .addr <Down>
.addr tag add sel "insert linestart" "insert lineend"
break
}
bind .addr k {
event generate .addr <Up>
.addr tag add sel "insert linestart" "insert lineend"
break
}
bind .addr / {
focus .to; break
}
bind .addr <Return> {
set recipient [.addr get "insert linestart" "insert lineend"]
set recipient [lindex [split $recipient " "] end]
set recipient [string map {< {} > {}} $recipient]
pack forget .addr
focus .body
break
}
bind .to <Escape> { focus .; set status $help }
bind .to <Return> {
focus .body; pack forget .addr
}
bind .to <FocusIn> {
if { $recipient eq "" } {
set recipient "Recipient:"
.to selection range 0 end
.to icursor end
}
}
bind .to <FocusOut> {
if { $recipient eq "Recipient:" } {
set recipient ""
}
}
bind .to <KeyRelease> {
.addr delete 1.0 end
.addr insert end [Filter $addressList $recipient]
}
bind .subject <Escape> { focus .; set status $help }
bind .subject <Return> {
focus .body; pack forget .subject
}
bind .subject <FocusIn> {
if { $subject eq "" } {
set subject "Subject:"
.subject selection range 0 end
.subject icursor end
}
}
bind .subject <FocusOut> {
if { $subject eq "Subject:" } {
set subject ""
}
}
bind . <Return> { focus .to }
bind . z {
font conf df -size [expr [font conf df -size]+2]
font conf mFont -size [expr [font conf mFont -size]+2]
}
bind . Z {
font conf df -size [expr [font conf df -size]-2]
font conf mFont -size [expr [font conf mFont -size]-2]
}
bind . h { set status $help }
bind . H { set status "press h for help" }
bind . T {
set recipient "mjbishop@fastmail.fm"
set subject "Testing tcl mail"
set status "Send a test mail"
focus .body
}
bind . T {
set recipient "mjbishop@fastmail.fm"
set subject "Testing tcl mail"
set status "Send a test mail"
focus .body
}
bind . x exit
sending mail is usually done with this protocol, but why not just use mutt?
I suppose you will have to connect to the smtp server with tls and a login.
an old example from 2002
-------
package require mime
package require smtp
set rcpts [open email-list.txt]
set files [lsort [glob *.JPG]]
set text {Some of the pictures got mixed up}
append text {Let's try this again.}
set i 0
while {[gets $rcpts rcpt] != -1} {
set this_image [lindex $files $i]
# create an image and text
set imageT [mime::initialize -canonical \
"image/jpg; name=\"$this_image\"" -file $this_image]
set textT [mime::initialize -canonical text/plain -string $text]
# create a multipart containing both, and a timestamp
set multiT [
mime::initialize -canonical multipart/mixed -parts [
list $imageT $textT]]
# send it to some friends
puts "Sending to $rcpt: $this_image"
smtp::sendmessage $multiT \
-header [list From {Juliane Mello <jmello@usu>}] \
-header [list To $rcpt] \
-header [list Subject Ooops!]
incr i
}
,,,
POP3 ....
consider using offlineimap with tcl/tk. This way you always have
an up-to-date local copy of you email.
tkbiff - check an email account. But looks a bit unmaintained
http://wiki.tcl.tk/9270
Using expect to check imap account.
http://wiki.tcl.tk/3064
more interesting stuff
Tcllib has the imap4 package which can communicate with imap mail
servers.
install the necessary debian packages for ssl and imap4
sudo apt-get install tcllib tcl-tls
tips to use the imap4 code with ssl
------
package require tls ; # must be loaded for TLS/SSL
set ::imap4::use_ssl 1 ; # request a secure connection
set chan [::imap4::open $server]
,,,
The code below says "sslv3 alert handshake failure"
This may have something to do with the 'poodle' security prob
with sslv3. So ...
the problem was fixed by disable sslv3 !!
tls::init -tls1 true -ssl2 false -ssl3 false
connect to a fastmail imap account and show 1st 3 messages
package require tls
package require imap4
set ::imap4::use_ssl 1
tls::init -tls1 true -ssl2 false -ssl3 false
set pass xxx
# Connect to server
set imap [::imap4::open "mail.messagingengine.com"]
::imap4::login $imap mjbishop@fastmail.fm $pass
::imap4::select $imap INBOX
# Output all the information about that mailbox
foreach info [::imap4::mboxinfo $imap] {
#puts "$info -> [::imap4::mboxinfo $imap $info]"
}
# fetch 3 records inline
set fields {from: to: subject: size }
foreach rec [::imap4::fetch $imap :3 -inline {*}$fields] {
puts -nonewline "#[incr idx])"
for {set j 0} {$j<[llength $fields]} {incr j} {
puts "\t[lindex $fields $j] [lindex $rec $j]"
}
}
# Show all the information available about the message ID 1
puts "Available info about message 1: [::imap4::msginfo $imap 1]"
# Use the capability stuff
puts "Capabilities: [::imap4::isableto $imap]"
puts "Is able to imap4rev1? [::imap4::isableto $imap imap4rev1]"
# Cleanup
::imap4::cleanup $imap
The code below returns a list of numbers
search for all message > 4K which have been seen
-------
::imap4::search $chan larger 4000 seen
puts "Found messages: [::imap4::mboxinfo $chan found]"
Found messages: 1 3 6 7 8 9 13 14 15 19 20
,,,,
connect to a fastmail imap account and fetch hundredth message header
package require tls
package require imap4
set ::imap4::use_ssl 1
tls::init -tls1 true -ssl2 false -ssl3 false
set pass xxxx
set chan [::imap4::open "mail.messagingengine.com"]
::imap4::login $chan mjbishop@fastmail.fm $pass
::imap4::select $chan INBOX
set fields {from: to: subject: size }
set rec [lindex [::imap4::fetch $chan 100:100 -inline {*}$fields] 0]
foreach item $rec { puts $item }
# Show all the information available about the message ID 1
puts "Available info about message 1: [::imap4::msginfo $chan 100]"
::imap4::cleanup $chan
Searching Imap Mail ‹↑›
The following is a simple inbox search. The results should be
displayed in a listbox. Also it is slow when a lot of results
are returned. So it would be better to use and offline maildir
search by subject
------
package require tls
package require imap4
option add *font "Courier 14"
wm attributes . -zoomed 1
tls::init -tls1 true
set ::imap4::use_ssl 1
entry .pass -textvar pass;
label .l -textvar ii -justify left -padx 50 -pady 20
text .t -width 70 -height 30
entry .search -textvar what
pack .l
set ii "Press c to connect"
bindtags .pass [list all Entry .pass]
bindtags .search [list all Entry .search]
bind . x exit
bind . c {
set ii "Enter Password"
pack forget .search; pack .pass; focus .pass
}
bind . / {
set ii "Enter search term"
pack forget .pass; pack forget .t; pack .search; focus .search
}
bind .pass <Return> {
set ii "Connecting..."
set chan [::imap4::open "mail.messagingengine.com"]
if {[catch {::imap4::login $chan mjbishop@fastmail.fm $pass} result]} {
set ii "Incorrect password \n $result"
} else {
::imap4::select $chan INBOX
set total [::imap4::mboxinfo $chan exists]
set ii "Connected: $total messages in inbox"
}
pack forget .pass; focus .
}
bind .search <Return> {
if {$what eq ""} {
pack forget .search; focus .
}
set ii "Searching $total mail subject lines for '$what'"
::imap4::search $chan subject $what
set ll [::imap4::mboxinfo $chan found]
set ii "Found [llength $ll] emails matching '$what' in subject"
set fields {from: subject: date: }
.t delete 1.0 end
foreach id $ll {
set head [lindex [::imap4::fetch $chan $id:$id -inline {*}$fields] 0]
.t insert end "[join $head |] \n"
}
pack .t
pack forget .search; focus .
#catch {::imap4::close $chan}
}
,,,
connect to a fastmail imap account and search for messages by subject
package require tls
package require imap4
set ::imap4::use_ssl 1
#tls::init -tls1 true -ssl2 false -ssl3 false
tls::init -tls1 true
set pass xxx
set chan [::imap4::open "mail.messagingengine.com"]
::imap4::login $chan mjbishop@fastmail.fm $pass
::imap4::select $chan INBOX
::imap4::search $chan subject shares
puts "Searched messages: [::imap4::mboxinfo $chan found]"
set fields {from: to: subject: size }
#set rec [lindex [::imap4::fetch $chan 100:100 -inline {*}$fields] 0]
#foreach item $rec { puts $item }
::imap4::cleanup $chan
#::imap4::close $chan
#::imap4::logout $chan
find out how many mails are in a mail folder
------
package require tls
package require imap4
option add *font "Times 14"
tls::init -tls1 true
set ::imap4::use_ssl 1
entry .e -textvar pass; label .l -textvar mails
pack .e .l; focus .e
bind .e <Return> {
set chan [::imap4::open "mail.messagingengine.com"]
::imap4::login $chan mjbishop@fastmail.fm $pass
::imap4::select $chan INBOX
set mails "[::imap4::mboxinfo $chan exists] mails in INBOX"
catch {::imap4::close $chan}
}
,,,
Deleting Mail ‹↑›
Messages have flags such as new, read, deleted etc.
mark a message or messages as deleted and then really delete
--------
::imap4::store $chan $start_msgid:$end_msgid +FLAGS "Deleted"
::imap4::expunge chan
,,,
get a web page
package require http
set token [http::geturl http://www.beedub.com/index.html]
puts [http::data $token]
,,,
get a web page through a proxy server
package require http
http::config -proxyhost eg.com -proxyport 8080
set token [http::geturl http://www.beedub.com/index.html]
puts [http::data $token]
,,,
Http Etc ‹↑›
Curl is a comprehensive wget like program for retrieving urls
in the most convoluted of circumstances. So it may well be a boon
that tcl has bindings for its library...
install libcurl tcl bindings
sudo apt-get install tclcurl
SYNTAX OF TCL/TK
get help for all tcl syntax
man 3tcl Tcl
values for options can be abbreviated if its unique
pack [frame .g -relief sun]
pack [frame .g -relief sunken] # the same
commands can be separated with a semi-colon
puts "hi"; puts " there"
use a semi colon to separate commands
puts -nonewline "enter a number"; flush stdout
Stdin And Stdout ‹↑›
stdin and stdout are special files highly useful
when using pipes etc |
create a listbox with elements each 1 line of stdin
set i [split [read stdin] \n]
pack [listbox .lb -listvariable i]
The script above can be executed with, for example
cat doc.txt | wish ss.tcl
and each element of the list box will be one line
from the text file 'doc.txt'. This is useful for creating
'zenity' or 'yad' like tools which can integrate with
bash scripts
create a maximized listbox with stdin
wm attributes . -zoomed 1
set i [split [read stdin] \n]
pack [listbox .lb -width 100 -listvariable i]
Printing Or Displaying Text On Stdout ‹↑›
Web examples use the map function to create colours
colors
colourfull text with ansi escape sequences
-----
set yy {\x1B[0;33m}
set nn {\x1B[0m}
puts "\033\[01;31m $yy Colour full"
,,,
Reading Stdin ‹↑›
read stdin one character at a time
set c [read stdin 1]
while {$c != "q"} {
puts -nonewline "$c"
set c [read stdin 1]
}
read one line of stdin and assign to variable
puts -nonewline "Enter your name: "
flush stdout
set name [gets stdin]
puts "Hello $name"
loop through standard input and print each line out
while {[gets stdin line] >= 0} {
puts $line
}
loop through stdin and print each line
foreach line [split [read stdin] \n] { puts $line }
Internationalisation ‹↑›
use message catalogs.
message catalog eg
------
package require msgcat
puts "hello" # standard
puts [msgcat::mc "hello"] # with a message catalog
,,,
Folders And Directories ‹↑›
Fix the directory name for the platform
set basedir [string trimright [file join [file normalize /home/mjb] { }]]
puts $basedir
display the number of tcl files in this folder
puts [llength [glob -nocomplain *.tcl]]
Globbing ‹↑›
Glob returns an error if nothing matches which needs to be
handled or else use -nocomplain .
file tools
glob - returns a list of file names |
show all txt files in the current directory:
---------
puts [join [glob *.txt] \n]
,,,
use multiple patterns with glob
----
set f ~/Pictures
set ll [glob -nocomplain -directory "[file dirname $f]" *.jpg *.JPG]
puts $ll
,,,
The following uses the nifty eval trick, which one
is not likely to stumble across by trial and error
put the multiple glob patterns in a list
set ss [list *.jpg *.png *.gif]
set ll [eval {glob -directory $env(HOME)/} $ss]
foreach f $ll { puts $f }
puts "found [llength $ll] text files"
print the users home folder
puts $env(HOME)
other tricks to get home folders
---
puts [exec find [glob -dir /home -type d *]/Pictures -iname '\*']
catch {exec find [glob -dir /home -type d *]/Pictures -iname '**'} result
puts $result
,,,
The tilde "~" does not expand when it is included as part of
a file name, but the "glob" command does seem to understand it.
find all the txt files in the user's home directory
puts [glob -directory ~ *.txt]
find all the txt files in the user's home directory
puts [exec {echo $HOME}]
find all subdirectories of the current directory
glob -type d *
find all home folders
puts [glob -directory /home/ -type d *]
puts [glob -dir /home -type d *]
create a combobox with the names of subfolders as values
pack [ttk::combobox .c -textvariable state -values [glob -type d *]]
find all files whose name contains an "a", a "b" or "cde"
glob -type f *{a,b,cde}*
Finding Files ‹↑›
maybe use tclx recursive_glob
fileutil::find is this recursive
a one folder level recursive jpg file search
set ll [glob -dir $env(HOME)/Pictures *.{jpg,JPG} */*.{jpg,JPG}]
foreach f $ll { puts $f }
puts "found [llength $ll] jpegs"
a 2 folder level recursive txt file search
set ll [glob -dir $env(HOME) *.txt */*.txt */*/*.txt]
foreach f $ll { puts $f }
puts "found [llength $ll] text files"
put the file search patterns in a list
set base $env(HOME)
set ss [list *.txt */*.txt */*/*.txt]
set ll [eval {glob -nocomplain -type {f r} -directory $base} $ss]
# foreach f $ll { puts $f }
puts "found [llength $ll] text files"
find lots of jpegs without recursion
set base $env(HOME)
set ss [list *.{jpg,JPG} */*.{jpg,JPG} */*/*.{jpg,JPG} \
*/*/*/*.{jpg,JPG} */*/*/*/*.{jpg,JPG} */*/*/*/*.{jpg,JPG}]
set t1 [clock millis]
set ll [eval {glob -nocomplain -type {f r} -directory $base} $ss]
set t2 [clock millis]
puts "found [llength $ll] jpg files in [expr $t2-$t1] ms"
Trying to glob "/root/" will produce a permissions error.
So that error needs to be caught, otherwise no results are
given.
globbing the root
set ll [glob -directory / *]
foreach f $ll { puts $f }
The code below also fails when there are permissions problems if
there is no {d r} directory and readable, or {f r} file and
readable.
The following 2 lines are not equivalent. In fact the first sometimes
finds more files than the second, strange as that may seem. Also
the first seems nearly 25% faster. But maybe there are cross platform
problems with the first... I am not sure
lappend ff {*}[glob -nocomplain -type {f r} -dir $baseDir $pattern]
lappend ff {*}[glob -nocomplain -type {f r} [file join $baseDir $pattern]]
find files matching a pattern
proc findFiles { baseDir pattern } {
set dirs [glob -nocomplain -type {d r} -dir $baseDir *]
set ff {}
foreach dir $dirs {
lappend ff {*}[findFiles $dir $pattern]
}
lappend ff {*}[glob -nocomplain -type {f r} -dir $baseDir $pattern]
return $ff
}
set t1 [clock milliseconds]
set ll [findFiles $env(HOME) "*.{jpg,JPG}" ]
set t2 [clock milliseconds]
puts "Found [llength $ll] files in [expr $t2-$t1] ms"
,,,,
* find files with file join, but this is slower
proc findFiles { baseDir pattern } {
set dirs [glob -nocomplain -type {d r} [file join $baseDir *]]
set ff {}
foreach dir $dirs {
lappend ff {*}[findFiles $dir $pattern]
}
lappend ff {*}[glob -nocomplain -type {f r} [file join $baseDir $pattern]]
return $ff
}
set t1 [clock milliseconds]
set ll [findFiles $env(HOME) "*.jpg" ]
set t2 [clock milliseconds]
#puts [ join $ll \n ]
puts "Found [llength $ll] files in [expr $t2-$t1] ms"
,,,,
* find files bigger than a certain size.
proc findFiles { baseDir size } {
set dirs [glob -nocomplain -type {d r} -dir $baseDir *]
set ff {}
foreach dir $dirs {
lappend ff {*}[findFiles $dir $size]
}
set gg [glob -nocomplain -type {f r} -dir $baseDir *]
foreach f $gg {
if {[file size $f] > $size} {
lappend ff $f
}
}
return $ff
}
set t1 [clock milliseconds]
set ll [findFiles $env(HOME) 60000000 ]
set t2 [clock milliseconds]
puts [join $ll \n]
puts "Found [llength $ll] files in [expr $t2-$t1] ms"
,,,,
The code below is an almost useful app.
* show big files in a listbox and delete on 'd' press (careful!)
---------
proc findFiles { baseDir size } {
set dirs [glob -nocomplain -type {d r} -dir $baseDir *]
set ff {}
foreach dir $dirs {
lappend ff {*}[findFiles $dir $size]
}
set gg [glob -nocomplain -type {f r} -dir $baseDir *]
foreach f $gg {
if {[file size $f] > $size} {
set ssize [file size $f]
if {$ssize > 1000000000} {
set ssize "[expr $ssize/1000000000]G"
} elseif {$ssize > 1000000} {
set ssize "[expr $ssize/1000000]M"
} elseif {$ssize > 1000} {
set ssize "[expr $ssize/1000]K" }
lappend ff "$ssize $f"
}
}
return $ff
}
wm attributes . -zoomed 1
set limit 100000000
set ll [findFiles $env(HOME) $limit]
wm title . "Found [llength $ll] files bigger than $limit"
listbox .lb -listvar ll -width 60 -font {-size 14}
pack .lb -fill both -expand true; focus .lb
bind .lb j { event generate .lb <Down> }
bind .lb k { event generate .lb <Up> }
bind .lb m {
wm title . "finding big files on media, please wait ..."
set ll {}
update idletasks
set ll [findFiles /media/ 100000000 ]
wm title . "Found [llength $ll] files bigger than $limit"
}
bind .lb d {
#puts [.lb get [.lb curselection]]
set index [.lb curselection]
set f [lindex [split [.lb get [.lb curselection]]] 1]
puts $f
set ll [lreplace $ll $index $index]
file delete $f
wm title . "Deleted $f, hope thats ok!"
}
bind .lb h {
puts "
j - down
k - up
d - delete current file
m - show media files
h - show this help
"
}
bind .lb x exit
a more verbose recursive file find
proc findFiles { basedir pattern } {
# add a separator to folder
set basedir \
[string trimright [file join [file normalize $basedir] { }]]
set fileList {}
# {f r} only readable normal files
foreach fileName [glob -nocomplain -type {f r} -path $basedir $pattern] {
lappend fileList $fileName
}
foreach dirName [glob -nocomplain -type {d r} -path $basedir *] {
set subDirList [findFiles $dirName $pattern]
if { [llength $subDirList] > 0 } {
foreach subDirFile $subDirList {
lappend fileList $subDirFile
}
}
}
return $fileList
}
set ll [findFiles $env(HOME) "*.txt" ]
puts [ join $ll \n ]
puts "Found [llength $ll] files"
But this produced errors when the folder is
unreadable, and it also seems slow compared to
searches above
find files using tcllibs "fileutil"
package require fileutil
set basepath $env(HOME)
foreach file [fileutil::findByPattern $basepath *.tcl] {
puts $file
}
,,,,
This is not working below.
* brent welchs recursive findfile
------
proc Findfile { startDir namePat } {
set pwd [pwd]
if {[catch {cd $startDir} err]} {
puts stderr $err
return
}
foreach match [glob -nocomplain -- $namePat] {
puts stdout [file join $startDir $match]
}
foreach file {[glob -nocomplain *]} {
puts $file
if [file isdirectory $file] {
FindFile [file join $startDir $file] $namePat
}
}
cd $pwd
}
Findfile /home/arjuna/Pictures *.JPG
Filenames ‹↑›
return the name of the file without folder
-----
set f "/usr/share/dict/words.en.txt"
puts "[file tail $f]"
,,,
turn a relative file name into an absolute one
----
set fn ../bob
set fn [file join [pwd] $fn]
puts $fn
,,,
get the file name extension
-----
set f "/usr/share/dict/words.en.txt"
puts "suffix of $f is [file extension $f]"
,,,
file name with no extension
-----
set f "/usr/share/dict/words.en.txt"
puts "rootname of $f is [file rootname [file tail $f]]"
,,,
Tildes ‹↑›
expanding the ~ into the home folder can cause problems.
file open seems to do it well.
Encodings Of Text Files ‹↑›
show all encodings known to tcl/tk
puts [lsort [encoding names]]
read a text file in a russian encoding
-----
set in [open text.russian]
fconfigure $in -encoding iso8859-7
,,,
Reading Files ‹↑›
read the entire contents of a text file
set f [open "/usr/share/dict/words" r]
set s [read $f]; puts $s
close $f
the most succinct, but then you cant close the file pointer
puts [read [open "~/Pictures/fav.txt" r]]
another way to read a file on unix (but doesnt expand '~' tildes)
set data [exec cat $filename]
read 2 text files at once on unix
set data [exec cat $file1 $file2]
compare performance of 'exec cat' vs 'open/read'
---------
set f /usr/share/dict/words
set a [clock milliseconds]
set data [exec cat $f $f $f]
set b [clock milliseconds]
puts "exec cat time: [expr $b-$a] ms"
set a [clock milliseconds]
set p [open $f r]; set data [read $p]; close $p
set p [open $f r]; append data [read $p]; close $p
set p [open $f r]; append data [read $p]; close $p
set b [clock milliseconds]
puts "open/read time: [expr $b-$a] ms"
,,,
Linux seems to buffer the contents of $f above after the
first 'exec cat' but the first time it is about twice as slow
read the entire contents of a text file
set f [open "~/sf/htdocs/books/tcl/tcl-book.txt" r]
set s [read $f]
puts $s
close $f
read a file a line at a time
-------
set fp [open $some_file]
while {-1 != [gets $fp line]} {
puts "The current line is '$line'."
}
,,,
The code below will fail if the script has been piped to tclsh or
wish (eg cat s.tcl | tclsh). In this case the value of $argv0 will
be 'tclsh' or 'wish'
read the contents of the script file itself
puts "The name of this script is $argv0"
set f [open $argv0 r]
set s [read $f]
close $f
File Checks ‹↑›
an entry box which turns red if invalid file name
-------
entry .e -textvar file -width 50 -validate all -validatecommand {isFile %P .e}
button .b -text "Browse..." -command {set file [tk_getOpenFile]}
pack .e .b
proc isFile {f w} {
if { [file exists $f] && [file isfile $f] } {
$w configure -fg black
} else {
$w configure -fg red
}
return 1
};
,,,
Writing Text Files ‹↑›
http://wiki.tcl.tk/928
a good tk text example, with opening saving etc
Also use 'flush' to force writing of data.
write one line of text to a text file
------
set data "Written by tcl.\n"
set filename "test.txt"
set f [open $filename "w"]
puts -nonewline $f $data
close $f
,,,
In the above closing the file $f causes the data to
be written straight away.
Below the 'w' event is activated even when the focus is
in the text box. which is not really what we want.
write text to a file from a text box
------
pack [text .t]; focus .t
bind . w {
set data [.t get 1.0 {end -1c}]
set f [open "test.txt" w]
puts -nonewline $f $data
close $f
}
bind . x exit
,,,
Time And Date ‹↑›
clock scan - parse a free string date including '1 day ago'
clock clicks - time code
print todays date in the format 'Friday, the 14 of August 2015'
-----
set now [clock seconds]
puts [clock format $now -format {Today is: %A, the %d of %B %Y}]
,,,
convert 'today' into a formatted date such as 'Friday, 14 August 2015'
-----
set d [clock scan "today"]
puts [clock format $d -format {Date: %A, %d %B %Y}]
,,,
check if a date is today
-----
set d [clock format [clock scan "today"] -format {%d%B%Y}]
puts $d
set e [clock format [clock scan "15aug2015"] -format {%d%B%Y}]
puts $e
,,,
check if a date is today , another way to do it
-----
#set now [clock seconds]
set now [clock sec]
set d [clock format $now -format {%d%B%Y}]
puts $d
set e [clock format [clock scan "15aug2015"] -format {%d%B%Y}]
puts $e
,,,
test if a string represents todays date
---
set date "15 aug 2016"
if { [string equal \
"[clock format [clock scan "today"] -format {%d%B%Y}]" \
"[clock format [clock scan $date] -format {%d%B%Y}]" ]} {
puts yes
} else { puts no }
,,,
print the current time in hours minutes and seconds
------
puts "The time is: [clock format [clock seconds] -format %H:%M:%S]"
,,,
The 'catch' phrase below stops the application from
throwing an error when a bad date string is typed
turn values like 'tomorrow' into actual dates with clock scan
---------
label .info \
-text {Type a text date like 'tomorrow' etc, esc=exit}
label .l -textvar tm -font {-size 20}
entry .e -width 40 -textvar s -font {-size 20}
pack .info .e .l; focus .e
bind .e <Return> {
set tm "'$s' is [clock format [clock scan $s]]"
set s {}
}
bind . <Escape> {exit}
,,,
Timezones ‹↑›
Another thorny issue are relative times and time zones.
The complications are endless.
find out what time it is in new york right now
-------
set t [clock seconds]
puts [clock format $t -timezone :America/New_York]
,,,
Arithmetic Of Times And Dates ‹↑›
It is often good to be able to add and subtract
hours/minutes/days etc from a calendar time. This is
easy with clock add
clock add time unit keywords
seconds, minutes, hours, days, weeks, months, or years,
find the date 2 weeks ago
--------
set a [clock seconds]
set b [clock add $a -2 weeks]
# clock add $a -2 w - the same
puts "Two weeks ago: [clock format $b]"
,,,
date and time arithmetic can also be done with the 'clock scan' function
print yesterdays date
---------
puts [clock format [clock scan "1 day ago"]]
puts [clock format [clock scan "-1 day"]] # the same
,,,
keywords with clock scan:
year, month, fortnight, week, day, hour, minute, second
tomorrow, yesterday, today, now, last, this, next ago
date 2 weeks before yesterday, gives strange results
---------
puts [clock format [clock scan "yesterday 2 fortnight ago"]]
puts [clock format [clock scan "2 fortnight ago"]]
,,,
one week before the 11th of july
----
puts [clock format [clock scan "11 july -1 week"]]
,,,
Performance Of Code ‹↑›
Performance of code can be measured in milliseconds.
measure elapsed time
--------
set a [clock milliseconds]
after 500
set b [clock milliseconds]
puts "elapsed: [expr $b - $a] ms"
,,,
Introspection ‹↑›
What does a language or program know about itself? Or pretend to know
show info
puts [info args clock]
show all tcl/tk commands which have 'cl' in their name
puts [info commands *cl*]
show all built-in tcl/tk commands sorted alphabetically
puts [lsort -dictionary [info commands]]
list all built-in math functions
puts [lsort -dictionary [info functions]]
show the folder where the script is running
puts [file dirname [info script]]
The above may just print '.' which is not helpful
give a bad option to a command to get a list of valid options
string -abc
The code below works by parsing the tcl/tk error message from a
bad option, deleting the 1st part of the message and then splitting
the options into a list.
display a list of options to a command in a listbox
-----
catch {string -junk} result
set options [string range [lindex [split $result :] 1] 9 end]
set oo [split $options ,]
pack [listbox .l -listvar oo -font {-size 16}]
,,,
Widget Introspection ‹↑›
cget
Widgets dont engage in philosophy but they do have some kind of
self-knowledge if we are happy to accept that sophistry.
".widget configure" or an abbreviation thereof, returns a list with all
possible configuration options and their values. This is invaluable. Mainly
we are interested in the indices 0,3,4 of this list
display all possible config options of a listbox
-----
listbox .lb -font {-size 14}
pack .lb
foreach item [.lb config] {
.lb insert end "[lindex $item 0]"
}
bind . x exit
,,,
display config options and their values
-----
wm attributes . -zoomed 1
listbox .lb -font {-size 14}
pack .lb -fill both
foreach item [.lb config] {
.lb insert end \
"[lindex $item 0]=[lindex $item 3] [lindex $item 4]"
}
bind . x exit
,,,
Threads ‹↑›
use fileevent to open a thread on a long running process
set $f [open "|find . -name '*big*'" r]
pack [text .t]
fileevent $f readable { .t insert end [gets $f]}
update a label once per second (but seems slower)
proc tick {} {
global ss; incr ss
after 1000 [list after 1000 tick]
}
set ss 0
pack [label .l -textvar ss -font {-size 20} -padx 20]
tick; puts "After tick"
use 'after' to update gui in event loop
proc doOneStep {} {
if {[::my_calc::one_step]} {
after idle [list after 0 doOneStep]
}
}
doOneStep
Fileevents ‹↑›
The fileevent command is a powerful way to process data a bit
at a time from a pipe without blocking your application.
a simple fileevent with pipe example
set pipe [open "|some command"]
fileevent $pipe readable [list Reader $pipe]
proc Reader { pipe } {
if [eof $pipe] {
catch {close $pipe}
return
}
gets $pipe line
# Process one line
}
Perhaps the "&| cat" trick below redirects standard error, but I
am not sure.
without the &| cat seems to also work
if [catch {open "|locate *.jpg"} input] {
read from a long running process and print results
----------
if [catch {open "|locate *.jpg &| cat"} input] {
puts "Error: \n $input"
} else { fileevent $input readable Log }
proc Log {} {
global input
if [eof $input] {
catch {close $input}
} else {
gets $input line
puts $line
}
}
bind . s {
global input
catch {close $input}
puts "process stopped!"
}
bind . x { exit }
,,,
In the code below, using "update idletasks" after every element is
appending to the listbox works, but... it is really slow. A solution
would be to only update the display every hundredth time etc. That
is better but still twice as slow as not updating at all.
update a listbox with the results from a long running process
----------
wm attributes . -zoomed 1
set count 0
set message "Finding Jpegs: $count"
label .l -textvar message
listbox .lb -width 40 -height 40
pack .l .lb -side top -fill x -expand true
if [catch {open "|locate *.jpg &| cat"} input] {
puts "Error: \n $input"
} else { fileevent $input readable Log }
proc Log {} {
global input count .lb .l message
if [eof $input] {
catch {close $input}
} else {
gets $input line
.lb insert end $line
.lb see end
incr count 1
set message "Jpegs: $count"
# using the following is slow
if { [expr $count % 100] == 0 } then {
# update idletasks
}
}
}
bind . s {
global input message count
catch {close $input}
set message "Process stopped! Found $count Jpegs"
}
bind . x { exit }
,,,
Vwait allows us to implement what are called sentry, or flag variables
in threaded programming.
wait for a variable to be set
-------
set x 0
after 500 {set x 1}
vwait x
,,,
Widget Libraries ‹↑›
gnocl- gtk style widgets for tcl/tk
The gnocl widgets are not compatible, option for option
with the standard Tk widgets.
By George Howlett. Includes large data graph widget
tabbed notebook, treeview, transparent busy widget with
watch cursor. C based
www.sourceforge.net/projects/blt
infrastructure for creating new widgets. Balloon
help, tabbed windows, paned window and heirarchy
browser. C based and compound.
tix.sf.net
install the tix widgets
sudo apt-get install tix
Tix contains lots of good widgets, including a 'balloon help'
(also known as 'tool tips')
use the tix widget library
-------
package require Tix
,,,
Incr Tk ‹↑›
[incr Tk] and [incr Widgets]
create compound widgets with [incr Tcl] object
system. Lots of widgets. Labelled entry,
html display.
Book: [incr Tcl] from the Ground Up
incrtcl.sf.net
Bwidgets ‹↑›
Tcl based. Tabbed notebook, combobox, hierarchy browser.
www.sf.net/projects/tcllib
Tktable ‹↑›
spreadsheet like widget.
www.sf.net/projects/tktable.
Widgets ‹↑›
Widget is the general name given to different
types of components in the graphical system. Examples
include frames, entry boxes, listboxes, dialogs etc
The code below demostrates the useful technique of returning
a widget from a proceedure.
return a listbox widget widget from a function
---------
proc NewListbox {f args} {
frame $f
listbox $f.list
eval {$f.list configure} $args
pack $f.list;
return $f.list
}
set l [NewListbox .f -listvariable fonts]
pack .f -expand yes -fill both
set fonts [lsort [font families]]
bind . x exit
,,,
Windows ‹↑›
set the title of a window
wm title . "New Title"
get the size and position of the main window
update; puts [wm geometry .]
display a window with the title hello
echo 'wm title . "hello"' | wish
destroy all widgets except the main window
eval destroy [winfo children .]
show a button
wm title . "hello"
button .b -text push -command {exit}
pack .b
Resizing Windows And Widgets ‹↑›
We seem to use the <Configure> event to grab window and widget resizes.
display widget sizes
---------
label .l -text Hello -font {-size 16};
pack .l -fill x
tkwait visibility .l
.l config -text "label dimensions: [winfo width .l]w x [winfo height .l]h px"
bind . <Configure> {
.l config -text \
"label dimensions: [winfo width .l]w x [winfo height .l]h px"
}
,,,
display widget sizes using geometry
---------
label .l -text Hello -font {-size 16};
pack .l -fill x
tkwait visibility .l
.l config -text "label geometry: [winfo geometry .l]"
bind . <Configure> {
.l config -text "label geometry: [winfo geometry .l]"
}
,,,
Dimensions Of Windows And Widgets ‹↑›
Widgets are sometimes referred to as 'windows' because they
are usually displayed in one, or have one, ...
create a top level window 400px wide by 300 high
------
wm geometry . 400x300
,,,
In the code below, we have to wait until the window is actually
displayed or else the sizes with be reported as 1x1
wait for the visibility of the widget .tk
tkwait visibility .t
wait for a window to appear before computing size
label .l -textvar size -font {-size 12}
pack .l -fill x -expand true
tkwait visibility .l
set size "label size=[winfo width .l]w x [winfo height .l]h px"
bind . x exit
screen dimensions
winfo screenwidth
winfo screenheight
see how much space we have on the screen
wm attributes . -fullscreen true
label .l -text "screen width=[winfo screenwidth .] screen height=[winfo screenheight .]"
pack .l -fill x
bind . x exit
Full Screen ‹↑›
a full screen window (tk 8.5)
wm attributes . -fullscreen 1
button .b -text push -command {exit} -font {-size 30}
pack .b
get the pixel dimensions of the toplevel window
tk_bisque
wm attributes . -fullscreen true
pack [label .l -textvar tt]
tkwait visibility .
set tt "window: width=[winfo width .], height=[winfo height .]"
bind . x exit
a full screen window (tk 8.5) with browny colours
tk_bisque
toplevel .foo
wm attributes .foo -fullscreen 1
pack [button .b -text push -command {exit}]
Visibility Of Windows And Widgets ‹↑›
The technique below can be used to create a dynamic interface
which adapts to the context of the application.
check if a widget is visible (mapped) and swap it if so
-------
label .l1 -text {first label}
label .l2 -text {2nd label}
pack .l1
bind . s {
if {[winfo ismapped .l1]} {
pack forget .l1; pack .l2
} else {
pack forget .l2; pack .l1
}
}
,,,
Minimized Windows ‹↑›
iconify when escape is pressed
---------
button .b -text push -command {exit} -font {-size 20}
pack .b
bind . <KeyPress-Escape> "wm iconify ."
,,,
Maximised Windows ‹↑›
a maximized window
wm attributes . -zoomed 1
button .b -text push -command {exit} -font {-size 30}
pack .b
Window Colours ‹↑›
In the example below, the background colour for the label
is not affected by the main window bg colour.
change background colour for the main window
------
. configure -borderwidth 20 -bg black
pack [label .l -text hello]
,,,
Dialog Boxes ‹↑›
A dialog box is a little window that pops up in order to annoy the user
by asking for information.
display a message box when a button is pressed
button .btn -text "press me" -command {
tk_messageBox -message "Button was pressed" }
pack .btn -padx 20 -pady 20
display a message box when a button is pressed
button .btn -text "press me" -command {
tk_messageBox -message "Button was pressed" }
pack .btn -padx 20 -pady 20
display an error message box when a button is pressed
button .btn -text "press me" -command {
tk_messageBox -icon error -message "Button was pressed" \
-title "some problem" -type abortretryignore
}
pack .btn -padx 20 -pady 20
tk dialogs
tk_getOpenFile |
tk_getSaveFile |
tk_chooseDirectory |
tk_chooseColor |
display a color chooser dialog when a button is pressed
button .btn -text "Choose a Colour" -command {
tk_chooseColor -initialcolor cyan1 -title "Which Colour"
}
pack .btn -padx 20 -pady 20
a folder chooser
button .btn -text "Choose a Folder" -command {
tk_chooseDirectory -initialdir /home/ -title "which folder"
}
pack .btn -padx 20 -pady 20
a dialog to open only '.gif' files
set typelist {
{"GIF Image" {".gif"}} {"GIF Image" {} {"GIFF"}}}
button .btn -text "Choose a file" -command {
tk_getOpenFile -filetypes $typelist
}
pack .btn -padx 20 -pady 20
a dialog to open only '.txt' files and display which was chosen
set typelist {{"" ".txt"}}
button .btn -text "Choose a file" -command {
set f [tk_getOpenFile -filetypes $typelist]
.l configure -text "choice: $f"
}
label .l -text choice:
pack .btn .l -padx 10 -pady 10
Frames ‹↑›
Frames are very useful for grouping elements within a graphical
user interface. This helps with layout problems and also allows
a whole feature of the interface to be easily hidden and shown
dynamically. eg with "pack forget $widget" where widget may be
a frame
possible reliefs are: flat, groove, raised, ridge, solid, or sunken
label frames
In the example below the entry box gets longer (but not fatter)
when the window is resized. The other elements (label and
buttons) do not change their size. This behavior is dictated by
the -expand and -fill attributes of the pack command.
an frame where the center text entry box resizes nicely
---------
#!/usr/local/bin/wish
wm title . "Frame Example"
frame .f -borderwidth 10
pack .f -side top -fill x
button .f.quit -text Quit -command exit
button .f.go -text "Go!" -command { set tt "$tt:Click!" }
pack .f.quit .f.go -side right
label .f.l -text Enter: -padx 0
entry .f.text -width 20 -relief sunken -textvar tt
pack .f.l -side left
pack .f.text -side left -fill x -expand true
bind . x exit
,,,
Labelframes ‹↑›
These are just frames with a label, of course
a simple example with a labelled frame
---------
labelframe .s -text Ideas
radiobutton .s.small -text Small -var size -value small
radiobutton .s.med -text Medium -var size -value medium
radiobutton .s.big -text Big -var size -value big
s.big select
pack .s.small .s.med .s.big -anchor w -padx 5 -pady 5; pack .s
,,,
reposition the label and increase its font size
---------
labelframe .s -text Ideas -labelanchor wn -font {-size 16}
radiobutton .s.small -text Small -var size -value small
radiobutton .s.med -text Medium -var size -value medium
radiobutton .s.big -text Big -var size -value big
s.big select
pack .s.small .s.med .s.big -anchor w -padx 5 -pady 5; pack .s
,,,
use a bitmap as the frames label
---------
label .l -bitmap question
labelframe .s -text Ideas -labelwidget .l
radiobutton .s.small -text Small -var size -value small
radiobutton .s.med -text Medium -var size -value medium
radiobutton .s.big -text Big -var size -value big
s.big select
pack .s.small .s.med .s.big -anchor w -padx 5 -pady 5; pack .s
,,,
Notebooks Or Tabbed Frames ‹↑›
That which is called in java a 'tabbed frame', in tk is
called a notebook.
create a simple notebook
ttk::notebook .n
ttk::frame .n.f1; # first page, which would get widgets gridded into it
ttk::frame .n.f2; # second page
frame .n.f3; # second page
.n add .n.f1 -text "One"
.n add .n.f2 -text "Two"
.n add .n.f3 -text "Three"
pack .n
Labels ‹↑›
create a label with a sunken look to it
pack [label .l -text hello -relief sunken -padx 4]
Styled or themed labels, ie ttk have a -style option
which allows styles to be configured for multiple
widgets at once, instead of one by one.
create a styled label (since Tk version 8.5)
---------
ttk::label .label -text {Full name:}
pack .label
,,,
Displaying images in 'tile' labels is the same as for the
traditional tk label widget
Styling Labels ‹↑›
create 3 labels with different styles
--------
label .l1 -text "This is what the default label looks like"
label .l2 -text "This is a yellow label on a blue background" \
-foreground Yellow -background Blue
label .l3 -text "This is a label in Times 24 font" \
-font {-family times -size 24}
# Put them in the window in row order
grid .l1 -row 0
grid .l2 -row 1
grid .l3 -row 2
,,,
the dingbats
--------
font create df -size 18 -family {Dingbats}
set tt "A Label Font"
pack [label .l -font df -textvar tt]
bind . x exit
,,,
create a label that doesnt repack even when text changes
-------
proc FixedWidthLabel {name values } {
set maxW 0
foreach value $values {
if {[string length $value] > $maxW} {
set maxW [string length $value]
}
}
label $name -width $maxW -anchor w -text [lindex $values 0]
return $name
}
FixedWidthLabel .l {Ok Busy Error}
pack .l
.l config -text Ok
bind . c {
.l config -text Error
}
,,,
Images In Labels ‹↑›
display a random gif image in a label widget
set im [exec locate -i *.gif | shuf -n1]
image create photo img -file $im
pack [label .l -image img]
display a random unscaled jpeg image in a label widget
package require Img
set im [exec locate -i *.jpg | shuf -n1]
image create photo img -file $im
pack [label .l -image img]
display a 3x3 grid of random gifs
------
set ii [split [exec locate -i *.gif | shuf -n9] \n]
puts [join $ii \n]
option add *font {Times 16 italic}
set n 0
foreach {i j k} $ii {
set a [image create photo -file $i]
set b [image create photo -file $j]
set c [image create photo -file $k]
label .labeli$n -image $a
label .labelj$n -image $b
label .labelk$n -image $c
grid .labeli$n .labelj$n .labelk$n
incr n
}
bind . x exit
,,,
Multiline Labels ‹↑›
create a multiline label
-------
label .l -font {-size 15} -text "Multi \n Line"
pack .l
,,,
another way to create a multiline label
-------
label .l -font {-size 15} -text "
Earth
Wind
Sea"
pack .l
,,,
multiline white on black
-------
font create fo -family Courier -size 14
set message "
a - alpha
b - beta
c - kappa "
label .l -font fo -bg black -fg white -text $message
pack .l
,,,
Buttons ‹↑›
display a button with the text 'push me'
pack [button .b -text {push me}]
create a 'tile' modern button (since Tk version 8.5)
ttk::button .button -text "Okay" -command "submitForm"
Radio Buttons ‹↑›
group radio buttons with the -variable option
radiobutton .left.b1 -text "Left 1" -variable a -value one
radiobutton .left.b2 -text "Left 2" -variable a -value 2
radiobutton .left.b3 -text "Left 3" -variable a -value three
Entry Boxes ‹↑›
create a simple entry box
set default "something"
entry .foo -width 25 -textvariable default
pack .foo
,,,
the -textvar name can also be held in a variable
-------
set vv name
pack [entry .e -width 30 -textvar $vv]; focus .e
bind . <Return> { puts "name=$name" }
,,,
create a password entry box
set pass ""
entry .en -width 25 -show "*" -textvariable pass
pack .en; focus .en
bind . <Return> { puts "pass=$pass" }
,,,
Inserting Text ‹↑›
insert text at the beginning of the entry box
. config -borderwidth 20
entry .e -width 20
pack .e; focus .e
bind . / { .e insert 0 "Insert at position 0 " }
bind . <Return> exit
Deleting Text ‹↑›
delete all text in the entry box on / keypress
. config -borderwidth 20
entry .e -width 20
pack .e; focus .e
bind . / { .e delete 0 end }
bind . <Return> exit
Appearance Of Entry Widgets ‹↑›
show the different border styles for an entry
set s "styles"
entry .en -width 40 -textvar s
foreach style {flat groove raised ridge solid sunken} {
entry .e$style -width 20 -relief $style -highlightcolor blue
.e$style insert 0 "$style border style"
pack .e$style -pady 5 -padx 10
}
Selection Of Text In Entry Widgets ‹↑›
The selection is text which is highlighted blue
when the mouse is dragged over it, or by some
other mechanism.
select all text in the entry box on / keypress
. config -borderwidth 20
entry .e -width 20
pack .e; focus .e
bind . / { .e selection range 0 end }
bind . <Return> exit
check if anything selected in widget
. config -borderwidth 20
entry .e -width 20
pack .e; focus .e
bind . / {
if { [.e selection present] } {
.e insert end " Yes Sel"
} else {
.e insert end " No Sel"
}
}
bind . <Return> exit
if {.e select present }
Hinted Entry ‹↑›
Interfaces these days often use entry boxes which display
a hint to the user about what to enter into them when they
are empty. The following example attempts to imitate that.
This example could be developed to make the text italic,
gray etc. But it seems useful as is.
an entry box which shows a message when empty
---
option add *font "Courier 14"
set message ""
entry .e -textvariable message
entry .f -width 20
bind .e <Return> {
focus .f
}
bind .e <FocusIn> {
if { $message eq "" } {
#.e configure -font {-slant italic}
set message "First Name:"
.e selection range 0 end
.e icursor end
}
}
bind .e <FocusOut> {
if { $message eq "First Name:" } {
set message ""
}
}
bind .f <Return> exit
pack .e -fill x
pack .f -fill x
,,,
Insertion Points For Entry Widgets ‹↑›
set the insertion cursor to the end of the text
--------
set s "poems"
entry .e -textvar s
pack .e; focus .e
.e icursor end
,,,
Entry Events ‹↑›
The entry box doesnt seem to have a <<modified>> event (unlike)
the text widget. But maybe we can use the 'trace' command to
achieve the same thing
doing something after an entry widget is modified
--------
entry .e -width 40 -textvar t
pack .e; focus .e
bind .e <KeyRelease> {
global t
puts "Logging! $t"
}
,,,
The code below works put suffers from the same problem as
the <key> event. It fires before the new key is in the box...
validate an entry box using vcmd
--------
entry .e -width 40 -validate key -vcmd Log -textvar tt
pack .e; focus .e
proc Log {} {
global tt
puts "Logging! $tt"
return 1
}
,,,
Validation Of Text In Entry Widgets ‹↑›
tcl/tk has good mechanisms to validate text
entered into the widget as the user is typing
Layouts ‹↑›
Layouts are often called 'geometry managers' in window
'grid'.
Padding ‹↑›
Padding is often the word used to describe extra bits
of space which are placed around or inside widgets.
This is important for aesthetic reasons
put 20 pixels of space in the toplevel window
. configure -borderwidth 20
pack [entry .e -width 20] [entry .f -width 20]
bind . x exit
padx pady ipadx ipady
Dynamic Layouts ‹↑›
There seem to be 2 options here. Use grid to put windows,
frames, widgets etc on top of each other, then use
'raise' etc to make the frame appear. Or else just use
pack and forget with frames.
'pack' is a command and also a graphical layout manager. That is,
it determines the geometry of the widgets which are layed out on
an interface.
Pack to a side with -side top, -side left etc
Fill on an axis with -fill x or -fill y
use -expand to make the widget grow when the window grows.
See the frame section for a good pack example.
pack 2 frames
frame .one -width 100 -height 40
...
Use 'anchor' to justify to a side of the container
left justify an entry box above a text box
pack [entry .e] -anchor w -pady 5
pack [text .t]
pack a series of buttons towards the left side of window
foreach b {yellow orange green blue} {
button .$b -text $b -bg $b -font {-size 14}
pack .$b -side left
}
bind . x exit
pack some buttons from the bottom of the window
foreach b {yellow orange green blue white} {
button .$b -text $b -bg $b -font {-size 14}
pack .$b -side bottom
}
bind . x exit
Dynamic Packing And Unpacking ‹↑›
The example below dynamically hides and shows an element (frame)
of a graphical user interface. This is really usefull
a dynamic pack example with forget and pack before
--------
frame .top
label .top.l -text Text:
entry .top.e -width 40
pack .top.l .top.e -side left
pack .top -side top -fill x
text .t -width 80 -height 20
pack .t -side top
.t insert end [glob *]
bind . h { pack forget .top }
bind . H { pack .top -before .t -fill x}
bind . x exit
,,,
Below is a prototype for showing help in a dynamically displayed text widget.
This is suitable for a keystroke driven program. Interestingly, "pack forget"
does not throw an error if the widget named is already forgotten
show help in a text box dynamically
--------
wm attributes . -zoomed 1
font create df -size 14
option add *font df
frame .help
label .help.title -text Help
text .help.t
.help.t insert end "
h - show help
H - hide help
z - make the text font bigger
Z - make the text font smaller
x - exit the application"
pack .help.title -fill x
pack .help.t -fill both -expand 1
frame .main
label .main.l -text {Main Interface: Press h for help}
pack .main.l
pack .main
bind . h {
pack forget .main
pack .help -fill both -expand 1
}
bind . H {
pack forget .help
pack .main -fill both -expand 1
}
bind . z {
font conf df -size [expr [font conf df -size]+2] }
bind . Z {
font conf df -size [expr [font conf df -size]-2] }
bind . x exit
,,,
Gotchas For Pack ‹↑›
If you create a frame after a button, then the frame
will obscure the button
a frame over a button, the button is hidden
-------
set b [button .b -text click -command {exit}]
set f [frame .f]
pack .b -in .f
pack .f
,,,
the correct way to do it
-------
set f [frame .f]
set b [button .b -text click -command {exit}]
pack .b -in .f
pack .f
,,,
Grid Layout Manager ‹↑›
The grid layout manager is useful for creating 'table' like layouts
of widgets, where the widgets are arranged in rows and columns.
The -sticky attribute of 'grid' is equivalent to the
-fill and -expand attributes of the pack geometry
manager.
Each new grid command starts a new row.
layout labels and colours using grid
------
option add *font {Times 16 italic}
foreach color {red orange yellow green blue purple} {
label .l$color -text $color
frame .f$color -background $color -width 100 -height 2
grid .l$color .f$color
}
,,,
a trick to layout in 3 columns a big list
grid .label$ii -row [expr {$ii/3}] -column [expr {$ii%3}]
layout a big list in 3 columns and lots of rows
--------
set ff [lsort [font families]]
set ii 0
foreach fo $ff {
label .label$ii -font [list -family $fo -size 14] -text "$fo"
grid .label$ii -row [expr {$ii/3}] -column [expr {$ii%3}]
incr ii
}
bind . x exit
,,,
One use of grid is forms or data entry forms, the sort of stuff
done with the html <form> tag in <table> tags (back in the day).
It is a bit surprising that the code below works without an 'eval'.
When we assign -textvar $item, then a variable is created with its name
being the current value of $item. How curious, but convenient.
a data entry form using grid
------
option add *font {Times 16 italic}
foreach item {name age weight address} {
label .l$item -text $item
entry .e$item -width 30 -textvar $item
grid .l$item .e$item -pady 2 -padx 3
grid .l$item -sticky w
}
button .b -text submit -command {
puts "name=$name, age=$age, weight=$weight, address=$address"
}
grid .b -columnspan 2
,,,
Sticky ‹↑›
stickiness makes the widgets align themselves in their grid cells
or take up the empty space. This is a bit like -fill and -expand and
-anchor for the 'pack' layout manager.
using sticky with grid layouts
------
option add *font {Times 16 italic}
foreach color {red orange yellow green blue purple} {
label .l$color -text $color -bg white
frame .f$color -background $color -width 100 -height 2
grid .l$color .f$color
grid .l$color -sticky w
grid .f$color -sticky ns
}
,,,
a 2 by 2 layout in a grid
--------
font create ff -family Helvetica -size 30
option add *font ff
button .b1 -text "Hello"
button .b2 -text "Quit"
button .b3 -text "B3"
button .b4 -text "B4"
# Put in the grid
grid .b1 -row 0 -column 0
grid .b2 -row 0 -column 1
grid .b3 -row 1 -column 0
grid .b4 -row 1 -column 1
,,,
Exceptions And Errors ‹↑›
Use 'catch' to handle exceptional situations and
errors
the simplest use of 'catch' is just to ignore errors
------
catch {exec abcd}
puts "Error ignored"
,,,
catch an error and print the error message
set f abc.txt
if { [catch {open $f r} fid] } {
puts stderr "An error occurred! \n$fid"
exit 1
}
using catch to catch errors or do something if no error
set f abc.txt
if { [catch {open $f r} fid] } {
puts stderr "An error occurred! \n$fid"
exit 1
} else {
puts [read fid]
}
Events ‹↑›
view help for binding events to commands
man 3tk bind
make the application exit when the 'e' key is pressed
bind . <e> { exit }
bind . e { exit } # the same
bind . e exit # the same
Multiple keystrokes can also be bound to actions.
bind keystroke sequence 'qu' to an action
bind . qu { exit }
exit when "e" pressed
---
bind . <e> { exit }
pack [label .l -text hello -font {-size 50} -padx 150]
,,,
exit when "x" pressed
---
bind . <x> exit
pack [label .l -text hello -font {-size 50} -padx 150]
,,,
stop other event handlers being used with 'break'
bind . <e> { .l configure -text hi ; break }
The recipe below is important because it allows the coder to
take advantage of the behavior which is already by default
attached to some event. I think the '+' must be the first
character after the bracket.
add some behaviour to the existing with '+'
bind . <e> {+ .l configure -text hi ; break }
do something when a canvas is resized
bind .canvas <Configure> { puts "%W is now %w %h" }
tk events
<ButtonRelease-2> - when the middle mouse button is released |
<Button-1> - when the right mouse button is pressed |
a text box which displays keys pressed and symbolic names
text .t -font {-size 14}; pack .t; focus .t
bind .t <KeyPress> {.t insert end "You pressed the key named: %K \n"}
bind .t <ButtonPress> {.t insert end "You pressed button: %b at %x %y \n"}
make a double click destroy a listbox widget
listbox .lb -listvariable f
pack .lb
set f [list one 2 three]
bind .lb <Double-Button-1> {+ destroy %W; break }
Destroy Event ‹↑›
check if it is the top level window being destroyed
---------
pack [label .l -text test]
bind . <Destroy> {if {"%W" == "."} {puts "Bye" }}
,,,
Keysyms Or Keystroke Symbols ‹↑›
The keysyms or keystroke keywords are not alway obvious, even
with the help of the utilities above
interesting keysyms
----
bind .lb G { event generate .lb <Control-Home> }
bind .lb gg { event generate .lb <Control-End> }
,,,
Gotchas For Keysyms ‹↑›
If keys are inexplicable not generating the correct events,
look at what keyboard mapping (language) you are using.
Maybe you have a spanish keyboard mapping in operation...!!!
Even more deviously, the keyboard mapping may automatically
change when wish starts and then stops when is stops. A
very odd error.
Mouse Events ‹↑›
use abbreviation for mouse button 1 click
bind .lb <1> {+ focus %W}
Bindtags ‹↑›
Events can be bound to 'tags' instead of actual components.
This allows a level of indirection. There are a number
of tags already defined
bind tags default
. - the top level window |
Text - all text boxes |
Listbox - all listboxes |
all - all components | used for tabbing |
The trick below is useful to exclude a text box from
other default bindings (such as keyboard accelerators),
since keystrokes are used to edit the text box
show what bindtags already exist for a widget
-----
pack [text .t]
.t insert end "Bindtags for text box:\n [bindtags .t]"
,,,
The code below is useful to create behaviors for when
the text box does not have focus. Normally, a text box
generates events which are "passed on" to the top level
window ".". So if we have a binding such as
bind . x exit
And we press "x" while typing in a Text widget, the
application will exit. This is almost never what is
wanted. So we can use the following trick to avoid this
bindtags .t [list all Text .t]
remove . binding for a text box and reorder bindtags
-----
pack [text .t]
bindtags .t [list all Text .t]
.t insert end "
Press <Escape> to put focus to main window
Bindtags for text box:
[bindtags .t]"
focus .t
bind . i {
.t insert end "\ni pressed with . focus \n"
}
bind .t <Escape> {
.t insert end "\nfocus to . \n"
focus .
}
bind . x exit
,,,
create a new bindtag and use it
-----
pack [text .t]
bindtags .t [list VimCmd all Text .t]
.t insert end "Bindtags for text box:\n [bindtags .t]"
bind VimCmd .. { %W insert end "VimCmd Bind Tag!" }
#or bind VimCmd .. { .t insert end "VimCmd Bind Tag!" }
,,,
The code below is an interesting way to create a 2 mode text
editor (similar to vi's insert and command modes). It comes
from an example by Brent Welch, who has a great deal of time to
cogitate on these matters. When the <Escape> key is pressed, the
user cannot edit the text widget because all its default editing
bindings are removed (by removing the the 'Text' binding tag).
This is a simple way to 'disable' the text editor. But the
insert cursor still blinks, which is a bit confusing for
the user
Also, there are many subtleties... the 1st bindtags is used
with list because there is a variable $t involved.
simple dual mode editor with bindtags
-----
set t [text .t]
pack $t
bindtags $t [list ViInsert Text $t all]
bind ViInsert <Escape> {bindtags %W {ViCmd %W all}}
bind ViCmd <Key-i> {bindtags %W {ViInsert Text %W all}}
,,,
create a 'vim' style 2 mode editor using bind tags.
-----
pack [text .t -font {-size 14}]
bindtags .t {ViEdit all Text .t}
.t insert end "Press escape for command mode"
focus .t
bind ViCmd <i> {
bindtags %W {ViInsert Text %W all}
wm title . "TkVi: Insert Mode"
}
bind ViCmd x exit
bind ViInsert <Escape> {
bindtags %W {ViCmd %W all}
wm title . "TkVi: Command Mode"
}
,,,
an expanded dual mode keystroke text editor
-----
pack [text .t -font {-size 14}]
bindtags .t [list VimEdit all Text .t]
.t insert end "Press escape for command mode"
focus .t
bind ViCmd <i> {
bindtags %W {ViInsert Text %W all}
wm title . "TkVi: Insert Mode"
}
bind ViCmd x exit
bind ViCmd f {
# make fullscreen
}
bind ViCmd z {
# zoom text
}
bind ViCmd Z {
# anti zoom text
}
bind ViCmd c {
# toggle green on black/ black on white
}
bind ViCmd w {
# save file
}
bind ViCmd r {
# search and replace with confirmation
}
bind ViCmd R {
# search and replace with no confirmation
}
bind ViCmd / {
# search and jump to
}
bind ViInsert <Escape> {
bindtags %W {ViCmd %W all}
wm title . "TkVi: Command Mode"
}
,,,
remove the (.) top level window bindings from a text box
pack [text .t]
bindtags .t {Text all}
make a text box uneditable by removing its default bindings
pack [text .t]
bindtags .t {. all}
frames turn red on mouse enter, destroy frame on control c
--------
frame .a -width 100 -height 100
frame .b -width 100 -height 100
bind Frame <Enter> {%W config -bg red}
bind Frame <Leave> {%W config -bg white}
bind .b <Button> {puts "Button %b at %x %y"}
pack .a .b -side left
bind all <Control-c> {destroy %W}
bind all <Enter> {focus %W}
,,,
Generating Events ‹↑›
Generating events is so much easier than programming
all the behavior of a key press in some cases
make the 'j' key act like a down-arrow
-------
set ll [exec ls]
listbox .lb -listvar ll -width 60 -font {-size 14}
pack .lb; focus .lb
bind .lb <j> { event generate .lb <Down> }
,,,
Focus Events ‹↑›
get some help for the focus command
man 3tk focus
focus events which can be bound to actions
FocusIn - when the widget gets focus |
FocusOut - loses focus |
Focus - loses or get focus |
set the focus to an entry widget
---
option add *font "Courier 14"
set var "click"
entry .e -textvariable var
button .b -text click
pack .e .b
focus .e
,,,
set text when the user clicks in a text entry box
---
option add *font "Courier 14 italic"
set var "click"
entry .e -textvariable var
entry .f
bind .e <FocusIn> { set var "Focus In!!" }
pack .e .f
,,,
set text when an entry box loses focus
---
option add *font "Courier 14 italic"
set var "click"
entry .e -textvariable var
entry .f
bind .e <FocusOut> { set var "Focus Out!!" }
pack .e .f
bind . <Return> exit
,,,
Keystroke Events ‹↑›
bind commands must come after the widget they refer to,
not surprisingly
The key codes shown below can be used as <Code> etc
in bind commands.
an abbreviated way of specify 'e' keypress
bind . e { exit }
show all key press codes usable with bind
-----
label .l -text "Press any key to see its code"
entry .e -textvar tt -font {-size 30}
pack .l
pack .e -padx 30 -pady 30
bind . <Key> { set tt %K }
bind . <e> { exit }
,,,
bind all key strokes in an entry box and print them
-------
entry .e -width 25 -font {-size 20}
pack .e
bind . <Key> {puts %K}
,,,
Virtual Events ‹↑›
Virtual events are denoted by double angle braces
eg <<Cancel>> <<ListboxSelect>> <<Cut>> <<Copy>> <<Paste>>
They provide a mechanism to smooth differences between
operating systems, so that the Tcl/tk application behaves
as the user expects for that particular system.
do something when a listbox selection changes
-----
set ll [split [exec ls] \n]
pack [listbox .lbox -listvar ll]
bind .lbox <<ListboxSelect>> {ListboxChanged %W}
proc ListboxChanged w {
puts "Changed"
}
,,,
The technique below can be used to activate the same
event through different keystrokes.
create a new virtual event and use it
-------
pack [label .l -text {Press 'x' or 'q' to exit}]
event add <<Exit>> <Key-x> <Key-q>
bind . <<Exit>> { exit }
,,,
Gotchas For Bind ‹↑›
bind <<Exit>> { do something }
doesnt throw an error even though we have left out
a window name
Because the % is used by the event system to indicate special values
such as %W (the widget) %P (the contents of the entry box) etc etc.
Then within bind commands you need to escape these with %% - if for example
you are using modulus arithmetic
you have to escape % signs !!!
-----
label .l -text "Percent Problem"
bind . e {
puts [expr {10%13}] # not what you want!
puts [expr {10%%13}] # yes, is modulus arithmetic!
}
,,,
set the font for a label
------
label .l -font {Times 30 bold} -text Hello
pack .l
,,,
times italic, very elegant.
------
label .l -font {Times 30 italic} -text Hello
pack .l
,,,
see what font a widget is really using (not what you want it to use)
--------
pack [label .l -font {-family Times} -text Hello]
puts [.l config -font]
,,,
Italic ‹↑›
make an existing font italic
font configure ff -family "another face" -size 24 -slant italic
Font Sizes ‹↑›
set the font in screen points ???
-font {-size -10}
create a font which can be resized with 'z' and 'Z'
------
font create ff -size 14 -family {Courier New}
label .l -text "z bigger, or Z smaller" -font ff
pack .l
bind . z { font conf ff -size [expr [font conf ff -size]+2] }
bind . Z { font conf ff -size [expr [font conf ff -size]-2] }
,,,
create a label with big text
pack [label .l -text hello -font {-size 50} -padx 4]
create a label with big text
---
pack [label .l -text hello -font {-size 50} -padx 150]
,,,
display big text in a text box
pack [text .t -font {-size 20}] -fill both -expand 1
.t insert 1.0 [exec cat /usr/share/dict/words]
Configuring Fonts ‹↑›
make font bigger when a button is pressed
button .hello -text "Hello, World!" \
-font {-size 14 } -command {
.hello configure -font {-size 18}
}
pack .hello
Font Families ‹↑›
a listbox with all font families
----
set ff [lsort -dict [font families]];
listbox .lb -listvar ff -width 20 -font {-size 20}
pack .lb
,,,
the same but with a scroll bar
----
set ff [lsort -dictionary [font fam]];
listbox .lb \
-listvar ff -width 20 -font {-size 20} \
-yscrollcommand [list .sb set] \
-selectmode single;
scrollbar .sb -command [list .lb yview];
pack .lb -side left -fill both -expand true
pack .sb -side right -fill y
bind . x exit
,,,
The code below demonstrates the 'list' trick for supplying list
values to options.
labels displayed in all available fonts
--------
set ff [lsort [font families]]
set ii 0
foreach fo $ff {
label .label$ii -font [list -family $fo -size 14] -text "$fo"
grid .label$ii -row [expr {$ii/3}] -column [expr {$ii%3}]
incr ii
}
bind . x exit
,,,
a text box with a list of all font families
--------
font create df -size 14 -family {Courier New}
set ff [join [lsort [font families]] "\n"];
text .t -font df
pack .t -expand true -fill both -padx 20 -pady 20
.t insert end $ff
bind . x exit
,,,
the dingbats
--------
font create df -size 14 -family {Dingbats}
label .l -font df -text Hello
pack .l
puts [.l config -font]
,,,
Default Fonts ‹↑›
http://web.archive.org/web/20090302082049/http://www.tclscripting.com/articles/jun06/article1.html
a good article about default fonts
create a named font to use as a default
--------
font create myDefaultFont -family Helvetica -size 20
option add *font myDefaultFont
,,,
When the named font is updated, so it the widget which is using it.
This allows very easy 'zooming' of text in an application.
The technique below has a problem when the font name has a space
in it, in which case use the recipe above.
a default font for all widgets
--------
option add *font "Times 30 italic"
pack [label .l -text {Hello}]
,,,
create a named font and use it with a text box
---------
font create df -size 14 -family {Courier New}
pack [text .t -font df] -padx 10 -pady 10
.t insert end "Courier Font 14"
,,,
a named font with just the size
---------
font create df -size 14
pack [text .t -font df] -padx 10 -pady 10
.t insert end "Some Font size 14"
,,,
'font configure' can be written 'font config' or
even 'font conf'
increase/decrease label size when z/Z pressed
----
font create df -family Helvetica -size 16
label .l -text "A Named Font" -font df
pack .l
bind all <z> {
font config df -size [expr [font config df -size]+2]
}
bind all <Z> {
font config df -size [expr [font config df -size]-2]
}
bind . x exit
,,,
the same as above but much more verbose
----
font create defaultFont -family Helvetica -size 12 -slant italic
label .label -text "A label with named font" -font defaultFont
pack .label
bind all <z> {
set size [font configure defaultFont -size]
incr size 2
if {$size >= 4 && $size <= 100} {
font configure defaultFont -size $size
}
}
bind all <Z> {
set size [font configure defaultFont -size]
incr size -2
if {$size >= 4 && $size <= 100} {
font configure defaultFont -size $size
}
}
bind all <x> { exit }
,,,
display a jpeg image in a label widget
package require Img
image create photo img -file "/usr/share/backgrounds/Climbing.jpg"
pack [label .l -image img]
create a modern themed label (since Tk version8.5)
ttk::label .label -text {Full name:}
Displaying images in 'tile' labels is the same as for the traditional
tk label widget
Tree Widgets ‹↑›
The new set of widget available in tk version 8.5 have a widget
for displaying 'tree' type data. If wish displays a message such as
'unknown command ttk::treeview' then you need to install wish 8.5 or
activate it with 'update-alternatives' (see above)
a basic example of using the ttk::treeiew
#package require tile
ttk::treeview .tree
pack .tree -side top
.tree insert {} end -id "Item 1" -text "Item 1"
.tree insert {} end -id "Item 2" -text "Item 2"
.tree insert {} end -id "Item 3" -text "Item 3"
.tree insert "Item 1" end -id "Item 1-1" -text "Item 1-1"
.tree insert "Item 1" end -id "Item 1-2" -text "Item 1-2"
.tree insert "Item 1" end -id "Item 1-3" -text "Item 1-3"
.tree insert "Item 2" end -id "Item 2-1" -text "Item 2-1"
.tree insert "Item 2" end -id "Item 2-2" -text "Item 2-2"
.tree insert "Item 2" end -id "Item 2-3" -text "Item 2-3"
Scroll Bars ‹↑›
create a listbox with scroll bar
--------
scrollbar .s -command ".l yview"
listbox .l -yscroll ".s set" -font {-size 20}
label .label -text "Nothing Selected" -font {-size 14}
bind .l <Double-B1-ButtonRelease> {
set color [.l get active]
.label configure -text $color
}
grid .l -row 0 -column 0 -sticky news
grid .s -row 0 -column 1 -sticky news
grid .label -row 1 -column 0 -columnspan 2
.l insert 0 red green blue yellow white a b c d e \
f g h i j k l m n
,,,
Listboxes ‹↑›
get some quite abstract help for the listbox widget
man 3tk listbox
add 'aa' at the end of a listbox .lb
.lb insert end aa
add 3 elements to a listbox using eval
listbox .lb
eval {.lb insert end} [list a b c]
pack .lb
,,,
append all elements of a list to a listbox
------
listbox .lb
set list [glob *]
foreach item $list {
.lb insert end $item
}
pack .lb
,,,
another way to append all elements of a list to a listbox
------
listbox .lb
set list [glob *]
eval {.lb insert end} $list
pack .lb
,,,
create a listbox synchronized with a list variable 'l'
set l [list one 2 three]
pack [listbox .lb -listvariable l]
,,,
create listbox with a list of file names in the current folder
set l [glob *]
pack [listbox .lb -listvariable l]
,,,
create a listbox with elements each 1 line of stdin
set i [split [read stdin] \n]
pack [listbox .lb -listvariable i]
,,,
The example below adjusts the width of the listbox to
the widest element.
create a listbox with elements each 1 line of stdin
---------
set ii [split [read stdin] \n]
set max 1
foreach s $ii {
if {[string length $s] > $max} {
set max [string length $s]
}
}
pack [listbox .lb -listvariable ii -width $max]
,,,
a listbox with sorted folder names from the 'etc' folder
set l [lsort [glob -type d -tails -directory /etc/ *]]
pack [listbox .lb -height 30 -listvariable l]
,,,
a listbox with sorted folder names from the 'etc' folder
set l [lsort [glob -type d -tails -directory /etc/ *]]
pack [listbox .lb -height 30 -listvariable l]
,,,
a listbox with mp3 files
set l [split [exec locate "*.mp3"] \n]
pack [listbox .l -width 80 -listvariable l -font {-size 16}]
,,,
a listbox with mp3 files which deletes an item on right click
listbox .l -width 80
eval {.l insert end} [split [exec locate "*.mp3"] \n]
bind .l <ButtonRelease-3> {.l delete [.l curselection]}
pack .l
,,,
a listbox with mp3 files, expands when the window is resized
set l [split [exec locate "*.mp3"] \n]
listbox .lb -width 80 -listvariable l
grid .lb -sticky news
# not working
,,,
Deleting Elements Of A Listbox ‹↑›
delete the first element of a listbox
$listbox delete 0
delete a range of elements
$listbox delete 3 5
delete all items in a listbox
.lb delete 0 end
Active Element Of Listbox ‹↑›
In the example below, the selected element is not
affected by the 'activate' command
activate the 2nd element when 'f' pressed
---------
set ll [split [exec apt-cache search java] \n]
listbox .lb -listvar ll -width 60
pack .lb; focus .lb
bind .lb <f> { .lb activate 1 }
,,,
Current Selection ‹↑›
display the text of the currently selected item
puts [.lb get [.lb curselection]]
show the currently selected item text in a label
---------
set l [split [exec apt-cache search java] \n]
listbox .lb -listvar l -width 60 -font {-size 14}
label .l -text "?" -font {-size 14}
pack .lb .l; focus .lb
bind .lb <<ListboxSelect>> {
.l configure -text [.lb get [.lb curselection]]
}
,,,
Selection Of Listbox ‹↑›
deselect all elements between 0 and 5 when 'c' pressed
---------
set l [split [exec apt-cache search java] \n]
listbox .lb -listvar l -width 60 -font {-size 14}
pack .lb; focus .lb
bind .lb <c> { .lb selection clear 0 5 }
,,,
deselect all elements when 'c' pressed
---------
set l [split [exec apt-cache search java] \n]
listbox .lb -listvar l -width 60 -font {-size 14}
pack .lb; focus .lb
bind .lb <c> { .lb selection clear 0 [.lb size] }
,,,
select first 6 elements when c pressed
---------
set l [split [exec apt-cache search java] \n]
listbox .lb -listvar l -width 60 -font {-size 14}
pack .lb; focus .lb
bind .lb <c> { .lb selection set 0 5 }
,,,
The example below uses the LisboxSelect event which fires
whenever the selection changes.
show the current selection in a label
---------
set l [split [exec apt-cache search java] \n]
listbox .lb -listvar l -width 60 -font {-size 14}
label .l -text "?" -font {-size 14}
pack .lb .l; focus .lb
bind .lb <<ListboxSelect>> {
.l configure -text [.lb curselection]
}
,,,
The point of the example below is just to move the selection
without having to remove the fingers from the keyboard. But
I am sure their is an easier way... the bind commands should
just substitute the arrows key for the j/k keys, since
the arrow keys already have the correct bindings.
use 'vi' keys j/k to move the selection in the listbox
---------
# by default list is split on white space
set ll [exec ls]
listbox .lb -listvar ll -width 60 -font {-size 14}
pack .lb; focus .lb
.lb selection set 0
bind .lb <j> {
set i [.lb curselection]
if {$i < [expr [.lb size] - 1]} {
.lb selection clear $i
incr i 1
.lb selection set $i
.lb activate $i
}
}
bind .lb <k> {
set i [.lb curselection]
if {$i > 0} {
.lb selection clear $i
incr i -1
.lb selection set $i
.lb activate $i
}
}
bind .lb <e> { exit }
,,,
a much much easier way to do the above
---------
set ll [exec ls]
listbox .lb -listvar ll -width 60 -font {-size 14}
pack .lb; focus .lb
bind .lb <j> { event generate .lb <Down> }
bind .lb <k> { event generate .lb <Up> }
bind .lb <x> { exit }
,,,
Searchable Listbox ‹↑›
This version is working much more quickly than using the listvariable.
This is useful. It could be improved by showing and hiding the
search box dynamically (pack forget $w), changing the colour of the
search box when no matches. When I tried to write this code with a
-listvar it was too slow. Even with a 90000 element list, this code
is reasonably fast
a searchable listbox, not using a listvariable
font create df -family Helvetica -size 14
option add *font df
wm attributes . -zoomed 1
set ll [split [exec apt-cache search .*] \n]
#set ll [split [exec cat /usr/share/dict/words] \n]
listbox .lb -width 60
entry .e -textvariable tt
label .l -textvar size
pack .l -side top -fill x
pack .lb -fill both -expand true
eval {.lb insert end} $ll
set size [.lb size]
focus .lb
bind .lb / {
pack .e -before .l -fill x; focus .e
}
bind .lb j { event generate .lb <Down> }
bind .lb k { event generate .lb <Up> }
bind .lb x exit
bind .e <Return> {
pack forget .e; focus .lb
.lb selection set 0; .lb activate 0
}
bind .e <KeyRelease> {
.lb delete 0 end
if { [string length $tt] == 0 } {
eval {.lb insert end} $ll
set size [.lb size]
break
}
foreach item $ll {
if {[string match -nocase *$tt* $item]} {
.lb insert end $item
}
}
set size [.lb size]
}
,,,
display all debian linux packages in a list box
set pp [split [exec apt-cache search e] \n]
pack [listbox .t -height 20 -width 80 -listvariable pp]
,,,
Below is a useful email address selection panel, where the
addresses are extracted from .muttrc alias lines There is a list
index bug when only one email address is displayed. Also, put
every thing in a frame, for easy hiding/showing. Make the search
box appear and disappear
search email addresses in a listbox
font create df -family {courier new} -size 14
option add *font df
wm attributes . -zoomed 1
set ll [lsort -dictionary [split \
[exec sed -n "/^alias /s/alias *//p" $env(HOME)/.muttrc] \n]]
listbox .lb -width 60
entry .e -textvar tt; label .l -textvar size
pack .e .l -fill x -padx 10 -pady 10
pack .lb -fill both -expand true -padx 10
eval {.lb insert end} $ll
.lb selection set 0; .lb activate 0
set size [.lb size]
focus .lb
bind .lb / { focus .e }
bind .lb <Return> {
set address [.lb get [.lb curselection]]
regsub -nocase {^.*?([-._a-z0-9]+@[-._a-z0-9]+).*$} $address {\1} ss
puts $ss
}
# make some 'vi' or 'less' like keybindings for the listbox
bind .lb gg { event generate .lb <Control-Home> }
bind .lb G { event generate .lb <Control-End> }
bind .lb j { event generate .lb <Down> }
bind .lb J {
event generate .lb <Next>
.lb selection clear 0 end; .lb selection set active
}
bind .lb k { event generate .lb <Up> }
bind .lb K {
event generate .lb <Prior>
.lb selection clear 0 end; .lb selection set active
}
bind .lb x exit
bind .e <Return> {
focus .lb
.lb selection set 0; .lb activate 0
}
bind .e <KeyRelease> {
.lb delete 0 end
#if { [string length $tt] == 0 } {
# eval {.lb insert end} $ll
# set size [.lb size]
# break
#}
foreach item $ll {
if {[string match -nocase *$tt* $item]} {
.lb insert end $item
}
}
set size [.lb size]
}
bind .lb z { font conf df -size [expr [font conf df -size]+2] }
bind .lb Z { font conf df -size [expr [font conf df -size]-2] }
,,,
Scrolled List Box ‹↑›
Even though scroll bars are fastidious, it may be useful to
use them as a visual hint to the user that there is more stuff
than meets the eye.
The following example is from Brent Welch and demonstrates
important tk widget procedure techniques including the use
of "args" and "eval" to do useful things
a scrolled listbox, not working ....
proc ScrollList {f args} {
frame $f
listbox $f.list \
-xscrollcommand [list $f.xscroll set] \
-yscrollcommand [list $f.yscroll set]
eval {$f.list configure} $args
scrollbar $f.xscroll -orient horizontal \
-command [list $f.list xview]
scrollbar $f.yscroll -orient vertical \
-command [list $f.list yview]
grid $f.list -sticky news
grid rowconfigure $f 0 -weight 1
grid columnconfigure $f 0 -weight 1
return $f.list
}
set l [ScrollList .f -listvar fonts]
pack .f -expand yes -fill both
set fonts [lsort -dictionary [font families]]
bind . x exit
Combobox ‹↑›
The original tk did not have the 'combobox' widget (made famous
by the html <select> tag), but since tk v8.5 a combobox is available
in the 'ttk' namespace (which are "tile" widgets).
A ttk::combobox combines a text field with a pop-down list of values; the user may select the value of the text field from among the values in the list
display a combobox with three drop down options (tk version> 8.5)
pack [ttk::combobox .c -textvariable state -values {nsw tas qld}]
display a combobox with file names of the current folder
ttk::combobox .c -textvariable state -values [glob *]
pack .c
,,,
display a message when the combo changes, not working?
ttk::combobox .c -textvariable state -values [glob *]
bind . <ComboboxSelected> {
tk_messageBox -message "List box was changed" }
pack .c
,,,
Text Boxes ‹↑›
A 'text box' in tcl/tk is a box which can contain editable
and formattable text. It has many capabilities but in its basic
form is simple to use
display the unix dictionary file in a text box
pack [text .t -height 20 -width 80]
.t insert 1.0 [exec cat /usr/share/dict/words]
get all text from a text widget
$t get 1.0 end
get 1st and 3rd lines
$t get 1.0 1.end 3.0 3.end
display the unix dictionary file in a resizable text box
pack [text .t] -fill both -expand 1
.t insert 1.0 [exec cat /usr/share/dict/words]
get the contents of thetext box
----
pack [text .text] -fill both -expand 1
pack [label .l -text "hello"]
bind . w { set content [.text get 1.0 end]; puts $content }
,,,
pack a resizable text box
pack [text .text] -fill both -expand 1
grid a resizable text box
grid [text .text] -sticky news
grid rowconfigure 0 -weight 1
grid columnconfigure 0 -weight 1
,,,
pack 3 widgets including a text
-------
entry .to -textvar recipient -width 30
text .body
label .status -text "hello"
pack .to -fill y
pack .body -fill both -expand true
pack .status
,,,
keep the last line of a text box in view
.t see end
another way
.text yview moveto 1
a listbox with text files which opens the selected file on right click
listbox .l -width 70
text .t -width 80
eval {.l insert end} [split [exec locate "*.txt"] \n]
bind .l <ButtonRelease-3> {
set f [open [.l get [.l curselection]] r]
.t delete 1.0 end
.t insert 1.0 [read $f]
close $f
}
grid .t
grid .l
,,,
a dialog to choose '.txt' files and open the chosen in a text box
set typelist {{"Text files" ".txt"} {"Latex files" ".tex"}}
button .btn -text "Choose a file" -command {
set f [open [tk_getOpenFile -filetypes $typelist] r]
.t delete 1.0 end
.t insert 1.0 [read $f]
close $f
}
text .t -width 70 -height 30
pack .btn .t
The example below is an example of a text box with a bound
scroll bar within a frame
Scrolling Or Moving Text Boxes ‹↑›
We can use the text yview command to create a simple
text viewer with vi like keys
scroll down a page when L is pressed
----
pack [text .text] -fill both -expand 1
set in [open "$env(HOME)/sf/htdocs/books/tcl/tcl-book.txt"]
.text insert end [read $in]
pack [label .l -text "Press L to scroll down"]
bind . L { .text yview scroll 1 pages }
bind . x exit
,,,
viewing a text file with vi keys
----
font create df -family {Courier New} -size 14
option add *font df
wm attributes . -zoomed 1
pack [text .text] -fill both -expand 1
set in [open "$env(HOME)/sf/htdocs/books/tcl/tcl-book.txt"]
.text insert end [read $in]
#puts "[df cget -size]"
pack [label .l -text "x:exit,j:down,k:up,K:pagedown,L:pageup,
g:top,G:bottom,z/Z:+/-Zoom,o:Open file"]
bind . o {
set f [tk_getOpenFile -filetypes {{"Text" ".txt"}}]
if {$f ne ""} {
.text delete 1.0 end
set in [open $f]
.text insert end [read $in]
}
}
bind . j { .text yview scroll 1 units }
bind . k { .text yview scroll -1 units }
bind . K { .text yview scroll 1 pages }
bind . L { .text yview scroll -1 pages }
bind . g { .text see 1.0 }
bind . G { .text see end }
bind . z { font conf df -size [expr [font conf df -size]+2] }
bind . Z { font conf df -size [expr [font conf df -size]-2] }
bind . x exit
,,,
Scrollbars And Text Widgets ‹↑›
Some people believe that scrollbars are a useful artifact.
Let them have their opinions.
create a text box with scroll bar within a frame
-------
frame .t
text .t.log -width 80 -height 20 -borderwidth 2 -relief raised \
-setgrid true -yscrollcommand {.t.scroll set}
scrollbar .t.scroll -command {.t.log yview}
pack .t.scroll -side right -fill y
pack .t.log -side left -fill both -expand true
pack .t -fill both -expand true
.t.log insert end [exec cat /usr/share/dict/words]
bind . x { exit }
,,,
Since the text widget, by default wraps lines which are too
long to display, horizontal scrolling may not be that important.
a procedure to create a vertically scrolled text component
proc ScrollText { f args } {
frame $f
eval {text $f.text \
-yscrollcommand [list $f.yscroll set]} $args
scrollbar $f.yscroll -orient vertical \
-command [list $f.text yview]
grid $f.text $f.yscroll -sticky news
grid columnconfigure $f 0 -weight 1
return $f.text
}
set t [ScrollText .f -width 60 -height 20 -font {courier 12}]
pack .f -side top -fill both -expand true
# This file join command hangs
# set in [open [file join /home/arjuna/sf/htdocs/books tcl "tcl-book.txt"]
set in [open "/home/arjuna/sf/htdocs/books/tcl/tcl-book.txt"]
$t insert end [read $in]
close $in
bind . x exit
Dumping Text Contents ‹↑›
dumping the contents can return all the info in a text
widget including marks and tags. This is useful for
transforming to another format such as Html, Pdf etc.
$t dump ...
Searching Text ‹↑›
If ".t search" doesnt find anything, then it returns
an empty string
search forward for the text 'tt'
$t search -forward "tt"
$t search "tt" # the same
search backward from the 4th line for the text 'tt'
$t search -backward "tt" 4.0
search 3rd and 4th lines only for "Q:" ignoring case
$t search -nocase "Q:" 3.0 5.0
search for a pattern and highlight it and jump to it
set t [text .t]; pack $t
$t insert end "This is the Q: what to do?"
bind . / {
set pattern {\s+Q:}
set start [$t search -count c -regexp -- $pattern 1.0 end]
$t see $start
$t tag add sel $start "$start +$c chars"
puts $start
}
bind . x exit
The code below shows just how succintly Tcl/Tk can
achieve complex and useful results. In 17 Lines of
code a searchable text box...
a simple regexp search box which highlights the match
option add *font "{Courier New} 14"
set pattern {}
pack [entry .e -textvar pattern] -anchor w -pady 5 -padx 5
set t [text .t]; pack $t -padx 5
focus .e
set in [open "$env(HOME)/sf/htdocs/books/tcl/tcl-book.txt"]
$t insert end [read $in]
close $in
bind .e <Return> {
set start [$t search -count c -regexp -- $pattern 1.0 end]
$t see $start
$t tag add sel $start "$start +$c chars"
puts "Start: $start, Count: $c"
}
bind . x exit
The code above also gives us a template for "marking up" documents
using tags. We search through the document, get a list of
indice pairs (start end) and then add a tag to all those
ranges, and configure the tag how ever we want.
The code below uses some tricky "index arithmetic" to
loop through all matches in the document, and add the tag to
those ranges. The logic could be simplified
But all this could be done much more succinctly with the
-all switch to "text search". I should have read the man page
mark up all parts of a document matching a pattern
option add *font "{Courier New} 14"
set pattern {}
pack [entry .e -textvar pattern] -anchor w -pady 5 -padx 5
set t [text .t]; pack $t -padx 5
focus .e
set in [open "$env(HOME)/sf/htdocs/books/tcl/tcl-book.txt"]
$t insert end [read $in]
close $in
$t tag configure tt -font "Times 14 italic"
bind .e <Return> {
set t1 [clock milliseconds]
if { $pattern eq "" } { break; }
set count 0
.t tag remove tt 1.0 end
set start [$t search -count c -regexp -- $pattern 1.0 end]
while {$start ne ""} {
incr count
$t see $start
$t tag add tt $start "$start +$c chars"
#puts "Start: $start, Chars: $c: Count: $count"
set start [$t search -count c -regexp -- $pattern "$start +$c chars" end]
}
set t2 [clock milliseconds]
puts "
Marked up $count matches of pattern $pattern
in [expr {$t2-$t1}] ms"
}
bind . x exit
We need to use index arithmetic with a mark
$t mark set start "start +2c"
Markup Markdown And The Text Widget ‹↑›
We can use the techniques above to automatically
format text which is in a particular format, so that
each of the significant parts of the text is
formatted distinctively.
a function to format a pattern in a document with a tag
option add *font "{Courier New} 14"
. config -borderwidth 20
proc Markup { tb format pattern } {
set count 0
$tb tag remove $format 1.0 end
set start [$tb search -count c -regexp -- $pattern 1.0 end]
while {$start ne ""} {
incr count
$tb tag add $format $start "$start +$c chars"
set start [$tb search -count c -regexp -- $pattern "$start +$c chars" end]
}
$tb insert end "Marked Up"
}
set t [text .t -height 20]; pack $t
set aa [exec sed -n "/^alias /s/alias *//p" $env(HOME)/.muttrc]
$t insert end $aa
$t tag config mailAddress -font "Times 14 italic" -relief raised
Markup $t mailAddress {[^ <@]+@[^> \n]+}
bind . x exit
# text
Attempting to use "text search -all"
display muttrc aliases with parts formatted
option add *font "{Courier New} 14"
. config -borderwidth 20
# a dodgy email address pattern
set pattern {[^ <@]+@[^> \n]+}
set t [text .t -height 20]; pack $t
set aa [exec sed -n "/^alias /s/alias *//p" $env(HOME)/.muttrc]
#puts [join [lsort [split $aa \n]] \n]
$t insert end "press return to mark up\n"
$t insert end $aa
$t tag config alias -font "Times 14 italic" -relief sunken
bind . <Return> {
set count 0
.t tag remove alias 1.0 end
set start [$t search -count c -regexp -- $pattern 1.0 end]
while {$start ne ""} {
incr count
$t tag add alias $start "$start +$c chars"
puts "Start: $start, Chars: $c: Count: $count"
set start [$t search -count c -regexp -- $pattern "$start +$c chars" end]
}
.t insert end "Marked Up"
}
bind . x exit
Deleting Text ‹↑›
delete the 1st and 4th lines of text
$t delete 1.0 2.0 4.0 5.0
delete the word containing the insert cursor
--------
set t [text .t]
button .b -text "Delete Word" -command {
$t delete "insert wordstart" "insert wordend"
}
pack $t .b
,,,
delete the line containing the insert cursor
--------
set t [text .t]
button .b -text "Delete Word" -command {
$t delete "insert linestart" "insert lineend +1 char"
}
pack $t .b
,,,
Indices And Arithmetic ‹↑›
indice arithmetic
+2 chars |
-3 chars |
+4 lines |
linestart |
lineend |
wordstart |
wordend |
indices
line.char - eg 1.0
@x,y - character under screen position
current - char under mouse
end - after last char in document
image - position of image
insert - just after insert cursor
mark - just after mark
tag.first - first char in range tag
tag.last
window - position of embedded window
using indices to get current line of text
text .t; label .l -textvar s; pack .l .t
.t insert end "Wind Sun \n Sea Earth"
focus .t
bind .t <Return> {
set s "Current Line: \
[.t get "insert linestart" "insert lineend"]"
break
}
bind . x exit
Cut And Paste In The Text Widget ‹↑›
There is an easier way to do this...
format text as it is getting pasted
---------
package require Tk
proc newTextPaste w {
$w delete 1.0 end
set txt [::tk::GetSelection $w CLIPBOARD]
regsub -all {,} $txt "\n ," txt
regsub -nocase -all {\sand\s} $txt "\n and " txt
$w insert 1.0 $txt
}
text .t -width 80 -height 40
pack .t
bind . <<Paste>> {newTextPaste %W; break}
,,,
Text Boxes And Colours ‹↑›
-bg -background, the background colour
-fg -foreground, foreground colour
display green on black text with white cursor
font create ff -family Helvetica -size 14
text .t -height 20 -width 80 -bg black -fg green -font ff \
-insertbackground white
pack .t
.t insert 1.0 [exec cat /usr/share/dict/words]
Text Widget Appearance ‹↑›
a trick to find out the top level windows bg colour
puts "[lindex [. configure -bg] 4]"
Tcl/tk labels are a bit limited. The text can only have one style
or colour for example. But we can use a text widget as a replacement
label.
a text widget which looks and behaves like a label
-------
font create ff -family Times -size 14
text .t -font ff -borderwidth 0 -bg [lindex [. configure -bg] 4]
pack .t -padx 20 -pady 20
.t tag configure rr -foreground red
.t insert end "red" rr " normal"
.t configure -state disabled
,,,
Tags And Styling Text ‹↑›
remove all tag info
.t tag delete someTag
remove a tag from an entire document
-------
option add *font "{Courier New} 14"
set t [text .t]; pack $t -padx 5
$t insert end "A small test document
to show some tag magic"
$t tag configure tt -foreground white -background blue
$t tag add tt 1.3 1.6 2.2 2.5
bind . d {
.t tag remove tt 1.0 end
}
bind . x exit
,,,
insert text with a tag
-------
font create ff -family Times -size 14
text .t -font ff
pack .t -padx 20 -pady 20
.t tag configure rr -foreground red
.t insert end "red" rr " normal"
,,,
insert text with a tag
-------
pack [text .t]
.t tag configure rr -foreground red -background white
.t insert end "red" rr " normal"
,,,
style some text by adding tags on regions
pack [text .t]
.t insert 1.0 "This is an example of tagging."
# Set some tags; they have no style as yet
.t tag add foo 1.5 1.10
.t tag add bar 1.15 1.20
# Configure the tags so that we can see them
.t tag configure foo -font {Times 16 {bold italic}}
.t tag configure bar -foreground yellow -background blue
,,,,
* use a named font with a tag
font create df -size 18 -family {Courier New}
pack [text .t] -padx 10 -pady 10
.t insert 1.0 "This is an example of tagging."
# set a tag on a range and then configure it
.t tag add bigtext 1.5 1.10
.t tag configure bigtext -font df
,,,,
TAG RANGES ....
tags can used special range numbers
== example tag ranges
.. 1.0 1.end - the whole of the first line
.. 2.0 2.end - the whole of the 2nd line
..
* format lines of text using tags and ranges
font create df -size 18 -family {Courier New}
pack [text .t] -padx 10 -pady 10
.t insert 1.0 "First line \n Second \n Third"
.t tag add tt 1.0 1.end
.t tag add ss 2.0 2.end
.t tag add rr 3.0 3.end
.t tag configure tt -font {Times 16 {bold italic}}
.t tag configure ss -font {{Helvetica}}
.t tag configure rr -font {{Courier New}}
,,,,
MARKS IN TEXT ....
* using a mark 'a' to delete text
--------
$t mark set a "insert worstart" # create a mark
$t delete a "a lineend" # use it
$t mark unset a #get rid of mark 'a'
show all marks
mark names
get rid of all marks in a document
eval {$t mark unset} [$t mark names]
show the gravity of mark 'm'
$t mark gravity m
Selection Of Text ‹↑›
select the current line
--------
pack [text .t]
.t insert end "
/ to select current line
w to select current word
sounds and sights
here and
there"
focus .t
bind .t / {
.t tag add sel "insert linestart" "insert lineend"
break
}
bind .t w {
.t tag add sel "insert wordstart" "insert wordend"
break
}
bind . x exit
,,,
Insertion Cursor For Text Widget ‹↑›
move the insertion cursor to the 1st character
.txtarea mark set insert 1.0
Undo And Redo ‹↑›
The text widget supports unlimited undo and redo of edits when
the feature has been enabled. By default these functions are bound
to <control-z> and <control-x?> respectively
Images In Text Widgets ‹↑›
We can embedd an image directly into a text widget,
instead of first embedding a label and displaying the
image in that.
display a random gif in a text box with info
. config -borderwidth 10
set ii [exec locate -i *.gif | shuf -n1]
image create photo img -file $ii
puts "file: $ii"
text .t -height 20 -font {-size 14}
.t insert end "
gif name: $ii
gif size: [file size $ii]
"
.t image create 2.10 -image img
pack .t -fill x
bind . x exit
Embedded Windows In Text Widgets ‹↑›
We can put any kind of windowed widget in a text box,
including buttons, frames etc
Gotchas For Text Widgets ‹↑›
If a make a largish default font, then the text
widget goes funny and things packed after it, dont
display... This is 8.5
font and text widget problems
-------
font create df -family {Courier New} -size 14
option add *font df
pack [text .t]
pack [label .l -text "Where am I?"]
,,,
Tkpath ‹↑›
Tkpath may be a superior version of the standard
tcl/tk canvas. It has anti aliased lines, among many
other things but requires Cairo on x11 and other
backends on other systems.
Canvases ‹↑›
The canvas is a Tk widget which allows the drawing of lines
curves shapes, and the embedding of other widgets.
print the coordinates of mouse clicks in a canvas
label .l -text "(x,y)"; canvas .c; pack .c .l
bind .c <Button-1> {.l configure -text "%x,%y" }
Images On Canvases ‹↑›
By putting an image on a canvas we can do fun stuff
like drag it around, draw things on the top of it, and
so on, hours of fun.
put an image on a canvas
#!/usr/local/bin/wish
package require Img
canvas .can
pack .can
set ii "~/Pictures/mandela.png"
set p [image create photo -file $ii]
.can create image 0 0 -image $p -anchor nw
It is impressive that a draggable image can be
created with such little code.
show an image that can be dragged about with the mouse
#!/usr/local/bin/wish
package require Img
wm attributes . -zoomed 1
canvas .can -bg black
pack .can
# select a random image from the Pictures folder
set ii [exec locate -i */Pictures/*.jpg | shuf -n1]
wm title . $ii
set p [image create photo -file $ii]
.can create image 0 0 -image $p -tag movable -anchor nw
.can bind movable <Button-1> {
set object [.can find closest %x %y]
set x %x; set y %y
}
.can bind movable <B1-Motion> {
.can move $object [expr %x -$x] [expr %y - $y]
set x %x; set y %y
}
bind . x exit
make a canvas fill its container window
pack .can -fill both -expand 1
The canvas below fills up the whole screen
draw a fat dashed rectangle on an image with the mouse
#!/usr/local/bin/wish
package require Img
wm attributes . -zoomed 1
canvas .can -bg black
pack .can -fill both -expand 1
label .info -textvar dim
pack .info -fill x
set dim "x y to xx yy"
tkwait visibility .can
puts "Canvas dim: [winfo width .can]w x [winfo height .can]h"
set ll [split [exec locate -i /home/*/Pictures/*.jpg] \n]
set ii [lindex $ll [expr {[clock microseconds]%[llength $ll]}]]
wm title . $ii
set picture [image create photo -file $ii]
.can create image 0 0 -image $picture
bind .can <Button-1> {
set startx %x ; set starty %y
catch {.can delete $lastbox}
catch {unset lastbox}
}
bind .can <B1-Motion> {
catch {.can delete $lastbox}
set lastbox \
[.can create rect $startx $starty %x %y \
-dash {6 6} -width 4]
set dim "box coords = ($startx,$starty) to (%x,%y)"
}
bind . x exit
The gotcha below is -anchor nw so that the image
doesnt get cut off (by default the anchor is center)
The code below should get the image name from standard
input, not randomly. It could be polished in many ways
a simple but useful image crop app
#!/usr/local/bin/wish
package require Img
option add *font "Times 14 italic"
. config -bg black
wm attributes . -zoomed 1
canvas .can -bg black
pack .can -fill both -expand 1
#pack .can
label .info -textvar dim
label .status -textvar status
pack .info -fill x
pack .status -fill x
set dim "x y to xx yy"
set status "Drag mouse to select, 'c' to crop, 'v' to view crop"
tkwait visibility .can
set ii [exec locate -i */Pictures/*.jpg | shuf -n1]
wm title . $ii
image create photo orig -file $ii
image create photo pic
set ww [winfo width .can]; set wh [winfo height .can]
set iw [image width orig]; set ih [image height orig]
set factor 1
if { $iw>$ww || $ih>$wh } {
if { [expr {$iw/double($ww)}] > [expr {$ih/double($wh)}] } {
set factor [expr {int(ceil($iw/double($ww)))}]
} else {
set factor [expr {int(ceil($ih/double($wh)))}]
}
}
pic copy orig -subsample $factor
puts "Canvas dim: [winfo width .can]w x [winfo height .can]h"
puts "Image dim: [image width orig]w x [image height orig]h"
puts "scale factor = $factor"
.can create image 0 0 -image pic -anchor nw
bind .can <Button-1> {
set startx %x ; set starty %y
catch {.can delete $lastbox}
catch {unset lastbox}
}
bind .can <B1-Motion> {
catch {.can delete $lastbox}
set lastbox \
[.can create rect $startx $starty %x %y \
-dash {6 6} -width 4]
set dim "box coords = ($startx,$starty) to (%x,%y)"
}
bind .can <ButtonRelease-1> {
set endx %x; set endy %y
set dim "final box coords = ($startx,$starty) to ($endx,$endy)"
}
bind . c {
set x1 [expr {$startx*$factor}]
set y1 [expr {$starty*$factor}]
set x2 [expr {$endx*$factor}]
set y2 [expr {$endy*$factor}]
set newname "[file rootname $ii].crop[file ext $ii]"
puts "
crop ($x1,$y1) to ($x2,$y2)
scale coord ($x1,$y1) to ($x2,$y2)
newname = $newname"
set t1 [clock millis]
orig write $newname -format JPEG -from $x1 $y1 $x2 $y2
set t2 [clock milliseconds]
set dim "Cropped [file tail $ii] ($startx,$starty) to ($endx,$endy) in [expr {$t2-$t1}] milliseconds !!"
set status "new file is $newname"
}
bind . d {
file delete $newname; set status "Deleted $newname" }
bind . v {
image create photo pic -file $newname
}
bind . h {
set status "Select with mouse, 'c' crop, 'v' view, 'd' delete "
}
bind . x exit
Events For Canvases ‹↑›
note 2 forms of the bind command
bind .can <...> {}
and
.can bind thing <event> {}
Dont know what the difference is
Drawing On Canvases ‹↑›
Rectangles And Squares ‹↑›
draw a dashed square
canvas .c; pack .c; .c create rect 10 10 80 80 -dash {6 6}
draw a rectangle and then delete it on left mouse click
canvas .c -bg white ; pack .c
.c create rect 10 10 160 80 -dash {6 6} -tag abox
bind .c <Button-1> {catch {.c delete abox}}
The recipe above shows deleting objects using tags rather than
'object ids'
use the mouse to drag and draw lots of rectangles
canvas .c -bg white ; pack .c
bind .c <Button-1> {
set startx %x ; set starty %y
catch {unset lastbox}
}
bind .c <B1-Motion> {
catch {.c delete $lastbox}
set lastbox [.c create rect $startx $starty %x %y -tag box]
}
use the mouse to drag out one dashed rectangle at a time
canvas .c -bg white ; pack .c
bind .c <Button-1> {
set startx %x ; set starty %y
catch {.c delete $lastbox}
catch {unset lastbox}
}
bind .c <B1-Motion> {
catch {.c delete $lastbox}
set lastbox [.c create rect $startx $starty %x %y -dash {6 6}]
}
Circles And Ellipses ‹↑›
The canvas 'create oval' command creates a circle or an ellipse
The shape of the ellipse is defined by its 'bounding box'
draw a red filled 4 pixel width border, circle on a canvas
canvas .c ; pack .c
.c create oval 10 10 80 80 -fill red -width 4
draw a salmon coloured ellipse with no outline
canvas .c ; pack .c
.c create oval 10 10 140 80 -fill salmon1 -width 0
draw an ellipse with a dashed outline
canvas .c ; pack .c
.c create oval 10 10 140 80 -dash {,}
draw an ellipse which moves when clicked with the mouse
canvas .c ; pack .c
.c create oval 10 10 140 80 -fill blue -tag movable
.c bind movable <Button-1> {
set object [.c find closest %x %y]
.c move $object 20 20
}
draw an ellipse which can be dragged about by the mouse
canvas .c ; pack .c
.c create oval 10 10 140 80 -fill green -tag movable
.c bind movable <Button-1> {
set object [.c find closest %x %y]
set x %x; set y %y
}
.c bind movable <B1-Motion> {
set dx [expr %x - $x]
set dy [expr %y - $y]
.c move $object $dx $dy
set x %x; set y %y
}
The arc, pieslice or chord is defined by a bounding rectangle
draw a 90 degree pie slice, starting at 45 degrees
canvas .c ; pack .c
.c create arc 10 10 100 100 -start 45 -extent -90 -style pieslice
draw a 90 degree chord, starting at 45 degrees
pack [canvas .c]
.c create arc 10 10 100 100 -start 45 -extent -90 -style chord
draw a 90 degree arc, starting at 45 degrees
pack [canvas .c]
.c create arc 10 10 100 100 -start 45 -extent -90 -style arc
Text On Canvases ‹↑›
Text can also be placed directly into Tk canvases
write some text which can be dragged about with the mouse
canvas .c ; pack .c
.c create text 50 50 -text "hello" -tag movable
.c create text 120 120 -text "drag me" -tag movable
.c bind movable <Button-1> {
set object [.c find closest %x %y]
set x %x; set y %y
}
.c bind movable <B1-Motion> {
.c move $object [expr %x -$x] [expr %y - $y]
set x %x; set y %y
}
Dynamic Interfaces ‹↑›
Standard windowed interfaces are generally terrible, caught in tired and
uncreative paradigms and dictated to by the orthodox widget libraries. It
takes ingenuity and insight to break these old bad habits. Interfaces should
be fluid and dynamic, constantly adapting to the current context and user
intentions.
Hiding Widgets ‹↑›
use 'unmap' or destroy
use pack forget .w
map
Forget And Pack ‹↑›
by packing and forgetting we can create dynamic interfaces.
Images ‹↑›
install extended image handlers (jpeg etc) and documentation
sudo apt-get install libtk-img libtk-img-doc
If you get an error like "couldn't recognize data in image file" then
you need to install the 'Img' extension
display a jpeg image
package require Img
#image create photo img -file "/usr/share/backgrounds/Climbing.jpg"
image create photo img -file "~/Pictures/mandela.png"
pack [label .l -image img] ;
The following is not as fast as simply displaying
an image which is already in memory.
one way to update an image
#!/usr/local/bin/wish
package require Img
set pic [image create photo -file picture1.jpg]
label .display -image $pic
pack .display
button .change -text Change -command {NewPicture}
pack .change
proc NewPicture {} {
global pic
$pic read ./picture2.jpg
}
,,,,
RANDOM IMAGES ....
A more succint version relying on gnu tool shuf
* return a random image name matching a name pattern
proc RandImage pat {
return [exec locate -i /home/*$pat*.jpg | shuf -n1]
}
puts [RandImage boat]
return a random image name matching a name pattern
proc RandImage pat {
set ll [split [exec locate -i /home/*$pat*.jpg] \n]
set ii [lindex $ll [expr {[clock microseconds]%[llength $ll]}]]
return $ii
}
puts [RandImage boat]
Images And Events ‹↑›
scale an image by 1/2 when "z" is pressed
----
package require Img
image create photo img -file "~/Pictures/mandela.png"
image create photo temp
bind . <e> { exit }
bind . <z> {
temp copy img
img blank
img copy temp -subsample 2
temp blank
}
pack [label .l -image img] ;
,,,
The example below is freezing the computer
scale an image by 3/4 when "z" is pressed, and 4/3 when "Z"
----
package require Img
image create photo img -file "~/Pictures/mandela.png"
image create photo temp
bind . <e> { exit }
bind . <z> {
temp copy img -zoom 3
img blank
img copy temp -subsample 4
temp blank
}
bind . <Z> {
temp copy img -zoom 4
img blank
img copy temp -subsample 3
temp blank
}
pack [label .l -image img] ;
,,,
Images ‹↑›
install extended image handlers (jpeg etc) and documentation
sudo apt-get install libtk-img libtk-img-doc
If you get an error like "couldn't recognize data in image file" then
you need to install the 'Img' extension
Creating Images ‹↑›
assign image to an array variable
set img(menu) [image create photo -arg ...]
Displaying Images ‹↑›
display some jpeg image
package require Img
set f [lindex [split [exec locate -i *.jpg] \n] 0]
image create photo img -file $f
pack [label .l -image img]
display a random jpeg image
package require Img
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr { int([llength $ll] * rand()) }]]
image create photo img -file $f
pack [label .l -image img]
bind . x exit
The recipe below seems a bit slower for various reasons, spawning a
shell, doing a 'shuf' etc
use bash to get a random jpg image
package require Img
set f [exec sh -c {locate -i *.jpg | shuf | head -1}]
image create photo img -file $f
pack [label .l -image img]
bind . x exit
display a random jpeg image and print info about it
package require Img
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr { int([llength $ll] * rand()) }]]
image create photo img -file $f
pack [label .l -image img]
bind . i {
puts "
File Name: $f
File size: [file size $f]
Width: [image width img]
Height: [image height img]
"
}
bind . x exit
display some png image
package require Img
set f [lindex [split [exec locate -i *.png] \n] 0]
image create photo img -file $f
pack [label .l -image img]
display a jpeg 'safely' checking for existance etc
package require Img
set f "/usr/share/backgrounds/xfce/xfce-blue.jpg"
label .l
if { [file exists $f] && [file isfile $f] } {
image create photo img -file $f
.l configure -image img
} else {
.l configure -text "Couldnt find '$f'"
}
pack .l
bind . x exit
another approach to safely opening an image using catch
package require Img
set f "/usr/share/backgrounds/xfce/xfce-blue.jpg"
label .l
if { [file exists $f] && [file isfile $f] } {
image create photo img -file $f
.l configure -image img
} else {
.l configure -text "Couldnt find '$f'"
}
pack .l
bind . x exit
Deleting Images ‹↑›
delete all images (and free up memory)
-------
foreach name [image names] {
image delete $name
}
,,,
delete unused images
--------
foreach name [image names] {
if {![image inuse $name]} { image delete $name }
}
,,,
Palettes Of Images ‹↑›
we can use the palette option of 'read' display an
image in greyscale or with a specified colour palette.
display a jpeg in greyscale with 10 shades of grey
package require Img
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr { int([llength $ll] * rand()) }]]
image create photo img -palette 10 -file $f
pack [label .l -image img]
bind . x exit
display a jpeg with 5 reds 5 greens and 4 blues
package require Img
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr { int([llength $ll] * rand()) }]]
image create photo img -palette 5/5/4 -file $f
pack [label .l -image img]
bind . x exit
display a jpeg with 5 reds 5 greens and 4 blues
package require Img
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr { int([llength $ll] * rand()) }]]
image create photo img -palette 5/5/4 -file $f
pack [label .l -image img]
bind . x exit
Greyscale ‹↑›
convert a jpeg to greyscale
package require Img
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr { int([llength $ll] * rand()) }]]
image create photo img -file $f
pack [label .l -image img]
img write "test.jpg" -grayscale -format JPEG
bind . x exit
convert a jpeg to greyscale and display info
package require Img
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr { int([llength $ll] * rand()) }]]
image create photo img -file $f
pack [label .l -image img]
img write "test.jpg" -grayscale -format JPEG
bind . x exit
convert a jpeg to greyscale and display info
package require Img
set i [expr { [clock milli]%1000}]
set f [lindex [split [exec locate -i *.jpg] \n] $i]
puts "Starting Conversion..."
set t1 [clock milliseconds]
image create photo ii -file $f
ii write "test.jpg" -grayscale -format JPEG
set t2 [clock milliseconds]
puts "
Original file: $f
Original size: [file size $f]
Converted file: test.jpg
Converted size: [file size test.jpg]
Conversion Time: [expr {$t2-$t1}] milliseconds
"
pack [label .l -image ii]
Thumbnail Images ‹↑›
Tcllib (which has to be separately installed) contains the jpeg package
which can deal with thumbnails
get the thumbnail from a jpeg as binary data, or empty string if none
-------
package require jpeg
set fh [open thumbnail.jpg w+]
fconfigure $fh -translation binary -encoding binary
puts -nonewline $fh [::jpeg::getThumbnail photo.jpg]
close $fh
,,,
Scaling Images ‹↑›
To scale 'down' (reduce the size of an image) we use the -subsample option of
the "copy" command. To scale 'up' (increase image size) use -zoom
shrink (scale down) a random image by 1/4
----
package require Img
set i [exec locate -i */Pictures/*.jpg | shuf -n1]
image create photo temp -file $i
image create photo img
img copy temp -subsample 4
image delete temp
pack [label .l -image img]
,,,
scale up (make bigger) an image by a factor of 3
----
package require Img
set i [exec locate -i */Pictures/*.jpg | shuf -n1]
image create photo temp -file $i
image create photo img
img copy temp -zoom 3
image delete temp
pack [label .l -image img]
,,,
scale in x and y directions
---
package require Img
set i [exec locate -i */Pictures/*.jpg | shuf -n1]
set im [image create photo]
set t [image create photo -file $i]
set xfactor 1
set yfactor 3
$im copy $t -shrink -zoom $xfactor $yfactor
image delete $t
pack [label .l -image $im]
,,,
The code below runs slowly, much more slowly than just
shrinking the image. This could be a ram problem, I suppose
scale a random image by 3/16 and print info
----
package require Img
set f [exec locate -i */Pictures/*.jpg | shuf -n1]
set t1 [clock milliseconds]
image create photo img -file $f
image create photo temp
temp copy img -zoom 3; img blank
img copy temp -shrink -subsample 16
image delete temp
set t2 [clock milliseconds]
pack [label .l -image img]
puts "
Info
name: $f
size: [file size $f]
scale time (3/16): [expr {$t2-$t1}]
"
,,,
Scale To Window Size ‹↑›
/home/mjb/OldEEE/.local/share/Trash/files/100_1596.JPG
Firstly, it should be said, that tcl/tk cant really do
scaling to an exact pixel dimension (or it would be too
slow). But approximate scaling is fast and good.
In the code below the test "iw/ww > ih/wh" accounts for the
idea that the widget in which the image must fit is not
necessarily a square, probably a rectangle. So this test
determines whether to scale by width or height. The first
test "iw>ww || ih>wh" determines whether scaling is necessary
at all.
perhaps the most succinct scale calculation.
--------
set ww [winfo width .w]; set wh [winfo height .w]
set iw [image width ii]; set ih [image height ii]
set factor 1
if { $iw>$ww || $ih>$wh } {
if { [expr {$iw/double($ww)}] > [expr {$ih/double($wh)}] } {
set factor [expr {ceil($iw/double($ww))}]
} else {
set factor [expr {ceil($ih/double($wh))}]
}
}
,,,
a function to perform the scale operation
--------
package require Img
proc ScaleToFit {w image} {
set ww [winfo width $w]; set wh [winfo height $w]
set iw [image width $image]; set ih [image height $image]
set factor 1
if { $iw>$ww || $ih>$wh } {
if { [expr {$iw/double($ww)}] > [expr {$ih/double($wh)}] } {
set factor [expr {ceil($iw/double($ww))}]
} else {
set factor [expr {ceil($ih/double($wh))}]
}
}
return $factor
}
wm geometry . 400x300
set f [exec locate -i */Pictures/*.jpg | shuf -n1]
image create photo im -file $f
tkwait visibility .
puts "image: $f, scale factor: [ScaleToFit . im]"
,,,
We could make the above even shorter and more obscure with
an eval statement.
an iterative way to get the scale (shrink) factor
------
set ww [winfo width .w]; set wh [winfo height .w]
set iw [image width ii]; set ih [image height ii]
set factor 1
set dividend [expr {$iw + $ih}]
if { $iw>$ih && $iw>$ww } {
while {$dividend > $ww} {
incr factor
set dividend [expr {$iw/$factor}]
}
}
if { $ih>=$iw && $ih>$wh } {
while {$dividend > $wh} {
incr factor
set dividend [expr {$ih/$factor}]
}
}
image create photo ss
ss copy ii -subsample $factor
.w configure -image ss
,,,
The technique below has the problem of scaling too much when
the operation iw/ww is an integer.
a scaling technique iw=image width, ww=widget width
set factor [expr {$iw/$ww + 1}]
fixes the excessive scale problem
set factor [expr {$iw/$ww + 1}]
puts [expr {$a<2? "a<2": "a>=2"}]
a much more succinct technique
--------
package require Img
wm geometry . 400x300
label .w
pack .w -fill both -expand 1
tkwait visibility .w
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr {[clock microseconds]%[llength $ll]}]]
image create photo ii -file $f
set ww [winfo width .w]; set wh [winfo height .w]
set iw [image width ii]; set ih [image height ii]
set factor 1
if { $iw>$ww || $ih>$wh } {
if { [expr {$iw/double($ww)}] > [expr {$ih/double($wh)}] } {
set factor [expr {int(ceil($iw/double($ww)))}]
} else {
set factor [expr {int(ceil($ih/double($wh)))}]
}
}
image create photo ss
ss copy ii -subsample $factor
.w configure -image ss
puts "
factor: $factor
image dimensions: $iw w X $ih h pixels
widget dimensions: $ww w X $wh h pixels
"
,,,
a random image viewer
package require Img
font create df -size 15
wm attributes . -fullscreen 1
label .w
label .status -textvar status -font df
pack .status -fill x
pack .w -fill both -expand 1
tkwait visibility .w
set status "h - help"
after 4000 { set status {} }
image create photo ss
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr {[clock microseconds]%[llength $ll]}]]
set ww [winfo width .w]; set wh [winfo height .w]
bind . h {
set status \
"<space> - next image, h - help, x - exit, f - file name, F - favourites"
after 4000 { set status {} }
}
bind . f {
set status $f
after 4000 { set status {} }
}
bind . F {
exec echo "$f" >> ~/Pictures/fav.txt
set status "added [file tail $f] to fav.txt"
after 4000 { set status {} }
}
bind . <space> {
set f [lindex $ll [expr {[clock microseconds]%%[llength $ll]}]]
image create photo ii -file $f
set ww [winfo width .w]; set wh [winfo height .w]
set iw [image width ii]; set ih [image height ii]
set factor 1
if { $iw>$ww || $ih>$wh } {
if { [expr {$iw/double($ww)}] > [expr {$ih/double($wh)}] } {
set factor [expr {int(ceil($iw/double($ww)))}]
} else {
set factor [expr {int(ceil($ih/double($wh)))}]
}
}
puts "
$f
file size: [file size $f]
widget dimensions: $ww w X $wh h pixels
image dimensions: $iw w X $ih h pixels
div factor: $factor
new dimensions: [expr {$iw/$factor}]w X [expr {$ih/$factor}]h pixels
"
ss blank
ss copy ii -shrink -subsample $factor
.w configure -image ss
}
bind . x exit
develop the image viewer into an app:
package require Img
#wm geometry . 400x300
wm attributes . -fullscreen 1
label .w -bg black
pack .w -fill both -expand 1
entry .search -textvar find; entry .rename -textvar newname
tkwait visibility .w
image create photo ss
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr {[clock microseconds]%[llength $ll]}]]
set ww [winfo width .w]; set wh [winfo height .w]
bind . / {
pack .search; focus .search
}
bind .search <Return> {
puts $find
pack forget .search
}
bind . r {
pack .rename; focus .rename
set newname {}
}
bind .rename <Return> {
if {$newname ne ""} {
puts "[file dirname $f]/$newname.jpg"
file rename $f "[file dirname $f]/$newname.jpg"
} else { puts "Nothing renamed" }
pack forget .rename
}
bind . <space> {
set old $f
set f [lindex $ll [expr {[clock microseconds]%%[llength $ll]}]]
image create photo ii -file $f
#ii blank; ii read $f
set iw [image width ii]; set ih [image height ii]
set factor 1
set dividend [expr {$iw + $ih}]
if { $iw>$ih && $iw>$ww } {
puts "w>h and scalable"
while {$dividend > $ww} {
incr factor; set dividend [expr {$iw/$factor}]
}
}
if { $ih>=$iw && $ih>$wh } {
puts "h>w and scalable"
while {$dividend > $wh} {
incr factor; set dividend [expr {$ih/$factor}]
}
}
puts "
$f
widget dimensions: $ww w X $wh h pixels
image dimensions: $iw w X $ih h pixels
div factor: $factor
new dimensions: [expr {$iw/$factor}]w X [expr {$ih/$factor}]h pixels
"
ss blank
ss copy ii -shrink -subsample $factor
.w configure -image ss
}
bind . x exit
Fullscreen Image Display ‹↑›
The script below is working toward displaying a set of images
from a folder in fullscreen with keystrokes cyclying through the
images. This is the beginning of a useful photo management
application
Ideas: create a favourite list (plain text), create mail lists
to particular people, based on aliases in muttrc (resize photo
to web size) resize photo, rename, add exiv comment, search by
name, search by exiv time stamp etc. Experiment with loading 2
images and swapping to see if it is faster
full screen image display
----
package require Img
wm attributes . -fullscreen 1
. configure -bg black
#set dir /home/arjuna/Pictures/camera.9.july.2013/
set dir /home/mjb/Pictures/9.september.2013/
set ii [glob -dir $dir *.{jpg,JPG}]
set size [llength $ii]
set index 0
set thisImage [lindex $ii 0]
image create photo img -file "$thisImage"
image create photo temp
label .pic -image img -bg black
label .caption -bg black -fg white -textvar dim \
-font {-size 10}
label .status -bg black -fg green -textvar info \
-font {-size 10}
temp copy img -zoom 3
img blank
img copy temp -shrink -subsample 16
image delete temp
pack .status -side top -fill x
pack .caption -side top -fill x
pack .pic -side top -fill both -pady 20
after 200 {
set sw [winfo width .pic]
set sh [winfo height .pic]
}
set dim "[image width img]w x [image height img]h"
set info "file=$thisImage (sw=$sw sh=$sh)"
proc SetImage {} {
# find the scale (shrink factor
# where x is the image width or height and
# y is the widget width or height
# First test if image width > height
# then text if image w/h > widget w/h
# if so the scale (shrink)
set x 2345; set y 1024
for {set i 2} {$i < 1000} {incr i} {
set q [expr {$x/$i}]
if {$q < $y} {
puts "$x/$i=$q < $y";
break
}
}
global index ii dim info img size
set t1 [clock milliseconds]
set thisImage [lindex $ii $index]
image create photo temp -file "$thisImage"
set dim "[image width temp]w x [image height temp]h"
img copy temp -shrink -subsample 4
image delete temp
set t2 [clock milliseconds]
set info \
"$thisImage: loaded/scaled in [expr $t2 - $t1] ms (index:$index of $size)"
}
bind . x { exit }
bind . h {
puts " l - jump to last image "
}
bind . l {
global index
set index [expr [llength $ii] - 1]
SetImage
}
bind . j { incr index 10; SetImage }
bind . J { incr index -10; SetImage }
bind . c {
set dir [tk_chooseDirectory -initialdir "~/Pictures"]
set ii [glob -dir $dir *.{jpg,JPG}]
set index 0
set info "loaded $dir: [llength $ii] files"
set dim {}
set size [llength $ii]
SetImage
}
bind . n {
global index
incr index 1
if { $index == [llength $ii] } { set index 0 }
SetImage
}
bind . N {
global index
if { $index > 0 } { incr index -1
} else {
set index [expr [llength $ii] - 1]
}
SetImage
}
bind . d {
exec rm "[lindex $ii $index]"
img blank
set info "[lindex $ii $index] was deleted permanently"
set ii [glob -directory $dir *.{jpg,JPG}]
set size [llength $ii]
}
,,,
swapping pre loaded images
--------
package require Img
wm attributes . -fullscreen 1
. configure -bg black
set dir /home/arjuna/Pictures/camera.9.july.2013/
set ii [glob -dir $dir *.{jpg,JPG}]
image create photo img -file "[lindex $ii 0]"
image create photo img2 -file "[lindex $ii 1]"
image create photo temp
temp copy img -shrink -subsample 4
img blank
img copy temp -shrink
temp copy img2 -shrink -subsample 4
img2 blank
img2 copy temp -shrink
image delete temp
label .pic -image img -bg black
label .caption -bg black -fg white -textvar dim \
-font {-size 10}
label .status -bg black -fg green -textvar info \
-font {-size 10}
pack .status -side top -fill x
pack .caption -side top -fill x
pack .pic -side top -fill both -pady 20
set dim "[image width img]w x [image height img]h"
set info {}
bind . x { exit }
bind . n {
.pic configure -image img2
}
bind . N {
.pic configure -image img
}
,,,
a listbox to choose a jpeg image to display
package require Img
listbox .lb -width 40 -height 40 -listvariable f
set f [glob -directory ~/Pictures/Camera/ *.{jpg,JPG}]
image create photo img
bind .lb <ButtonRelease-1> {
img blank
img read [.lb get [.lb curselection]]
}
label .l -image img
pack .lb .l -side left
draw a gif image on a canvas centred at point (100,100)
set i "/usr/share/doc/tk8.5/examples/images/earth.gif"
image create photo img -file $i
pack [canvas .c]
.c create image 100 100 -image img
draw a gif image on a canvas anchored at 0,0
set i "/usr/share/doc/tk8.5/examples/images/earth.gif"
image create photo img -file $i
pack [canvas .c]
.c create image 0 0 -image img -anchor nw
draw a only a 30pixel block from a gif image on a canvas
set f "/usr/share/doc/tk8.4/examples/images/earth.gif"
set ii [image create photo img]
$ii read $f -from 0 0 30 30
pack [canvas .c]
.c create image 0 0 -image img -anchor nw
choose a gif to open from a dialog box
set typelist {{"Gif Images" ".gif"}}
set ii [image create photo img]
button .btn -text "Choose an image" -command {
set f [tk_getOpenFile -filetypes $typelist]
$ii read $f
}
canvas .c
.c create image 0 0 -image img -anchor nw
pack .c .btn
copy with zoom a square block from a gif image
set f "/usr/share/doc/tk8.4/examples/images/earth.gif"
set ii [image create photo img -file $f]
set jj [image create photo jj]
$jj copy img -zoom 2 -from 30 30 100 100
pack [canvas .c]
.c create image 0 0 -image img -anchor nw
.c create image 300 0 -image jj -anchor nw
draw a gif image on a canvas and display width in a label
image create photo img -file /usr/lib/perl5/Tk/icon.gif
label .l -text "image width: [image width img]"
canvas .c -bg white
pack .c .l
.c create image 100 100 -image img
display a gif which can be dragged about with the mouse
image create photo img -file /usr/lib/perl5/Tk/icon.gif
canvas .c ; pack .c
.c create image 100 100 -image img -tag movable
.c bind movable <Button-1> {
set object [.c find closest %x %y]
set x %x; set y %y
}
.c bind movable <B1-Motion> {
.c move $object [expr %x -$x] [expr %y - $y]
set x %x; set y %y
}
display an image, select a section and write selection to file
#package require Img
set s "/usr/share/doc/tk8.4/examples/images/earth.gif"
set ii [image create photo img -file $s]
label .l -text "()"
canvas .c -bg white ; pack .c .l
.c create image 0 0 -image img -anchor nw
bind .c <Button-1> {
set startx %x ; set starty %y
catch {.c delete $lastbox}
catch {unset lastbox}
}
bind .c <B1-Motion> {
catch {.c delete $lastbox}
set lastbox [.c create rect $startx $starty %x %y -dash {6 6}]
set x %x; set y %y
.l configure -text "($startx,$starty) - ($x,$y)"
}
bind . <Return> {
.l configure -text "writing selection to 'test.gif'"
$ii write test.gif -format gif -from $startx $starty $x $y
catch {image delete jj}
catch {.c delete jj}
set jj [image create photo jj -file test.gif]
.c create image 350 0 -image jj -anchor nw
}
Analysing Images ‹↑›
find out how many images are defined
-------
puts [image names]
,,,,
Formats Of Images ‹↑›
show what types of image are available (!= format)
-----
package require Img
puts [image types]
,,,
Format Conversion ‹↑›
convert a several '.bmp' files to a different image format
package require Img
cd /temp
foreach file [glob *.bmp] {
set root [file rootname $file]
set image [image create photo -file $file]
foreach {format suffix} {JPEG jpg GIF gif PNG png} {
$image write $root.$suffix -format $format
}
}
convert a jpeg to png format
package require Img
set f [exec locate -i *.jpg | shuf -n1]
set t1 [clock millis]
image create photo im -file $f
set t2 [clock millis]
im write "$f.png" -format PNG
set t3 [clock millis]
puts "
loaded $f in [expr $t2-$t1] ms, ([file size $f]bytes)
wrote $f.png in [expr $t3-$t1] ms, ([file size $f.png]bytes)
"
These conversions are creating massive png files, 16 times
as big as the original jpeg
convert a jpeg to png format and display info
package require Img
set i [expr { [clock milli]%1000}]
set f [lindex [split [exec locate -i *.jpg] \n] $i]
puts "Starting Conversion..."
set t1 [clock milliseconds]
image create photo ii -file $f
ii write "$f.png" -format PNG
set t2 [clock milliseconds]
puts "
Original file: $f
Original size: [file size $f]
Converted file: $f.png
Converted size: [file size $f.png]
Conversion Time: [expr {$t2-$t1}] milliseconds
"
Editing Images ‹↑›
use -put {#rrggbb #rrggbb #rrggbb ... etc}
or is it a list of rows
write a photo image pixel by pixel, this can be slow
--------
for {set x 0} {$x<$xmax} {incr x} {
for {set y 0} {$y<$ymax} {incr y} {
$photo put $color -to $x $y
}
}
,,,
Pixels Of Images ‹↑›
We can edit images by writing pixels with the "put"
command.
write a blue colour block to a photo image
--------
package require Img
set color blue
set im [exec locate -i *.png | shuf -n1]
set pic [image create photo -file $im]
for {set x 0} {$x<200} {incr x} {
for {set y 0} {$y<200} {incr y} {
$pic put $color -to $x $y
}
}
label .l -image $pic
pack .l
,,,
another faster way to do the same
--------
package require Img
set im [exec locate -i *.jpg | shuf -n1]
image create photo pic -file $im
pic put blue -to 0 0 200 200
pack [label .l -image pic]
,,,
an array of colours
--------
package require Img
set im [exec locate -i *.jpg | shuf -n1]
set pic [image create photo -file $im]
$pic put {blue lightblue green lightgreen} -to 0 0 200 200
label .l -image $pic
pack .l
,,,
write a tiny matrix of pixels to an image
------
package require Img
set f [exec locate -i *pictures*.jpg | shuf -n1]
set data {
{red yellow green blue}
{white black orange purple}
{red yellow green blue}
}
image create photo im -file $f
im put $data -to 0 0
pack [label .l -image im]
,,,
write a block of data and tile the data over the target area
------
package require Img
set f [exec locate -i *pictures*.jpg | shuf -n1]
set data {
{red yellow green blue}
{white black orange purple}
{red yellow green blue}
{white black orange purple}
{red yellow green blue}
{white black orange purple}
}
image create photo im -file $f
im put $data -to 0 0 300 300
label .l -image im
pack .l
,,,
slow way and fast
-------
image create photo $pic -data $data
# faster
image create photo $pic
$pic put $data
,,,
put a 5x5 pixel black block into an image
$src put black -to 5 5 10 10
put a square black block into top left of photo
package require Img
set f [exec locate -i *.jpg | shuf -n1]
image create photo ii -file $f
ii put black -to 5 5 30 30
pack [label .l -image ii]
bind . x exit
put a big orange block in
------
package require Img
set cc orange
set f [lindex [split [exec locate -i *.jpg] \n] 50]
image create photo ii -file $f
ii put $cc -to 5 5 80 80
pack [label .l -image ii]
bind . x exit
,,,
Note in the code below that the destination block is much bigger than
the data block (which is only 3x3 pixels). So the data block just
gets replicated throughout the destination block
put coloured pixel data in
------
package require Img
set f [lindex [split [exec locate -i *.jpg] \n] 50]
image create photo ii -file $f
set data {
{yellow green blue} {yellow green blue} {yellow green blue}}
ii put $data -to 5 5 80 80
pack [label .l -image ii]
bind . x exit
,,,
cut out a block of an image
---------
set src [image create photo]
set bgcolor black
# load the source ... let's say it's 32x32
set dst [image create photo]
$dst copy $src -from 5 5 10 10
$src put $bgcolor -to 5 5 10 10
,,,
Drawing On Images ‹↑›
draw a green rectangle on an image
-------
package require Img
set f [exec locate -i *pictures*.jpg | shuf -n1]
image create photo ii -file $f
ii put darkgreen -to 20 20 300 40
pack [label .l -image ii]
bind . x exit
,,,
draw a hollow green rectangle on an image
-------
package require Img
set f [exec locate -i *pictures*.jpg | shuf -n1]
image create photo ii -file $f
ii put darkgreen -to 20 20 300 40
ii put darkgreen -to 20 20 40 200
ii put darkgreen -to 20 200 300 200
pack [label .l -image ii]
bind . x exit
,,,
Cropping Images ‹↑›
Rectangle Crop ‹↑›
The simplest image crop is a rectangle. We can just use
$result copy $img -from $x1 $y1 $x2 $y2
a rectangle image crop
-------
package require Img
proc RectCrop { img x1 y1 x2 y2} {
set result [image create photo]
set iw [image width $img]
set ih [image height $img]
if {$x1 > $iw} { set x1 $iw }
if {$x2 > $iw} { set x2 $iw }
if {$y1 > $ih} { set y1 $ih }
if {$y2 > $ih} { set y2 $ih }
$result copy $img -from $x1 $y1 $x2 $y2
return $result
}
set f [exec locate -i *pictures*.jpg | shuf -n1]
set im [image create photo -file $f]
set cropped [image create photo]
puts " image: $f \n size: [file size $f]"
pack [label .l -image $im]
wm title . "c to crop, w to save, x to exit"
bind . c {
set x1 200; set y1 200; set x2 400; set y2 3000
set t1 [clock milliseconds]
set cropped [RectCrop $im $x1 $y1 $x2 $y2]
set t2 [clock milliseconds]
puts "Cropped in [expr {$t2 - $t1}] milliseconds"
.l configure -image $cropped
}
bind . w {
set newname "[file rootname $f].circle[file ext $f]"
$cropped write $newname -format JPEG
puts "
Saved file: $newname
File size: [expr [file size $newname]/1024]K"
}
bind . x exit
,,,
Rounded Corner Crop ‹↑›
The code is similar to the circle crop but there are
4 for loops, one for each corner. The code is running
much faster than the circle crop because we are only
iterating over a small number of points
a rounded corner crop
-------
package require Img
# crops an image into a rectangle with rounded
# corners.
# image, topleft, bottom right, radius of corner
proc RoundedCornerCrop { img x1 y1 x2 y2 radius} {
set result [image create photo]
$result copy $img -from $x1 $y1 $x2 $y2
set iw [image width $result]
set ih [image height $result]
# round top left corner
set cx $radius; set cy $radius
for {set x 0} {$x<$radius} {incr x} {
for {set y 0} {$y<$radius} {incr y} {
set dx [expr {$x - $cx}]
set dy [expr {$y - $cy}]
# test if the point is in the circle or not
# if not make it white
if {[expr {hypot($dx,$dy)}] >= $radius} {
$result put white -to $x $y
}
}
}
# round top right corner
set cx [expr {$iw-$radius}]; set cy $radius
for {set x $cx} {$x<$iw} {incr x} {
for {set y 0} {$y<$radius} {incr y} {
set dx [expr {$x - $cx}]
set dy [expr {$y - $cy}]
if {[expr {hypot($dx,$dy)}] >= $radius} {
$result put white -to $x $y
}
}
}
# round bottom left corner
set cx $radius
set cy [expr {$ih-$radius}];
for {set x 0} {$x<$radius} {incr x} {
for {set y $cy} {$y<$ih} {incr y} {
set dx [expr {$x - $cx}]
set dy [expr {$y - $cy}]
if {[expr {hypot($dx,$dy)}] >= $radius} {
$result put white -to $x $y
}
}
}
# round bottom right corner
set cx [expr {$iw-$radius}];
set cy [expr {$ih-$radius}];
for {set x $cx} {$x<$iw} {incr x} {
for {set y $cy} {$y<$ih} {incr y} {
set dx [expr {$x - $cx}]
set dy [expr {$y - $cy}]
if {[expr {hypot($dx,$dy)}] >= $radius} {
$result put white -to $x $y
}
}
}
return $result
} ; # end proc
set f [exec locate -i *pictures*.jpg | shuf -n1]
set im [image create photo -file $f]
set cropped [image create photo]
puts " image: $f \n size: [file size $f]"
pack [label .l -image $im]
wm title . "c to circle crop, w to save, x to exit"
bind . c {
set x1 200; set y1 200; set x2 400; set y2 500
set t1 [clock milliseconds]
set cropped [RoundedCornerCrop $im $x1 $y1 $x2 $y2 30]
set t2 [clock milliseconds]
puts "Cropped in [expr {$t2 - $t1}] milliseconds"
.l configure -image $cropped
}
bind . w {
set newname "[file rootname $f].circle[file ext $f]"
$cropped write $newname -format JPEG
puts "
Saved file: $newname
File size: [file size $newname]"
}
bind . x exit
,,,
Circle Crop ‹↑›
Rectangle crops are straight forward, but other shapes
require some programming.
The code below is running very quickly 880 ms for a 100
pixel radius circle, well at least its acceptable. A
rounded corner rectangle crop would be even faster
Also, this may be much faster by eliminating the function
call InCircle. Nope: eliminating a function call to
"InCircle" doesnt speed up the code much
I wonder if all this could be done by drawing on
the canvas with geometry primitives and then saving
to postscript??
another version of the image circle crop
-------
package require Img
# crops an image into a circle based on a bounding
# box (top left corner, bottom right corner)
proc CircleCrop { img x1 y1 x2 y2 } {
set result [image create photo]
# make the rectangle a square
if { [expr {$x2-$x1}] > [expr {$y2-$y1}]} {
set x2 [expr {$x1+$y2-$y1}]
} elseif { [expr {$y2-$y1}] > [expr {$x2-$x1}]} {
set y2 [expr {$y1+$x2-$x1}]
}
$result copy $img -from $x1 $y1 $x2 $y2
set iw [image width $result]
set ih [image height $result]
set cx [expr {($x2-$x1)/2}]
set cy [expr {($y2-$y1)/2}]
set radius $cx
for {set x 0} {$x<$iw} {incr x} {
for {set y 0} {$y<$ih} {incr y} {
set dx [expr {$x - $cx}]
set dy [expr {$y - $cy}]
# test if the point is in the circle or not
# if not make it white
if {[expr {hypot($dx,$dy)}] >= $radius} {
$result put white -to $x $y
}
}
}
return $result
} ; # end proc CircleCrop
set f [exec locate -i *pictures*.jpg | shuf -n1]
set im [image create photo -file $f]
set cropped [image create photo]
puts " image: $f \n size: [file size $f]"
label .l -image $im
label .help -textvar help
pack .l .help
set help "c to circle crop, w to save, x to exit"
bind . c {
set x1 200; set y1 200; set x2 400; set y2 500
set t1 [clock milliseconds]
set cropped [CircleCrop $im $x1 $y1 $x2 $y2]
set t2 [clock milliseconds]
puts "Cropped in [expr {$t2 - $t1}] milliseconds"
.l configure -image $cropped
}
bind . w {
set newname "[file rootname $f].circle[file ext $f]"
$cropped write $newname -format JPEG
puts "
Saved file: $newname
File size: [file size $newname]"
}
bind . x exit
,,,
Strangely this is not much slower than the version
above with no function call.
a version of the image circle crop with Incircle fn
-------
package require Img
proc InCircle {point center radius} {
#puts "$point, $center, $radius"
set x [expr {[lindex $point 0] - [lindex $center 0]}]
set y [expr {[lindex $point 1] - [lindex $center 1]}]
if {[expr {hypot($x,$y)}] <= $radius} {
return true
} else {
return false
}
}
set f [exec locate -i *pictures*.jpg | shuf -n1]
image create photo orig -file $f
image create photo ii
puts " image: $f \n size: [file size $f]"
set cx 200; set cy 200; set rad 100
set t1 [clock milliseconds]
set x 0; set y 0
ii copy orig -from \
[expr {max($cx - $rad, 0)}] [expr {$cy - $rad}] \
[expr {$cx + $rad}] [expr {$cy + $rad}]
set iw [image width ii]
set ih [image height ii]
for {set x 0} {$x<$iw} {incr x} {
for {set y 0} {$y<$ih} {incr y} {
if {! [InCircle [list $x $y] [list $rad $rad] $rad]} {
ii put white -to $x $y
}
}
}
set t2 [clock milliseconds]
puts "Took [expr {$t2 - $t1}] milliseconds"
pack [label .l -image ii]
bind . w {
set newname "[file rootname $f].circle[file ext $f]"
ii write $newname -format JPEG
puts "
Saved file: $newname
File size: [file size $newname]"
}
bind . x exit
,,,
crop an image in a circle with black border
-------
package require Img
# crops an image into a circle based on a bounding
# box (top left corner, bottom right corner, border thickness)
proc CircleBorderCrop { img x1 y1 x2 y2 border } {
set result [image create photo]
# make the rectangle a square
if { [expr {$x2-$x1}] > [expr {$y2-$y1}]} {
set x2 [expr {$x1+$y2-$y1}]
} elseif { [expr {$y2-$y1}] > [expr {$x2-$x1}]} {
set y2 [expr {$y1+$x2-$x1}]
}
$result copy $img -from $x1 $y1 $x2 $y2
set iw [image width $result]
set ih [image height $result]
set cx [expr {($x2-$x1)/2}]
set cy [expr {($y2-$y1)/2}]
set radius $cx
for {set x 0} {$x<$iw} {incr x} {
for {set y 0} {$y<$ih} {incr y} {
set dx [expr {$x - $cx}]
set dy [expr {$y - $cy}]
# test if the point is in the circle or not
# if not make it white
if {[expr {hypot($dx,$dy)}] > $radius} {
$result put white -to $x $y
} elseif {[expr {hypot($dx,$dy)}] > [expr {$radius-$border}]} {
$result put black -to $x $y
}
}
}
return $result
} ; # end proc
set f [exec locate -i *pictures*.jpg | shuf -n1]
set im [image create photo -file $f]
set cropped [image create photo]
puts " image: $f \n size: [file size $f]"
label .l -image $im
label .help -textvar help
pack .l .help
set help "c to circle crop, w to save, x to exit"
bind . c {
set x1 200; set y1 200; set x2 400; set y2 500
set t1 [clock milliseconds]
set cropped [CircleBorderCrop $im $x1 $y1 $x2 $y2 5]
set t2 [clock milliseconds]
puts "Cropped in [expr {$t2 - $t1}] milliseconds"
.l configure -image $cropped
}
bind . w {
set newname "[file rootname $f].circle[file ext $f]"
$cropped write $newname -format JPEG
puts "
Saved file: $newname
File size: [file size $newname]"
}
bind . x exit
,,,
Compositing Or Montage Images ‹↑›
This refers to overlaying one image on top of another.
This can be done with the "copy" function
insert a shrunk image in another
-------
package require Img
set f [exec locate -i *pictures*.jpg | shuf -n1]
set g [exec locate -i *pictures*.jpg | shuf -n1]
image create photo ii -file $f
image create photo jj -file $g
ii copy jj -to 100 100 -subsample 4
pack [label .l -image ii]
bind . x exit
,,,
insert a shrunk image in itself
-------
package require Img
set f [exec locate -i *pictures*.jpg | shuf -n1]
image create photo ii -file $f
image create photo jj -file $f
ii copy jj -to 100 100 -subsample 4
pack [label .l -image ii]
bind . x exit
,,,
If the source data is less that the destination area for the
"copy" command, then tcl/tk automatically tiles the data onto
the destination image.
tile an image on itself
-------
package require Img
set f [exec locate -i *pictures*.jpg | shuf -n1]
image create photo ii -file $f
image create photo jj -file $f
set iw [expr {[image width ii] - 50}]
set ih [expr {[image height ii] - 50}]
ii copy jj -to 50 50 $iw $ih -subsample 6
pack [label .l -image ii]
bind . x exit
,,,
Montages ‹↑›
A montage, apparently is a set of photos all stuck together
side by side, the aesthetics of which are dubious to say the least
The code below is incomplete, and could also be accomplished
by using labels in a grid.
a diagonal tile of images of different sizes
-------
package require Img
set ii [split [exec locate -i *pictures*.jpg | shuf -n10] \n]
set f [lindex $ii 0]
image create photo ii -file $f
for {set n 1} {$n<10} {incr n} {
set f [lindex $ii $n]
image create photo image$n -file $f
ii copy image$n -to [expr {30*$n}] [expr {30*$n}] -subsample 8
}
pack [label .l -image ii]
bind . x exit
,,,
Metadata For Images ‹↑›
Exif Information ‹↑›
exif is textual information embedded with jpeg files.
try the tcllib::jpeg code for writing exif comments etc
But you have to install tcllib first
get one value from the the exif data
-------
package require jpeg
set f [lindex [split [exec locate -i *.jpg] \n] 3245]
array set exif [::jpeg::getExif $f]
#puts "max f-stop: [::jpeg::formatExif [list MaxAperture $exif(MaxAperture)]]"
puts "$exif"
,,,
get all exif info from a jpeg
-------
package require jpeg
set ii [lindex [split [exec locate -i *.jpg] \n] 3245]
foreach {key val} [::jpeg::formatExif [::jpeg::getExif $ii]] {
puts "$key: $val"
}
,,,
Dimensions Of Images ‹↑›
It can be handy to interrogate image files for their
dimensions. One way to do this is with exif info
get all exif info from a jpeg
-------
package require jpeg
set ii [split [exec locate -i *.jpg | shuf -n20] \n]
foreach jpg $ii {
array set exif [::jpeg::getExif $jpg]
puts "max f-stop: [::jpeg::formatExif [list MaxAperture $exif(MaxAperture)]]"
}
,,,
Writing Or Saving Images ‹↑›
convert a jpeg to png format
package require Img
image create photo myJpeg -file "image.jpeg"
pack [label .l -image myJpeg] ;
myJpeg write "image.png" -format PNG
Imagemagick And Tcl Tk ‹↑›
It is possible to use the imagemagick image manipulation software
from within tcl/tk. This provides many additional capabilities for
tcl/tk programs to transform images.
Gotchas For Images ‹↑›
When you create an image with something like ....
image create photo im -file "image.jpeg"
Then "im" is not a variable, it is a command name, so you
dont reference it with "$im", just with "im". And you
pass it to procedures in the same way eg
[Shrink im 4]
Dashed Lines ‹↑›
Not all dash patterns are supported on all platforms (operating systems).
create a line with a 6 pixel dash and a 2 pixel space
pack [canvas .c] ; .c create line -dash {6 2 12 6}
create a line with 6px dash, 2 px space, 12 px dash, 6, px space
pack [canvas .c] ; .c create line -dash {6 2 12 6}
dash patterns
. - dash 1/2 length of following space |
| - dash equal in length to following space |
- - dash 1 1/2 times longer than space |
_ - twice as long as following space |
<space> - longer space |
Colours ‹↑›
Ansi Escape Sequences For Colours ‹↑›
In a text only Tcl script we can use ansi sequences to print a
limited number of colours. We use the standard ansi sequences
but also need to escape the [ character which occurs in them
(since it is special to the tclsh interpreter)
print red text on a terminal with ansi escape sequences
puts "\033\[01;31m Red Text"
red followed by normal colour text
puts "\033\[01;31m Red\033\[0m Normal"
print a colourful prompt and save to variable
puts -nonewline "\033\[01;31m Enter Name: \033\[0m"
flush stdout
set name [gets stdin]
puts "Hello $name"
use variables to create ansi colours
---------
set greenongray "\033\[0;32;40m"
set red "\033\[1;31m"
puts "${red}Red Bold ${greenongray}Green On Gray"
,,,
using string map for colours
set cc {<g> \033\[01;32m <b> \033\[01;34m <c> \033\[01;36m}
puts [string map $cc "<b> blue <c> cyan <g> green"]
Below is a simple an useful 'colour printing' proceedure
using string map etc
define a colour puts proc and use it
proc cputs {s} {
set cc {
<g> \033\[01;32m <b> \033\[01;34m <c> \033\[01;36m
<u> \033\[4;36m
<x> \033\[24m
}
puts [string map $cc $s]
}
cputs "<b> Blue and <g> Green <u>underline<x>
underline off"
[1m underline [24m underline off
all colours
--------
Text attributes
0 All attributes off
1 Bold on
4 Underscore (on monochrome display adapter only)
5 Blink on
7 Reverse video on
8 Concealed on
Foreground colors
30 Black
31 Red
32 Green
33 Yellow
34 Blue
35 Magenta
36 Cyan
37 White
Background colors
40 Black
41 Red
42 Green
43 Yellow
44 Blue
45 Magenta
46 Cyan
47 White
,,,
Tk Colours ‹↑›
on a unix system, show all defined tk colour names
man 3tk colors
showrgb
show all named colors in a listbox
--------
wm attributes . -fullscreen 1
set tt [exec man 3tk colors 2>/dev/null]
set cc [split $tt \n]
set cc [lreplace $cc 0 13]
listbox .l -listvar cc -height 20 -font {-size 14}
pack .l -fill x
bind . x exit
,,,
not working, tricky
show all named colors in colourful labels
--------
wm attributes . -fullscreen 1
set tt [exec man 3tk colors 2>/dev/null]
set cc [split $tt \n]
set cc [lreplace $cc 0 13]
foreach item $cc {
string trim $item;
set colour [lindex [split $item " "] 0]
puts $colour
catch {eval {label .l$colour -bg $colour -text $colour}}
catch {pack .l$colour}
}
bind . x exit
,,,
Rgb Colours ‹↑›
Rgb as you may guess means red green blue
example rgb values
#FF8080 #F80
Random Colours ‹↑›
This is a nice test pattern
create random colours
-------
proc randomColor {} {format #%06x [expr {int(rand() * 0xFFFFFF)}]}
set ii 0
while {$ii < 40} {
foreach n {a b c d e} {
frame .$n$ii -width 100 -height 20 -bg [randomColor]
}
grid .a$ii .b$ii .c$ii .d$ii .e$ii
incr ii
}
bind . x exit
,,,,
Named Colours ‹↑›
convert a colour name in rgb values (a list)
winfo rgb . red
convert a colour name into a valid rgb format
eval format "#%04x%04x%04x" [winfo rgb . red]
Modifying Colours ‹↑›
This does something but not sure what.
darken a colour slightly
------
set color orange
frame .f -width 100 -height 20 -bg $color
pack .f
set new [::tk::Darken $color 1000]
frame .g -width 100 -height 20 -bg $new
pack .g
,,,
Exec And Pipes ‹↑›
an exec example
set n [exec sort < /etc/passwd | uniq | wc -l 2> /dev/null]
pipe both stdout and stderr to stdout
exec find . -name '*aa*' |& cat
display the unix dict file in a text box
pack [text .t -height 20 -width 80]
.t insert 1.0 [exec cat /usr/share/dict/words]
,,,
display all debian linux packages in a list box
set pp [split [exec apt-cache search e] \n]
pack [listbox .t -height 20 -width 80 -listvariable pp]
,,,
display files from a directory in a text box using a pipe
set f [open "|ls" r]
set s [read $f]
close $f
pack [text .t -height 20 -width 80]
.t insert 1.0 $s
,,,
display the 1st 10 text files in a listbox
pack [listbox .t -height 20 -width 80 -listvariable results]
catch {exec locate "*.txt" | head -10 } results
,,,
The recipe above illustrates a tcl 'exec' problem,
"write on pipe with no readers". This occurs when a process that
takes a long time (in this case 'locate') writes to a process
that exits quickly ('head').
spawn a shell to get rid of this problem
set entropyfeed [exec sh -c {tr -dc '[:graph:]' < /dev/urandom | head -c 320}]
a minimalist email sender via exec and mutt
------
set recipient "mjbishop@fastmail.fm"
set about "stuff"
label .l -text Email
text .t
entry .to -textvar recipient
entry .sub -textvar about
pack .l .to .sub .t -fill x -padx 2 -pady 2; focus .t
bind .l x exit
bind .l s {
set message [.t get 1.0 {end -1c}]
puts "
Sending mail to <$recipient>
Subject:$about
Message:\n$message"
exec echo "$message" | mutt -s "$about" $recipient
}
bind . xxx { focus .l }
,,,
Errors With Exec ‹↑›
When a command executed with 'exec' writes to stderr
or returns a non zero result, exec throws an error.
Which is inconvenient, but can be overcome by calling
exec within a catch clause.
redirect stderr from an exec call
puts [exec man 3tk colors | cat 2>/dev/null]
another way to redirect stdout and stderr
exec man 3tk colors &| less
Gotchas With Exec ‹↑›
With long pipes with exec you get a problem with the message
"child killed: write on pipe with no readers"
set f [exec locate -i *.jpg | shuf | head -1 ]
The recipe above illustrates a tcl 'exec' problem, "write on pipe with no
readers". This occurs when a process that takes a long time (in this case
'locate') writes to a process that exits quickly ('head'). The solution
may be to spawn a sub bash/sh shell
spawn a shell to get rid of probs
set entropyfeed [exec sh -c {tr -dc '[:graph:]' < /dev/urandom | head -c 320}]
Killing Processes ‹↑›
open a process and then kill it later
set pipe [open "|mplayer a.wav"]
set p [pid $pipe]
exec kill $p
Printing ‹↑›
But where can we find this package?
an example of using the pdf4tcl package
--------
package require pdf4tcl
pdf4tcl::new mypdf -paper a3
mypdf startPage
mypdf setFont 12 Courier
mypdf text "Hejsan" -x 50 -y 50
mypdf write -file mypdf.pdf
mypdf destroy
,,,
Sound And Audio ‹↑›
play a random wav file
-------
set ii [exec locate -i *.wav | shuf -n1]
puts "file is $ii"
exec play $ii
,,,
a listbox which plays an mp3 when enter is pressed
set ll [split [exec locate "*.mp3"] \n]
pack [listbox .l -width 80 -listvariable ll -font {-size 16}]
bind all <Return> {
exec mplayer [.l get active] &
}
bind all x exit
,,,
play audio files with spaces in the name
set pipe [open "|mplayer \"[.l get active]\""]
play and stop playing wavs and mp3s
wm attributes . -zoomed 1
set ll [split [exec locate "*.mp3" "*.wav"] \n]
wm title . "Found [llength $ll] audio files"
pack [listbox .l -listvariable ll -font {-size 16}] -fill both -expand 1
focus .l
bind all <Return> {
set pipe [open "|mplayer \"[.l get active]\""]
set p [pid $pipe]
}
bind all s { exec kill $p }
bind all j { event generate .l <Down> }
bind all x exit
,,,
a simple app to play and delete audio
# would be good to make it searchable
wm attributes . -zoomed 1
. config -borderwidth 20
font create ff -size 14; option add *font ff
set ll [split [exec locate "*.mp3" "*.wav"] \n]
set help \
"Enter - play, s - stop, i - info, d - delete (careful!), + vi keys"
wm title . "Found [llength $ll] audio files: $help"
pack [listbox .l -listvariable ll -font ff] -fill both -expand 1
focus .l
bind all <Return> {
set pipe [open "|mplayer \"[.l get active]\""]
set p [pid $pipe]
wm title . "Playing audio [.l get active]"
}
bind all s {
exec kill $p
wm title . "Stopped playing [.l get active] audio"
}
bind all i {
set f [.l get active]
wm title . "File: [file tail $f], size [file size $f]"
}
bind all d {
set f [.l get active]
set index [.l curselection]
set l [lreplace $ll $index $index]
file delete $f
wm title . "Deleted $f audio [file size $f]b, hope thats OK!"
}
bind all j { event generate .l <Down> }
bind all J { event generate .l <Next> }
bind all k { event generate .l <Up> }
bind all K { event generate .l <Prior> }
bind all G { event generate .l <Control-Home> }
bind all gg { event generate .l <Control-End> }
bind all h {
wm title . "[llength $ll] audio files: $help "
}
bind . z { font conf ff -size [expr [font conf ff -size]+2] }
bind . Z { font conf ff -size [expr [font conf ff -size]-2] }
bind all x exit
,,,
Snack is a good library, designed to be used with scripting
languages such as tcl/tk
install the snack library for tcl tk on a debian system
sudo apt-get install libsnack2-alsa
sudo apt-get install libsnack2 older...
May have to uninstall libsnack2 before installing
libsnack2-alsa. libsnack2 is something called oss which
is older apparently
use the snack sound library with tcl/tk
package require snack
May have to change /dev/dsp if using alsa module
maybe hw,0:0 ???. The code is running with alsa
without errors but no sound is heard
play a wav sound file, using snack
package require snack
#snack::audio selectOutput /dev/dsp
snack::sound s
set f [exec locate -i *.mp3 | shuf -n1]
puts "playing $f"
#s read /home/matth3wbishop/yolngu/audio/one/0234s.mp3
# s read snack2.2a1/demos/tcl/ex1.wav
s read $f
s play
the same
package require snack
snack::sound s -load /home/mjb/sf/htdocs/audio/ja/tsuma.wav
s play
snack can convert formats, can play mp3s etc
Wav form drawing is working
draw the waveform of a sound
package require snack
snack::sound snd
snd read /home/mjb/sf/htdocs/audio/ja/tsuma.wav
#snd play
canvas .c; pack .c
.c create waveform 0 0 -sound snd
Exec Gotchas ‹↑›
If the external program writes to 'standard error' (stderr)
then you need to use catch
catch {exec wget ...} results
dont enclose the whole exec in " quotes (like a c 'system' call
or a perl 'exec')
exec "blah ..." ## error
Dont use single quotes (') they dont work
Environment And Platform ‹↑›
see help for environment variables
man 3tcl tclvars
show the architecture of machine
puts $tcl_platform(machine)
show some computer specific information
---------
puts "
Architecture of machine: $tcl_platform(machine)
Operating System: $tcl_platform(os)
Os version: $tcl_platform(osVersion)
Is Tcl/tk threaded: $tcl_platform(threaded)
User: $tcl_platform(user)
"
,,,
Cross Platform Issues ‹↑›
Android ‹↑›
The androwish app for android appears very promising,
actively developed (oct 2015) and even with some advantages
over traditional platforms (aliased drawing for example)
Deployment ‹↑›
Use tclkit to make a stand alone tcl/tk app
Investigate starkits and starpacks
The 'freewrap' tool claims to be able to wrap tcl/tk apps as
stand alone executables for windows and linux. This seems to good
to be true, since the target machine doesnt have to have wish or
tclsh installed in order to run the app. active dev but no deb package
http://freewrap.sourceforge.net/
Email And Tcl Tk ‹↑›
look at exmh a tcl/tk interface for the unix mh mail system.
written by brent welch
Bash And Tcl Tk ‹↑›
Since tcl and tk are interpreters they can be used directly
from bash by piping input to 'wish' and 'tclsh'
echo hello to standard output
echo "puts hello" | tclsh
get an iso formatted date from a date string
echo "puts [clock format [clock scan {tomorrow}]]" | wish
converting a bash date string to eg: Friday, 14 August 2014
a=today; echo 'puts [clock format [clock scan "'$a'"] -format {%A, %d %B %Y}]' | tclsh
test if a string is today in bash
------
a=$(echo 'puts [string equal [clock format [clock scan "today"] -format {%d%B%Y}] [clock format [clock scan "15aug2015"] -format {%d%B%Y}] ]' | tclsh); if [ "$a" == "0" ]; then echo no; fi
,,,
execute a tcl statement using echo & eval from the bash shell
eval echo 'wm title . "hi"' | wish
The technique below is useful for including short tcl/tk
scripts within a bash script file
use a 'here document' to execute some tk commands
wish << xx
wm title . "hello"
button .b -text push -command {exit}
pack .b
xx
another way to run a here document
cat << eos | tclsh
puts "Seconds: [clock seconds]"
eos
a vim command to execute in bash
command! Bmr ?^ *---?+1,/^ *,,,/-1w !sed 's/^ *>>//' | bash | less
Vim And Tcl Tk ‹↑›
Since vim is the editor I am used to I find it easier to
use than editing in a tcl/tk text widget
Using Vim From With Tcl Tk ‹↑›
The code below is not working because vim needs a terminal to run,
so how can we use vim from within tcl/tk???
launch vim
-----
set typelist {{"Text File" {".txt"}} }
button .btn -text "Choose a text file" -command {
set f [tk_getOpenFile -filetypes $typelist]
exec bash -c vim $f
}
pack .btn -padx 20 -pady 20
,,,
Vim Commands ‹↑›
execute the current file as a tcl or tk script (from within 'vim')
:!./%
start vim in a particular folder
vim -c "cd C:/pro"
start vim and change to folder of file being edited
vim -c 'cd %:p:h' ~/docs/info.txt
execute the current line as a tcl/tk script
:.w !wish
Below is a one line tcl/tk script piped to wish and
set up as a map in the .vimrc file.
a mapping to show 'keysyms' or key code, for events
map ,k :!echo "bind . <Key> {puts \%K}" \| wish <cr>
another version, easier to exit
map ,k :!echo "bind . <Key> {puts \%K};bind . e {exit}" \| wish <cr>
make a mapping to execute current line as a tk script
map ,dd :.w !sed 's/^ *>>//' \| wish
remove the 2 '>' characters and execute current line as a tk command
:.w !sed 's/^ *>>//' | wish
write out a fragment to a file, overwriting if file exists
comm! App ?^ *---?+1,/^ *,,,/-1w! app/new.tcl
a command to execute fragment between '---' and ',,,' with bash
command! Bmr ?^ *---?+1,/^ *,,,/-1w !bash | less
make a mapping to execute current line as a tcl script
map ,tc :.w !sed 's/^ *>>//' \| tclsh
make a mapping to execute current line as a tk script
map ,tk :.w !sed 's/^ *>>//' \| wish
execute between --- and ,,, as tk commands
command! Tk ?^ *---?+1,/^ *,,,/-1w !wish
execute the fragment between --- and ,,, as a tcl script
command! Tc ?^ *---?+1,/^ *,,,/-1w !tclsh
execute the current line as a tcl command
command! Tcl .w !sed 's/^ *>>//' | tclsh
create vim maps to show help for tcl/tk keywork, widget etc
nmap ,htk :!man 3tk <cword> <cr>
nmap ,htc :!man 3tcl <cword> <cr>
a map to indent tcl code fragments
map ,ti :?^ *---?+1,/^ *,,,/-1! astyle -s2 <bar> sed 's/^/ /'<cr>
Vimrc Example Code ‹↑›
a set of useful vim maps and commands to go in vimrc
" a mapping to show tk keysyms or codes
map ,k :!echo "bind . <Key> {puts \%K}" \| wish <cr>
" execute the current file as Tk or Tcl
command! Tkx 1,$w ! wish
command! Tcx 1,$w ! tclsh
"show the tk/tcl man page for the word under cursor
"help must be installed
nmap ,htk :!man 3tk <cword> <cr>
nmap ,htc :!man 3tcl <cword> <cr>
command! Htk !man 3tk <cword> <cr>
command! Htc !man 3tcl <cword> <cr>
" a command to sometimes show valid options
command! Hto !echo "catch {<cword> -xx} r; puts [lindex [split \$r :] 1]" | wish
" a vim command to display all tcl/tk commands
command! Hcc !echo "puts [lsort [info commands]]; bind . x exit" | wish
"a mapping to execute current line as a tk script
map ,tc :.w !sed 's/^ *>>//' \| tclsh
map ,tk :.w !sed 's/^ *>>//' \| wish
" commands to execute tcl/tk fragments
command! Tc ?^ *---?+1,/^ *,,,/-1w ! tclsh
command! Tcl .w !sed 's/^ *>>//' | tclsh
command! Tk ?^ *---?+1,/^ *,,,/-1w ! wish
command! Tkl .w !sed 's/^ *>>//' | wish
" write a temporary file and then run
command! Tc ?^ *---?+1,/^ *,,,/-1w test.tcl !tclsh test.tcl
" export a fragment (recipe) as an app
comm! -nargs=1 App ?^ *---?+1,/^ *,,,/-1w app/<args>
comm! -nargs=1 AppOver ?^ *---?+1,/^ *,,,/-1w! app/<args>
" append a fragment to the app script file
comm! -nargs=1 AppA ?^ *---?+1,/^ *,,,/-1w >>app/<args>
Performance ‹↑›
see time and date for a simple way to use [clock milliseconds] to get
some speed performance info
Memory Usage ‹↑›
to see how much memory some tcl/tk code uses you could try
comparing the vsize (as reported by ps) of tclsh before and after
evaluating the code
an example
---------
package require Img
puts [exec ps p [exec pidof wish] -o vsize]
set ll [split [exec locate -i *.jpg] \n]
set f [lindex $ll [expr { int([llength $ll] * rand()) }]]
puts "File size: [file size $f]"
image create photo img -file $f
label .l -image img
pack .l
puts [exec ps p [exec pidof wish] -o vsize]
bind . x exit
,,,
Debugging ‹↑›
Wish can give you better error messages than tclsh even when your app
is only tcl!!! So if you have an intractable buggy program try
running it in wish.
use 'cget' to get current option values: this may
simplify some existing code.
tk_setPalette changes the colour scheme for the whole app
use lset to set 2 variables at once from a split etc
lset {a, b} [split "one:two" :]
Use Tkhtml 3.0 to display labels with formatted text
amongst other uses.
Use tclsh on ms windows to display tk interfaces, because
tclsh outputs puts to the console (command line) where as
wish returns immediately. This can be handy for debugging
purposes.
DOCUMENT-NOTES: