Tcl 编程/Tk 示例
以下示例最初出现在 Tcler 的 Wiki http://wiki.tcl.tk 。它们都属于公有领域 - 无权利保留。
这个有趣的小程序生成随机的烹饪食谱。虽然它很小,但它可以生成 900 种不同的食谱,虽然它们可能不符合每个人的口味...... 基本的想法是从列表中选择一个任意元素,这在 Tcl 中很容易做到,方法如下
proc ? L {lindex $L [expr {int(rand()*[llength $L])}]}
这在以下几个地方被多次使用
proc recipe {} {
set a {
{3 eggs} {an apple} {a pound of garlic}
{a pumpkin} {20 marshmallows}
}
set b {
{Cut in small pieces} {Dissolve in lemonade}
{Bury in the ground for 3 months}
{Bake at 300 degrees} {Cook until tender}
}
set c {parsley snow nutmeg curry raisins cinnamon}
set d {
ice-cream {chocolate cake} spinach {fried potatoes} rice {soy sprouts}
}
return " Take [? $a].
[? $b].
Top with [? $c].
Serve with [? $d]."
}
并且由于现代程序总是需要一个 GUI,这里有一个最小的 GUI,它会在您在顶层源代码此文件时出现,并且每次单击它时都会显示一个新的食谱
if {[file tail [info script]]==[file tail $argv0]} {
package require Tk
pack [text .t -width 40 -height 5]
bind .t <1> {showRecipe %W; break}
proc showRecipe w {
$w delete 1.0 end
$w insert end [recipe]
}
showRecipe .t
}
尽情享用!
这是一个时钟,可以显示模拟或数字时间 - 只需单击它即可切换。
#!/usr/bin/env tclsh
package require Tk
proc every {ms body} {eval $body; after $ms [info level 0]}
proc drawhands w {
$w delete hands
set secSinceMidnight [expr {[clock sec]-[clock scan 00:00:00]}]
foreach divisor {60 3600 43200} length {45 40 30} width {1 3 7} {
set angle [expr {$secSinceMidnight * 6.283185 / $divisor}]
set x [expr {50 + $length * sin($angle)}]
set y [expr {50 - $length * cos($angle)}]
$w create line 50 50 $x $y -width $width -tags hands
}
}
proc toggle {w1 w2} {
if [winfo ismapped $w2] {
foreach {w2 w1} [list $w1 $w2] break ;# swap
}
pack forget $w1
pack $w2
}
#-- Creating the analog clock:
canvas .analog -width 100 -height 100 -bg white
every 1000 {drawhands .analog}
pack .analog
#-- Creating the digital clock:
label .digital -textvar ::time -font {Courier 24}
every 1000 {set ::time [clock format [clock sec] -format %H:%M:%S]}
bind . <1> {toggle .analog .digital}
画布的弧形元素默认情况下被渲染为饼形切片(圆周的一部分,通过半径线连接到中心。因此,生成饼图非常容易。以下代码稍微复杂一些,因为它还确定了饼图标签的位置
proc piechart {w x y width height data} {
set coords [list $x $y [expr {$x+$width}] [expr {$y+$height}]]
set xm [expr {$x+$width/2.}]
set ym [expr {$y+$height/2.}]
set rad [expr {$width/2.+20}]
set sum 0
foreach item $data {set sum [expr {$sum + [lindex $item 1]}]}
set start 270
foreach item $data {
foreach {name n color} $item break
set extent [expr {$n*360./$sum}]
$w create arc $coords -start $start -extent $extent -fill $color
set angle [expr {($start-90+$extent/2)/180.*acos(-1)}]
set tx [expr $xm-$rad*sin($angle)]
set ty [expr $ym-$rad*cos($angle)]
$w create text $tx $ty -text $name:$n -tag txt
set start [expr $start+$extent]
}
$w raise txt
}
测试
pack [canvas .c -bg white]
piechart .c 50 50 150 150 {
{SPD 199 red}
{CDU 178 gray}
{CSU 23 blue}
{FDP 60 yellow}
{Grüne 58 green}
{Linke 55 purple}
}
以下脚本在画布上显示一个条形图,带有伪 3D 条形 - 前面的矩形按指定方式绘制,并用两个多边形装饰 - 一个用于顶部,一个用于侧面:}
proc 3drect {w args} {
if [string is int -strict [lindex $args 1]] {
set coords [lrange $args 0 3]
} else {
set coords [lindex $args 0]
}
foreach {x0 y0 x1 y1} $coords break
set d [expr {($x1-$x0)/3}]
set x2 [expr {$x0+$d+1}]
set x3 [expr {$x1+$d}]
set y2 [expr {$y0-$d+1}]
set y3 [expr {$y1-$d-1}]
set id [eval [list $w create rect] $args]
set fill [$w itemcget $id -fill]
set tag [$w gettags $id]
$w create poly $x0 $y0 $x2 $y2 $x3 $y2 $x1 $y0 \
-fill [dim $fill 0.8] -outline black
$w create poly $x1 $y1 $x3 $y3 $x3 $y2 $x1 $y0 \
-fill [dim $fill 0.6] -outline black -tag $tag
}
为了更具塑性效果,多边形的填充颜色亮度降低(“变暗”)
proc dim {color factor} {
foreach i {r g b} n [winfo rgb . $color] d [winfo rgb . white] {
set $i [expr int(255.*$n/$d*$factor)]
}
format #%02x%02x%02x $r $g $b
}
为 y 轴绘制一个简单的刻度,并返回缩放因子
proc yscale {w x0 y0 y1 min max} {
set dy [expr {$y1-$y0}]
regexp {([1-9]+)} $max -> prefix
set stepy [expr {1.*$dy/$prefix}]
set step [expr {$max/$prefix}]
set y $y0
set label $max
while {$label>=$min} {
$w create text $x0 $y -text $label -anchor w
set y [expr {$y+$stepy}]
set label [expr {$label-$step}]
}
expr {$dy/double($max)}
}
一个有趣的子挑战是粗略地对数字进行四舍五入,保留 1 或最多 2 个有效数字 - 默认情况下向上舍入,添加 "-" 向下舍入
proc roughly {n {sgn +}} {
regexp {(.+)e([+-])0*(.+)} [format %e $n] -> mant sign exp
set exp [expr $sign$exp]
if {abs($mant)<1.5} {
set mant [expr $mant*10]
incr exp -1
}
set t [expr round($mant $sgn 0.49)*pow(10,$exp)]
expr {$exp>=0? int($t): $t}
}
所以这里是我的小条形图生成器。给定画布路径名、边界矩形和要显示的数据(一个 {name value color} 三元组列表),它会计算出几何图形。首先绘制一个灰色的“地面”。注意,负值用 "d"(赤字)标记,因此它们看起来像是“穿过平面”下降的。
proc bars {w x0 y0 x1 y1 data} {
set vals 0
foreach bar $data {
lappend vals [lindex $bar 1]
}
set top [roughly [max $vals]]
set bot [roughly [min $vals] -]
set f [yscale $w $x0 $y0 $y1 $bot $top]
set x [expr $x0+30]
set dx [expr ($x1-$x0-$x)/[llength $data]]
set y3 [expr $y1-20]
set y4 [expr $y1+10]
$w create poly $x0 $y4 [expr $x0+30] $y3 $x1 $y3 [expr $x1-20] $y4 -fill gray65
set dxw [expr $dx*6/10]
foreach bar $data {
foreach {txt val col} $bar break
set y [expr {round($y1-($val*$f))}]
set y1a $y1
if {$y>$y1a} {swap y y1a}
set tag [expr {$val<0? "d": ""}]
3drect $w $x $y [expr $x+$dxw] $y1a -fill $col -tag $tag
$w create text [expr {$x+12}] [expr {$y-12}] -text $val
$w create text [expr {$x+12}] [expr {$y1a+2}] -text $txt -anchor n
incr x $dx
}
$w lower d
}
一般有用的辅助函数
proc max list {
set res [lindex $list 0]
foreach e [lrange $list 1 end] {
if {$e>$res} {set res $e}
}
set res
}
proc min list {
set res [lindex $list 0]
foreach e [lrange $list 1 end] {
if {$e<$res} {set res $e}
}
set res
}
proc swap {_a _b} {
upvar 1 $_a a $_b b
foreach {a b} [list $b $a] break
}
测试整个程序(参见截图)
pack [canvas .c -width 240 -height 280]
bars .c 10 20 240 230 {
{red 765 red}
{green 234 green}
{blue 345 blue}
{yel-\nlow 321 yellow}
{ma-\ngenta 567 magenta}
{cyan -123 cyan}
{white 400 white}
}
.c create text 120 10 -anchor nw -font {Helvetica 18} -text "Bar Chart\nDemo"
这里有一个小的 Tcl/Tk 计算器。除了屏幕上的按钮外,您还可以通过键盘输入使用 expr 的任何其他功能。
package require Tk
wm title . Calculator
grid [entry .e -textvar e -just right] -columnspan 5
bind .e <Return> =
set n 0
foreach row {
{7 8 9 + -}
{4 5 6 * /}
{1 2 3 ( )}
{C 0 . = }
} {
foreach key $row {
switch -- $key {
= {set cmd =}
C {set cmd {set clear 1; set e ""}}
default {set cmd "hit $key"}
}
lappend keys [button .[incr n] -text $key -command $cmd]
}
eval grid $keys -sticky we ;#-padx 1 -pady 1
set keys [list]
}
grid .$n -columnspan 2 ;# make last key (=) double wide
proc = {} {
regsub { =.+} $::e "" ::e ;# maybe clear previous result
if [catch {set ::res [expr [string map {/ *1.0/} $::e]]}] {
.e config -fg red
}
append ::e = $::res
.e xview end
set ::clear 1
}
proc hit {key} {
if $::clear {
set ::e ""
if ![regexp {[0-9().]} $key] {set ::e $::res}
.e config -fg black
.e icursor end
set ::clear 0
}
.e insert end $key
}
set clear 0
focus .e ;# allow keyboard input
wm resizable . 0 0
而且,正如 Cameron Laird 指出的那样,这个东西甚至可以编程:例如,输入
[proc fac x {expr {$x<2? 1: $x*[fac [incr x -1]]}}]
到输入框中,忽略警告;现在您可以执行
[fac 10]
并收到 [fac 10] = 3628800.0 作为结果...
计算尺是一种模拟的机械设备,用于近似工程计算,在 20 世纪 70 年代到 80 年代左右被袖珍计算器淘汰。基本原理是,乘法是通过加对数来完成的,因此大多数刻度是对数的,具有不均匀的增量。
这个有趣的项目用一个白色的“机身”和一个米色的“滑块”来重新创建一个计算尺(大约是 Aristo-Rietz Nr. 89,带有 7 个刻度 - 高级计算尺最多有 24 个刻度),您可以用单击的鼠标按钮 1 将其向左或向右移动,也可以用 <Shift-Left>/<Shift-Right> 方向键以像素增量移动。最后,蓝色的线代表“标记”(这个东西正确的说法是什么?“游标”?“滑块”?),您可以用鼠标在整个东西上移动它来读取一个值。用 <Left>/<Right> 键进行微调。
由于舍入误差(整数像素),这个玩具甚至比实体计算尺的精度还要低,但也许您仍然会喜欢这些回忆...... 截图显示了我是如何发现 3 乘以 7 大约是 21 的......(查看 A 和 B 刻度)。
proc ui {} {
set width 620
pack [canvas .c -width $width -height 170 -bg white]
pack [label .l -textvariable info -fg blue] -fill x
.c create rect 0 50 $width 120 -fill grey90
.c create rect 0 50 $width 120 -fill beige -outline beige \
-tag {slide slidebase}
.c create line 0 0 0 120 -tag mark -fill blue
drawScale .c K x3 10 5 5 log10 1 1000 186.6666667
drawScale .c A x2 10 50 -5 log10 1 100 280
drawScale .c B x2 10 50 5 log10 1 100 280 slide
drawScale .c CI 1/x 10 90 -5 -log10 1 10 560 slide
drawScale .c C x 10 120 -5 log10 1 10 560 slide
drawScale .c D x 10 120 5 log10 1 10 560
drawScale .c L "lg x" 10 160 -5 by100 0 10 5600
bind .c <Motion> {.c coords mark %x 0 %x 170; set info [values .c]}
bind .c <1> {set x %x}
bind .c <B1-Motion> {%W move slide [expr {%x-$x}] 0; set x %x}
bind . <Shift-Left> {.c move slide -1 0; set info [values .c]}
bind . <Shift-Right> {.c move slide 1 0; set info [values .c]}
bind . <Left> {.c move mark -1 0; set info [values .c]}
bind . <Right> {.c move mark 1 0; set info [values .c]}
}
proc drawScale {w name label x y dy f from to fac {tag {}}} {
set color [expr {[string match -* $f]? "red": "black"}]
$w create text $x [expr $y+2*$dy] -text $name -tag $tag -fill $color
$w create text 600 [expr $y+2*$dy] -text $label -tag $tag -fill $color
set x [expr {[string match -* $f]? 580: $x+10}]
set mod 5
set lastlabel ""
set lastx 0
for {set i [expr {$from*10}]} {$i<=$to*10} {incr i} {
if {$i>100} {
if {$i%10} continue ;# coarser increments
set mod 50
}
if {$i>1000} {
if {$i%100} continue ;# coarser increments
set mod 500
}
set x0 [expr $x+[$f [expr {$i/10.}]]*$fac]
set y1 [expr {$i%(2*$mod)==0? $y+2.*$dy: $i%$mod==0? $y+1.7*$dy: $y+$dy}]
set firstdigit [string index $i 0]
if {$y1==$y+$dy && abs($x0-$lastx)<2} continue
set lastx $x0
if {$i%($mod*2)==0 && $firstdigit != $lastlabel} {
$w create text $x0 [expr $y+3*$dy] -text $firstdigit \
-tag $tag -font {Helvetica 7} -fill $color
set lastlabel $firstdigit
}
$w create line $x0 $y $x0 $y1 -tag $tag -fill $color
}
}
proc values w {
set x0 [lindex [$w coords slidebase] 0]
set x1 [lindex [$w coords mark] 0]
set lgx [expr {($x1-20)/560.}]
set x [expr {pow(10,$lgx)}]
set lgxs [expr {($x1-$x0-20)/560.}]
set xs [expr {pow(10,$lgxs)}]
set res K:[format %.2f [expr {pow($x,3)}]]
append res " A:[format %.2f [expr {pow($x,2)}]]"
append res " B:[format %.2f [expr {pow($xs,2)}]]"
append res " CI:[format %.2f [expr {pow(10,-$lgxs)*10}]]"
append res " C:[format %.2f $xs]"
append res " D:[format %.2f $x]"
append res " L:[format %.2f $lgx]"
}
proc pow10 x {expr {pow(10,$x)}}
proc log10 x {expr {log10($x)}}
proc -log10 x {expr {-log10($x)}}
proc by100 x {expr {$x/100.}}
#--------------------------------
ui
bind . <Escape> {exec wish $argv0 &; exit}
这里有一个微小的但完整的脚本,允许在画布小部件上涂鸦(用鼠标绘制)
proc doodle {w {color black}} {
bind $w <1> [list doodle'start %W %x %y $color]
bind $w <B1-Motion> {doodle'move %W %x %y}
}
proc doodle'start {w x y color} {
set ::_id [$w create line $x $y $x $y -fill $color]
}
proc doodle'move {w x y} {
$w coords $::_id [concat [$w coords $::_id] $x $y]
}
pack [canvas .c -bg white] -fill both -expand 1
doodle .c
bind .c <Double-3> {%W delete all}
这里它又来了,但这次有了解释
如果您想要这种正式的语言,它的“应用程序编程接口”(API)是 doodle 命令,您可以在其中指定哪个画布小部件应该启用涂鸦,以及用什么颜色(默认为黑色):}
proc doodle {w {color black}} {
bind $w <1> [list doodle'start %W %x %y $color]
bind $w <B1-Motion> {doodle'move %W %x %y}
}
它为画布注册了两个绑定,一个 (<1>) 用于单击鼠标左键,另一个用于在按住鼠标左键(1)的情况下移动鼠标。这两种绑定都只是分别调用一个内部函数。
在单击鼠标左键时,将在画布上以指定的填充颜色创建一个线条项目,但它还没有长度,因为起点和终点重合。项目 ID(由画布分配的一个数字)保存在一个全局变量中,因为它必须在该过程返回后很久才能持续存在
proc doodle'start {w x y color} {
set ::_id [$w create line $x $y $x $y -fill $color]
}
左键移动过程获取全局已知的涂鸦线条对象的坐标(交替的 x 和 y),将当前坐标附加到它,并将其设为新的坐标 - 换句话说,将线条扩展到当前鼠标位置
proc doodle'move {w x y} {
$w coords $::_id [concat [$w coords $::_id] $x $y]
}
这就是我们实现涂鸦所需的一切 - 现在让我们创建一个画布来测试它,并将其打包,这样它就可以根据您的需要绘制得很大
pack [canvas .c -bg white] -fill both -expand 1
这行代码打开了上面创建的涂鸦功能(默认为黑色)
doodle .c
为双击鼠标右键/双击按钮 3 添加一个绑定,以清除画布(由 MG 添加,2004 年 4 月 29 日)
bind .c <Double-3> {%W delete all}
这里有一个在画布上的微小的绘图程序。顶部的单选按钮允许选择绘制模式和填充颜色。在“移动”模式下,您当然可以四处移动项目。右键单击项目以将其删除。
单选按钮是一个明显的“超级小部件”,用于容纳一行单选按钮。这个简单的单选按钮允许文本或颜色模式:}
proc radio {w var values {col 0}} {
frame $w
set type [expr {$col? "-background" : "-text"}]
foreach value $values {
radiobutton $w.v$value $type $value -variable $var -value $value \
-indicatoron 0
if $col {$w.v$value config -selectcolor $value -borderwidth 3}
}
eval pack [winfo children $w] -side left
set ::$var [lindex $values 0]
set w
}
根据绘制模式,鼠标事件“按下”和“移动”具有不同的处理程序,它们由看起来像数组元素的名称调度。因此,对于模式 X,我们需要一对过程,down(X) 和 move(X)。调用之间使用的值保存在全局变量中。
首先,自由手绘线绘制的处理程序
proc down(Draw) {w x y} {
set ::ID [$w create line $x $y $x $y -fill $::Fill]
}
proc move(Draw) {w x y} {
$w coords $::ID [concat [$w coords $::ID] $x $y]
}
#-- Movement of an item
proc down(Move) {w x y} {
set ::ID [$w find withtag current]
set ::X $x; set ::Y $y
}
proc move(Move) {w x y} {
$w move $::ID [expr {$x-$::X}] [expr {$y-$::Y}]
set ::X $x; set ::Y $y
}
#-- Clone an existing item
proc serializeCanvasItem {c item} {
set data [concat [$c type $item] [$c coords $item]]
foreach opt [$c itemconfigure $item] {
# Include any configuration that deviates from the default
if {[lindex $opt end] != [lindex $opt end-1]} {
lappend data [lindex $opt 0] [lindex $opt end]
}
}
return $data
}
proc down(Clone) {w x y} {
set current [$w find withtag current]
if {[string length $current] > 0} {
set itemData [serializeCanvasItem $w [$w find withtag current]]
set ::ID [eval $w create $itemData]
set ::X $x; set ::Y $y
}
}
interp alias {} move(Clone) {} move(Move)
#-- Drawing a rectangle
proc down(Rect) {w x y} {
set ::ID [$w create rect $x $y $x $y -fill $::Fill]
}
proc move(Rect) {w x y} {
$w coords $::ID [lreplace [$w coords $::ID] 2 3 $x $y]
}
#-- Drawing an oval (or circle, if you're careful)
proc down(Oval) {w x y} {
set ::ID [$w create oval $x $y $x $y -fill $::Fill]
}
proc move(Oval) {w x y} {
$w coords $::ID [lreplace [$w coords $::ID] 2 3 $x $y]
}
多边形是通过单击角点来绘制的。当一个角点足够靠近第一个角点时,多边形就会闭合并绘制。
proc down(Poly) {w x y} {
if [info exists ::Poly] {
set coords [$w coords $::Poly]
foreach {x0 y0} $coords break
if {hypot($y-$y0,$x-$x0)<10} {
$w delete $::Poly
$w create poly [lrange $coords 2 end] -fill $::Fill
unset ::Poly
} else {
$w coords $::Poly [concat $coords $x $y]
}
} else {
set ::Poly [$w create line $x $y $x $y -fill $::Fill]
}
}
proc move(Poly) {w x y} {#nothing}
#-- With little more coding, the Fill mode allows changing an item's fill color:
proc down(Fill) {w x y} {$w itemconfig current -fill $::Fill}
proc move(Fill) {w x y} {}
#-- Building the UI
set modes {Draw Move Clone Fill Rect Oval Poly}
set colors {
black white magenta brown red orange yellow green green3 green4
cyan blue blue4 purple
}
grid [radio .1 Mode $modes] [radio .2 Fill $colors 1] -sticky nw
grid [canvas .c -relief raised -borderwidth 1] - -sticky news
grid rowconfig . 0 -weight 0
grid rowconfig . 1 -weight 1
#-- The current mode is retrieved at runtime from the global Mode variable:
bind .c <1> {down($Mode) %W %x %y}
bind .c <B1-Motion> {move($Mode) %W %x %y}
bind .c <3> {%W delete current}
要保存当前图像,您需要 Img 扩展,因此如果您没有 Img,请省略以下绑定
bind . <F1> {
package require Img
set img [image create photo -data .c]
set name [tk_getSaveFile -filetypes {{GIFF .gif} {"All files" *}}\
-defaultextension .gif]
if {$name ne ""} {$img write $name; wm title . $name}
}
#-- This is an always useful helper in development:
bind . <Escape> {exec wish $argv0 &; exit}
这里有一个非常简单的编辑器,只有 26 行代码,它只允许加载和保存文件,当然还有编辑、剪切和粘贴,以及文本小部件本身内置的任何功能。它还有一些“在线帮助”...... ;-)
用一些关于名称、目的、作者和日期的解释来开始一个源文件总是一个好主意。我最近养成了将这些信息放在一个字符串变量中的习惯(在 Tcl 中,它可以轻松地跨越多行),这样,相同的的信息就会呈现给源代码的阅读者,并且可以作为在线帮助显示:}
set about "minEd - a minimal editor Richard Suchenwirth 2003 F1: help F2: load F3: save "
图形用户界面 (GUI) 的可见部分由小部件组成。对于这个编辑器,我当然需要一个文本小部件和一个垂直滚动条。使用文本小部件的 "-wrap word" 选项,不需要额外的水平滚动条 - 超出窗口的线条会以单词边界为界进行换行。
Tk 小部件以两个步骤出现在屏幕上:首先,它们被创建并具有初始配置;然后,被传递给“几何管理器”以进行显示。由于小部件创建命令返回路径名,因此它们可以嵌套到管理器命令中(在本例中为 pack),以将所有小部件的设置保存在一个地方。但这可能会导致过长的行。
虽然滚动条位于文本的右侧,但我首先创建并打包它。原因是当用户缩小窗口时,最后打包的小部件会失去可见性。
这两行也说明了滚动条与其控制的小部件之间的耦合关系。
- 滚动条在移动时向它发送 yview 消息。
- 当视图发生变化时,例如从光标键,小部件会向滚动条发送 set 消息。
这两行已经给了我们一个用于任意长文本的编辑器,它具有内置的剪切、复制和粘贴功能——请参见文本手册页。我们只需要添加文件 I/O 才能使其真正可用。
pack [scrollbar .y -command ".t yview"] -side right -fill y pack [text .t -wrap word -yscrollc ".y set"] -side right -fill both -expand 1
你的目标是 8.4 或更高版本吗?如果是,请将 -undo 1 添加到文本选项中,以获得完整的撤消/重做支持!
pack [text .t -wrap word -yscrollc ".y set" -undo 1] -side right -fill both -expand 1
GUI 的另一个重要部分是绑定——什么事件将触发什么动作。为了简单起见,我将这里的绑定限制为标准键盘上的一些功能键。
bind . <F1> {tk_messageBox -message $about}
联机帮助使用的是一个没有花哨功能的 tk_messageBox,其“关于”文本在文件开头定义。——其他绑定调用自定义命令,这些命令从 Tk 的文件选择器对话框中获取文件名参数。
bind . <F2> {loadText .t [tk_getOpenFile]}
bind . <F3> {saveText .t [tk_getSaveFile]}
这些对话框也可以用多种方法进行配置,但即使在这种简单的形式下,它们也相当强大——允许在文件系统中导航等等。在 Windows 上,它们调用本机文件选择器,这些选择器具有以前打开文件的历史记录、详细视图(大小/日期等)。
当这个编辑器在命令行中使用文件名调用时,该文件将在启动时加载(它很简单,它一次只能处理一个文件)。
if {$argv != ""} {loadText .t [lindex $argv 0]}
加载和保存文本的过程都从对文件名参数的健全性检查开始——如果它是一个空字符串(由用户取消时文件选择器对话框生成),则它们会立即返回。否则,它们会将文件内容传输到文本小部件或反之。loadText 添加了一个“奢侈”功能,即当前文件的名称也被放入窗口标题中。然后它打开文件,清除文本小部件,一次性读取所有文件内容,并将它们放入文本小部件中。
proc loadText {w fn} {
if {$fn==""} return
wm title . [file tail $fn]
set fp [open $fn]
$w delete 1.0 end
$w insert end [read $fp]
close $fp
}
saveText 通过将范围限制为“end - 1 c”(字符)来确保不会保存文本小部件在末尾附加的额外换行符。
proc saveText {w fn} {
if {$fn==""} return
set fp [open $fn w]
puts -nonewline $fp [$w get 1.0 "end - 1 c"]
close $fp
}
文件监视
[edit | edit source]一些编辑器(例如 PFE、MS Visual Studio)在正在编辑的文件在磁盘上被更改时弹出警告对话框——这可能会导致编辑冲突。Emacs 在首次尝试更改磁盘上已更改的文件时显示一个更微妙的警告。
这里我尝试模拟此功能。它过于简化,因为它不会更新 mtime(文件修改时间)以进行检查,一旦你从编辑器本身保存了它。因此,请确保在保存后再次调用 text'watch'file。
使用全局变量 ::_twf,至少可以避免误报——对于更严肃的实现,你可能会使用按文件名索引的命名空间数组,以防你想要多个编辑窗口。}
proc text'watch'file {w file {mtime -}} {
set checkinterval 1000 ;# modify as needed
if {$mtime eq "-"} {
if [info exists ::_twf] {after cancel $::_twf}
set file [file join [pwd] $file]
text'watch'file $w $file [file mtime $file]
} else {
set newtime [file mtime $file]
if {$newtime != $mtime} {
set answer [tk_messageBox -type yesno -message \
"The file\n$file\nhas changed on disk. Reload it?"]
if {$answer eq "yes"} {text'read'file $w $file}
text'watch'file $w $file
} else {set ::_twf [after $checkinterval [info level 0]]}
}
}
proc text'read'file {w file} {
set f [open $file]
$w delete 1.0 end
$w insert end [read $f]
close $f
}
#-- Testing: pack [text .t -wrap word] -fill both -expand 1 set file textwatch.tcl text'read'file .t $file text'watch'file .t $file
当你从外部更改文件时,例如通过在纯 Tcl 中对其进行触碰,对话框应该出现,这可能是通过在另一个编辑器中对其进行编辑,或者
file mtime $filename [clock seconds]
微型演示图形
[edit | edit source]这是一个粗略的画布演示图形,它可以在 PocketPC 上运行,也可以在更大的盒子(人们可能会在那里缩放字体和尺寸)上运行。使用左/右光标或鼠标左/右键切换页面(虽然触控笔无法右键单击)。
功能不多,但代码非常紧凑,并且使用了一种可爱的用于内容规范的小语言,请参见末尾的示例(它显示了我在 2003 年纽伦堡 Euro-Tcl 大会上展示的内容……)。
proc slide args {
global slides
if {![info exist slides]} slide'init
incr slides(N)
set slides(title,$slides(N)) [join $args]
}
proc slide'line {type args} {
global slides
lappend slides(body,$slides(N)) [list $type [join $args]]
}
foreach name {* + -} {interp alias {} $name {} slide'line $name}
proc slide'init {} {
global slides
array set slides {
canvas .c N 0 show 1 dy 20
titlefont {Tahoma 22 bold} * {Tahoma 14 bold} + {Tahoma 12}
- {Courier 10}
}
pack [canvas .c -bg white] -expand 1 -fill both
foreach e {<1> <Right>} {bind . $e {slide'show 1}}
foreach e {<3> <Left>} {bind . $e {slide'show -1}}
wm geometry . +0+0
after idle {slide'show 0}
}
proc slide'show delta {
upvar #0 slides s
incr s(show) $delta
if {$s(show)<1 || $s(show)>$s(N)} {
incr s(show) [expr -$delta]
} else {
set c $s(canvas)
$c delete all
set x 10; set y 20
$c create text $x $y -anchor w -text $s(title,$s(show))\
-font $s(titlefont) -fill blue
incr y $s(dy)
$c create line $x $y 2048 $y -fill red -width 4
foreach line $s(body,$s(show)) {
foreach {type text} $line break
incr y $s(dy)
$c create text $x $y -anchor w -text $text \
-font $s($type)
}
}
}
bind . <Up> {exec wish $argv0 &; exit} ;# dev helper
剩下的就是数据——还是代码?无论如何,这是我的节目。
slide i18n - Tcl for the world + Richard Suchenwirth, Nuremberg 2003 + * i18n: internationalization + 'make software work with many languages' + * l10n: localization + 'make software work with the local language'
slide Terminology * Glyphs: + visible elements of writing * Characters: + abstract elements of writing * Byte sequences: + physical text data representation * Rendering: character -> glyph * Encoding: character <-> byte sequence
slide Before Unicode * Bacon (1580), Baudot: 5-bit encodings * Fieldata (6 bits), EBCDIC (8 bits) * ASCII (7 bits) + world-wide "kernel" of encodings * 8-bit codepages: DOS, Mac, Windows * ISO 8859-x: 16 varieties
slide East Asia * Thousands of characters/country + Solution: use 2 bytes, 94x94 matrix + Japan: JIS C-6226/0208/0212 + China: GB2312-80 + Korea: KS-C 5601 + * coexist with ASCII in EUC encodings
slide Unicode covers all * Common standard of software industry * kept in synch with ISO 10646 + Used to be 16 bits, until U 3.1 + Now needs up to 31 bits * Byte order problem: + little-endian/big-endian + U+FEFF "Byte Order Mark" + U+FFFE --illegal--
slide UTF-8 * Varying length: 1..3(..6) bytes + 1 byte: ASCII + 2 bytes: pages 00..07, Alphabets + 3 bytes: pages 08..FF, rest of BMP + >3 bytes: higher pages + * Standard in XML, coming in Unix
slide Tcl i18n
* Everything is a Unicode string (BMP)
+ internal rep: UTF-8/UCS-2
* Important commands:
- fconfigure \$ch -encoding \$e
- encoding convertfrom \$e \$s
- encoding convertto \$e \$s
+
* msgcat supports l10n:
- {"File" -> [mc "File"]}
slide Tk i18n * Any widget text is Unicoded * Automatic font finding + Fonts must be provided by system + * Missing: bidi treatment + right-to-left conversion (ar,he)
slide Input i18n
* Keyboard rebinding (bindtags)
* East Asia: keyboard buffering
+ Menu selection for ambiguities
+
* Virtual keyboard (buttons, canvas)
* String conversion: *lish family
- {[ruslish Moskva]-[greeklish Aqh'nai]}
slide i18n - Tcl for the world + + + Thank you.
时间线显示
[edit | edit source]在画布上还可以做另一件事:水平时间线的历史可视化,上面显示了年份刻度。目前有以下几种类型的对象可用。
- “eras”,以黄色显示在时间线下方,在方框中。
- “背景项”,它们是灰色的,并且在整个画布的高度上延伸。
- 普通项,它们以堆叠的橙色条形显示。
你可以使用 <1> 放大,使用 <3> 缩小(两者仅在 x 方向上)。鼠标移动时,当前年份将显示在顶层窗口的标题中。普通项可以是单个年份,例如哥伦布示例,也可以是一系列年份,例如人物的寿命。(该示例表明莫扎特没有活很长时间……)
namespace eval timeliner {
variable ""
array set "" {-zoom 1 -from 0 -to 2000}
}
proc timeliner::create {w args} {
variable ""
array set "" $args
#-- draw time scale
for {set x [expr ($(-from)/50)*50]} {$x<=$(-to)} {incr x 10} {
if {$x%50 == 0} {
$w create line $x 8 $x 0
$w create text $x 8 -text $x -anchor n
} else {
$w create line $x 5 $x 0
}
}
bind $w <Motion> {timeliner::title %W %x ; timeliner::movehair %W %x}
bind $w <1> {timeliner::zoom %W %x 1.25}
bind $w <2> {timeliner::hair %W %x}
bind $w <3> {timeliner::zoom %W %x 0.8}
}
proc timeliner::movehair {w x} {
variable ""
if {[llength [$w find withtag hair]]} {
set x [$w canvasx $x]
$w move hair [expr {$x - $(x)}] 0
set (x) $x
}
}
proc timeliner::hair {w x} {
variable ""
if {[llength [$w find withtag hair]]} {
$w delete hair
} else {
set (x) [$w canvasx $x]
$w create line $(x) 0 $(x) [$w cget -height] \
-tags hair -width 1 -fill red
}
}
proc timeliner::title {w x} {
variable ""
wm title . [expr int([$w canvasx $x]/$(-zoom))]
}
proc timeliner::zoom {w x factor} {
variable ""
$w scale all 0 0 $factor 1
set (-zoom) [expr {$(-zoom)*$factor}]
$w config -scrollregion [$w bbox all]
if {[llength [$w find withtag hair]]} {
$w delete hair
set (x) [$w canvasx $x]
$w create line $(x) 0 $(x) [$w cget -height] \
-tags hair -width 1 -fill red
}
}
此命令将对象添加到画布。对于“item”的代码,我花了一些力气,因为它必须在画布上找到一个空闲的“插槽”,从上到下搜索。
proc timeliner::add {w type name time args} {
variable ""
regexp {(\d+)(-(\d+))?} $time -> from - to
if {$to eq ""} {set to $from}
set x0 [expr {$from*$(-zoom)}]
set x1 [expr {$to*$(-zoom)}]
switch -- $type {
era {set fill yellow; set outline black; set y0 20; set y1 40}
bgitem {set fill gray; set outline {}; set y0 40; set y1 1024}
item {
set fill orange
set outline yellow
for {set y0 60} {$y0<400} {incr y0 20} {
set y1 [expr {$y0+18}]
if {[$w find overlap [expr $x0-5] $y0 $x1 $y1] eq ""} break
}
}
}
set id [$w create rect $x0 $y0 $x1 $y1 -fill $fill -outline $outline]
if {$type eq "bgitem"} {$w lower $id}
set x2 [expr {$x0+5}]
set y2 [expr {$y0+2}]
set tid [$w create text $x2 $y2 -text $name -anchor nw]
foreach arg $args {
if {$arg eq "!"} {
$w itemconfig $tid -font "[$w itemcget $tid -font] bold"
}
}
$w config -scrollregion [$w bbox all]
}
这是一个示例应用程序,它以作曲家的形式展示了音乐简明历史。
scrollbar .x -ori hori -command {.c xview}
pack .x -side bottom -fill x
canvas .c -bg white -width 600 -height 300 -xscrollcommand {.x set}
pack .c -fill both -expand 1
timeliner::create .c -from 1400 -to 2000
这些用于添加项目的简便缩写使数据规范变得轻而易举——比较原始调用和简便缩写。
timeliner::add .c item Purcell 1659-1695 - Purcell 1659-1695
使用附加的“!”参数,你可以使项目的文本变为粗体。
foreach {shorthand type} {* era x bgitem - item} {
interp alias {} $shorthand {} timeliner::add .c $type
}
现在是显示的数据(以易读的形式编写)。
* {Middle Ages} 1400-1450
- Dufay 1400-1474
* Renaissance 1450-1600
- Desprez 1440-1521
- Luther 1483-1546
- {Columbus discovers America} 1492
- Palestrina 1525-1594 !
- Lasso 1532-1594
- Byrd 1543-1623
* Baroque 1600-1750
- Dowland 1563-1626
- Monteverdi 1567-1643
- Schütz 1585-1672
- Purcell 1659-1695
- Telemann 1681-1767
- Rameau 1683-1764
- Bach,J.S. 1685-1750 !
- Händel 1685-1759
x {30-years war} 1618-1648
* {Classic era} 1750-1810
- Haydn 1732-1809 !
- Boccherini 1743-1805
- Mozart 1756-1791 !
- Beethoven 1770-1828 !
* {Romantic era} 1810-1914
- {Mendelssohn Bartholdy} 1809-1847
- Chopin 1810-1849
- Liszt 1811-1886
- Verdi 1813-1901
x {French revolution} 1789-1800
* {Modern era} 1914-2000
- Ravel 1875-1937 !
- Bartók 1881-1945
- Stravinskij 1882-1971
- Varèse 1883-1965
- Prokof'ev 1891-1953
- Milhaud 1892-1974
- Honegger 1892-1955
- Hindemith 1895-1963
- Britten 1913-1976
x WW1 1914-1918
x WW2 1938-1945
函数乐
[edit | edit source]我十几岁的女儿讨厌数学。为了激励她,我加强了之前的一个小函数绘图器,该绘图器以前只能从命令行获取一个函数,并使用严格的 Tcl(expr)表示法。现在有一个条目小部件,并且接受的语言也得到了丰富:除了 exprs 规则之外,你还可以省略美元符号和乘号,例如 2x+1,幂可以写成 x3 而不是 ($x*$x*$x);在简单的情况下,你可以省略围绕函数参数的括号,例如 sin x2。在条目小部件中按
如果你需要一些想法,请单击“?”按钮以循环浏览一组演示函数,从无聊到古怪(例如,如果使用 rand())。除了默认缩放之外,你还可以放大或缩小。将鼠标指针移到画布上会显示 x 和 y 坐标,如果你在曲线上的某个点上,则显示会变为白色。
目标没有达到:我女儿仍然讨厌数学。但至少我又有了几个小时的 Tcl(和函数)乐趣,在笛卡尔平面上冲浪……希望你也喜欢它!
proc main {} {
canvas .c -bg white -borderwidth 0
bind .c <Motion> {displayXY .info %x %y}
frame .f
label .f.1 -text "f(x) = "
entry .f.f -textvar ::function -width 40
bind .f.f <Return> {plotf .c $::function}
button .f.demo -text " ? " -pady 0 -command {demo .c}
label .f.2 -text " Zoom: "
entry .f.fac -textvar ::factor -width 4
set ::factor 32
bind .f.fac <Return> {zoom .c 1.0}
button .f.plus -text " + " -pady 0 -command {zoom .c 2.0}
button .f.minus -text " - " -pady 0 -command {zoom .c 0.5}
eval pack [winfo children .f] -side left -fill both
label .info -textvar ::info -just left
pack .info .f -fill x -side bottom
pack .c -fill both -expand 1
demo .c
}
set ::demos {
"cos x3" 2 1-x 0.5x2 x3/5 "sin x" "sin x2" 1/x sqrt(x)
"tan x/5" x+1/x x abs(x) "exp x" "log x" "log x2"
round(x) "int x%2" "x-int x" "0.2tan x+1/tan x" x*(rand()-0.5)
x2/5-1/(2x) "atan x" sqrt(1-x2) "abs(x-int(x*2))" (x-1)/(x+1)
"sin x-tan x" "sin x-tan x2" "x-abs(int x)" 0.5x-1/x
-0.5x3+x2+x-1 3*sin(2x) -0.05x4-0.2x3+1.5x2+2x-3 "9%int x"
0.5x2/(x3-3x2+4) "abs x2-3 int x" "int x%3"
}
proc displayXY {w cx cy} {
set x [expr {double($cx-$::dx)/$::factor}]
set y [expr {double(-$cy+$::dy)/$::factor}]
set ::info [format "x=%.2f y=%.2f" $x $y]
catch {
$w config -fg [expr {abs([expr $::fun]-$y)<0.01?"white":"black"}]
} ;# may divide by zero, or other illegal things
}
proc zoom {w howmuch} {
set ::factor [expr round($::factor*$howmuch)]
plotf $w $::function
}
proc plotf {w function} {
foreach {re subst} {
{([a-z]) +(x[0-9]?)} {\1(\2)} " " "" {([0-9])([a-z])} {\1*\2}
x2 x*x x3 x*x*x x4 x*x*x*x x \$x {e\$xp} exp
} {regsub -all $re $function $subst function}
set ::fun $function
set ::info "Tcl: expr $::fun"
set color [lpick {red blue purple brown green}]
plotline $w [fun2points $::fun] -fill $color
}
proc lpick L {lindex $L [expr {int(rand()*[llength $L])}]}
proc fun2points {fun args} {
array set opt {-from -10.0 -to 10.0 -step .01}
array set opt $args
set res "{"
for {set x $opt(-from)} {$x<= $opt(-to)} {set x [expr {$x+$opt(-step)}]} {
if {![catch {expr $fun} y]} {
if {[info exists lasty] && abs($y-$lasty)>100} {
append res "\} \{" ;# incontinuity
}
append res " $x $y"
set lasty $y
} else {append res "\} \{"}
}
append res "}"
}
proc plotline {w points args} {
$w delete all
foreach i $points {
if {[llength $i]>2} {eval $w create line $i $args -tags f}
}
set fac $::factor
$w scale all 0 0 $fac -$fac
$w create line -10000 0 10000 0 ;# X axis
$w create line 0 -10000 0 10000 ;# Y axis
$w create line $fac 0 $fac -3 ;# x=1 tick
$w create line -3 -$fac 0 -$fac ;# y=1 tick
set ::dx [expr {[$w cget -width]/2}]
set ::dy [expr {[$w cget -height]/2}]
$w move all $::dx $::dy
$w raise f
}
proc demo {w} {
set ::function [lindex $::demos 0] ;# cycle through...
set ::demos [concat [lrange $::demos 1 end] [list $::function]]
set ::factor 32
plotf $w $::function
}
main
功能成像
[edit | edit source]在 Conal Elliott 的 Pan 项目(“功能图像合成”,[1])中,图像(任意大小和分辨率)以一种优雅的功能方式生成和操作。用 Haskell 编写的函数(参见 Playing Haskell)被应用,主要以函数组合的形式应用于像素,以返回它们的色值。常见问题解答:“我们也可以在 Tcl 中这样做吗?”
正如下面的 funimj 演示所示,原则上可以;但这需要一些耐心(或非常快的 CPU)——对于一个 200x200 的图像,该函数被调用了 40000 次,在我的 P200 盒子上需要 9..48 秒。尽管如此,输出通常值得等待……并且编写此代码所花费的时间微不足道,因为 Haskell 原件只需稍加修改即可在 Tcl 中表示。函数组合必须重写为 Tcl 的波兰表示法——Haskell 的
foo 1 o bar 2 o grill
(其中“o”是组合运算符)在 Tcl 中将如下所示
o {foo 1} {bar 2} grill
正如示例所示,可以指定其他参数;只有最后一个参数通过生成的“函数嵌套”传递。
proc f {x} {foo 1 [bar 2 [grill $x]]}
但生成的函数的名称比“f”好得多:即,对“o”的完整调用被使用,因此示例 proc 的名称是
"o {foo 1} {bar 2} grill"
这很有自说明性 ;-)我这样实现了“o”。
proc o args {
# combine the functions in args, return the created name
set name [info level 0]
set body "[join $args " \["] \$x"
append body [string repeat \] [expr {[llength $args]-1}]]
proc $name x $body
set name
}
# Now for the rendering framework:
proc fim {f {zoom 100} {width 200} {height -}} {
# produce a photo image by applying function f to pixels
if {$height=="-"} {set height $width}
set im [image create photo -height $height -width $width]
set data {}
set xs {}
for {set j 0} {$j<$width} {incr j} {
lappend xs [expr {($j-$width/2.)/$zoom}]
}
for {set i 0} {$i<$height} {incr i} {
set row {}
set y [expr {($i-$height/2.)/$zoom}]
foreach x $xs {
lappend row [$f [list $x $y]]
}
lappend data $row
}
$im put $data
set im
}
基本成像函数(“绘图器”)具有共同的功能点 -> 颜色,其中点是一对 {x y}(或者,在应用极坐标变换后,{r a}……),颜色是 Tk 颜色名称,例如“green”或 #010203。
proc vstrip p {
# a simple vertical bar
b2c [expr {abs([lindex $p 0]) < 0.5}]
}
proc udisk p {
# unit circle with radius 1
foreach {x y} $p break
b2c [expr {hypot($x,$y) < 1}]
}
proc xor {f1 f2 p} {
lappend f1 $p; lappend f2 $p
b2c [expr {[eval $f1] != [eval $f2]}]
}
proc and {f1 f2 p} {
lappend f1 $p; lappend f2 $p
b2c [expr {[eval $f1] == "#000" && [eval $f2] == "#000"}]
}
proc checker p {
# black and white checkerboard
foreach {x y} $p break
b2c [expr {int(floor($x)+floor($y)) % 2 == 0}]
}
proc gChecker p {
# greylevels correspond to fractional part of x,y
foreach {x y} $p break
g2c [expr {(fmod(abs($x),1.)*fmod(abs($y),1.))}]
}
proc bRings p {
# binary concentric rings
foreach {x y} $p break
b2c [expr {round(hypot($x,$y)) % 2 == 0}]
}
proc gRings p {
# grayscale concentric rings
foreach {x y} $p break
g2c [expr {(1 + cos(3.14159265359 * hypot($x,$y))) / 2.}]
}
proc radReg {n p} {
# n wedge slices starting at (0,0)
foreach {r a} [toPolars $p] break
b2c [expr {int(floor($a*$n/3.14159265359))%2 == 0}]
}
proc xPos p {b2c [expr {[lindex $p 0]>0}]}
proc cGrad p {
# color gradients - best watched at zoom=100
foreach {x y} $p break
if {abs($x)>1.} {set x 1.}
if {abs($y)>1.} {set y 1.}
set r [expr {int((1.-abs($x))*255.)}]
set g [expr {int((sqrt(2.)-hypot($x,$y))*180.)}]
set b [expr {int((1.-abs($y))*255.)}]
c2c $r $g $b
}
除了 Conal Elliott 论文中的示例外,我发现功能成像也可以被滥用于(缓慢且不精确的)函数绘图器,如果你使用 $y + f($x) 作为第一个参数调用它,则该绘图器会显示 y = f(x) 的图形。
proc fplot {expr p} {
foreach {x y} $p break
b2c [expr abs($expr)<=0.04] ;# double eval required here!
}
这里有一个用于两个二进制图像的组合器,它以不同的颜色显示两个图像中的哪一个点是“真”的——很好但很慢:}
proc bin2 {f1 f2 p} {
set a [eval $f1 [list $p]]
set b [eval $f2 [list $p]]
expr {
$a == "#000" ?
$b == "#000" ? "green"
: "yellow"
: $b == "#000" ? "blue"
: "black"
}
}
#--------------------------------------- Pixel converters:
proc g2c {greylevel} {
# convert 0..1 to #000000..#FFFFFF
set hex [format %02X [expr {round($greylevel*255)}]]
return #$hex$hex$hex
}
proc b2c {binpixel} {
# 0 -> white, 1 -> black
expr {$binpixel? "#000" : "#FFF"}
}
proc c2c {r g b} {
# make Tk color name: {0 128 255} -> #0080FF
format #%02X%02X%02X $r $g $b
}
proc bPaint {color0 color1 pixel} {
# convert a binary pixel to one of two specified colors
expr {$pixel=="#000"? $color0 : $color1}
}
此绘图器以给定颜色的色调对灰度图像进行着色。它通过除以“白色”的相应值来规范给定颜色,但看起来也很慢。
proc gPaint {color pixel} {
set abspixel [lindex [rgb $pixel] 0]
set rgb [rgb $color]
set rgbw [rgb white]
foreach var {r g b} in $rgb ref $rgbw {
set $var [expr {round(double($abspixel)*$in/$ref/$ref*255.)}]
}
c2c $r $g $b
}
此 proc 缓存 [winfo rgb] 调用的结果,因为这些调用相当昂贵,尤其是在远程 X 显示器上 - rmax。
proc rgb {color} {
upvar "#0" rgb($color) rgb
if {![info exists rgb]} {set rgb [winfo rgb . $color]}
set rgb
}
#------------------------------ point -> point transformers
proc fromPolars p {
foreach {r a} $p break
list [expr {$r*cos($a)}] [expr {$r*sin($a)}]
}
proc toPolars p {
foreach {x y} $p break
# for Sun, we have to make sure atan2 gets no two 0's
list [expr {hypot($x,$y)}] [expr {$x||$y? atan2($y,$x): 0}]
}
proc radInvert p {
foreach {r a} [toPolars $p] break
fromPolars [list [expr {$r? 1/$r: 9999999}] $a]
}
proc rippleRad {n s p} {
foreach {r a} [toPolars $p] break
fromPolars [list [expr {$r*(1.+$s*sin($n*$a))}] $a]
}
proc slice {n p} {
foreach {r a} $p break
list $r [expr {$a*$n/3.14159265359}]
}
proc rotate {angle p} {
foreach {x y} $p break
set x1 [expr {$x*cos(-$angle) - $y*sin(-$angle)}]
set y1 [expr {$y*cos(-$angle) + $x*sin(-$angle)}]
list $x1 $y1
}
proc swirl {radius p} {
foreach {x y} $p break
set angle [expr {hypot($x,$y)*6.283185306/$radius}]
rotate $angle $p
}
现在是演示程序。它在按钮栏上显示预定义的基本图像运算符和一些组合。单击一个按钮,耐心等待,相应的图像将显示在右侧的画布上。你也可以在底部的条目小部件中尝试使用图像运算符——按
- 组合运算符“o”
- 零个或多个“绘图器”(颜色 -> 颜色)
- 一个“绘图器”(点 -> 颜色)
- 零个或多个“变换器”(点 -> 点)
}
proc fim'show {c f} {
$c delete all
set ::try $f ;# prepare for editing
set t0 [clock seconds]
. config -cursor watch
update ;# to make the cursor visible
$c create image 0 0 -anchor nw -image [fim $f $::zoom]
wm title . "$f: [expr [clock seconds]-$t0] seconds"
. config -cursor {}
}
proc fim'try {c varName} {
upvar #0 $varName var
$c delete all
if [catch {fim'show $c [eval $var]}] {
$c create text 10 10 -anchor nw -text $::errorInfo
}
}
组合函数只需要提到一次,这将创建它们,并且它们以后可以通过信息过程获取。这里的 o 看起来很像子弹……
o bRings
o cGrad
o checker
o gRings
o vstrip
o xPos
o {bPaint brown beige} checker
o checker {slice 10} toPolars
o checker {rotate 0.1}
o vstrip {swirl 1.5}
o checker {swirl 16}
o {fplot {$y + exp($x)}}
o checker radInvert
o gRings {rippleRad 8 0.3}
o xPos {swirl .75}
o gChecker
o {gPaint red} gRings
o {bin2 {radReg 7} udisk}
#----------------------------------------------- testing
frame .f2
set c [canvas .f2.c]
set e [entry .f2.e -bg white -textvar try]
bind $e <Return> [list fim'try $c ::try]
scale .f2.s -from 1 -to 100 -variable zoom -ori hori -width 6
#--------------------------------- button bar:
frame .f
set n 0
foreach imf [lsort [info procs "o *"]] {
button .f.b[incr n] -text $imf -anchor w -pady 0 \
-command [list fim'show $c $imf]
}
set ::zoom 25
eval pack [winfo children .f] -side top -fill x -ipady 0
eval pack [winfo children .f2] -side top -fill x
pack .f .f2 -side left -anchor n
bind . <Escape> {exec wish $argv0 &; exit} ;# dev helper
bind . ? {console show} ;# dev helper, Win/Mac only
TkPhotoLab
[edit | edit source]以下代码可用于图像处理实验,包括
- 卷积(见下文)
- 从颜色到灰度级别的转换
- 灰度级到伪彩色转换
- 亮度和对比度修改
Tcl 在处理大量数字运算方面并不快,例如处理数千个像素时,但我不建议在有趣的项目中使用 C ;) 所以慢慢来,或者找一个真正的 CPU。至少您可以观察进度,因为目标图像会在每行处理后更新。
拉普拉斯 5 滤波器边缘增强
演示 UI 显示两个图像,左侧是原始图像,右侧是处理结果。您可以使用“选项/接受”将结果推到左侧。查看菜单以了解我提供的功能。但最让我感兴趣的是“卷积”,您可以编辑矩阵(固定为 3x3 - 速度足够慢..)并单击“应用”以在输入图像上运行它。“C” 将矩阵设置为全零。
卷积是一种技术,其中目标像素的颜色根据给定矩阵及其邻居的乘积之和来确定。例如,卷积矩阵
1 1 1 1 1 1 1 1 1
使用自身及其八个邻居的平均值对中间的像素进行着色,这将模糊图像。
0 0 0 0 1 0 0 0 0
应该忠实地复制输入图像。这些
0 -1 0 -1 -1 -1 -1 5 -1 or: -1 9 -1 0 -1 0 -1 -1 -1
增强{水平,垂直}边缘,使图像看起来更“清晰”。}
proc convolute {inimg outimg matrix} {
set w [image width $inimg]
set h [image height $inimg]
set matrix [normalize $matrix]
set shift [expr {[matsum $matrix]==0? 128: 0}]
set imat [photo2matrix $inimg]
for {set i 1} {$i<$h-1} {incr i} {
set row {}
for {set j 1} {$j<$w-1} {incr j} {
foreach var {rsum gsum bsum} {set $var 0.0}
set y [expr {$i-1}]
foreach k {0 1 2} {
set x [expr {$j-1}]
foreach l {0 1 2} {
if {[set fac [lindex $matrix $k $l]]} {
foreach {r g b} [lindex $imat $y $x] {}
set rsum [expr {$rsum + $r * $fac}]
set gsum [expr {$gsum + $g * $fac}]
set bsum [expr {$bsum + $b * $fac}]
}
incr x
}
incr y
}
if {$shift} {
set rsum [expr {$rsum + $shift}]
set gsum [expr {$gsum + $shift}]
set bsum [expr {$bsum + $shift}]
}
lappend row [rgb [clip $rsum] [clip $gsum] [clip $bsum]]
}
$outimg put [list $row] -to 1 $i
update idletasks
}
}
proc alias {name args} {eval [linsert $args 0 interp alias {} $name {}]}
alias rgb format #%02x%02x%02x
proc lambda {argl body} {K [set n [info level 0]] [proc $n $argl $body]}
proc K {a b} {set a}
proc clip x {expr {$x>255? 255: $x<0? 0: int($x)}}
proc photo2matrix image {
set w [image width $image]
set h [image height $image]
set res {}
for {set y 0} {$y<$h} {incr y} {
set row {}
for {set x 0} {$x<$w} {incr x} {
lappend row [$image get $x $y]
}
lappend res $row
}
set res
}
proc normalize matrix {
#-- make sure all matrix elements add up to 1.0
set sum [matsum $matrix]
if {$sum==0} {return $matrix} ;# no-op on zero sum
set res {}
foreach inrow $matrix {
set row {}
foreach el $inrow {lappend row [expr {1.0*$el/$sum}]}
lappend res $row
}
set res
}
proc matsum matrix {expr [join [join $matrix] +]}
以下例程也可以通用化为一个
proc color2gray image {
set w [image width $image]
set h [image height $image]
for {set i 0} {$i<$h} {incr i} {
set row {}
for {set j 0} {$j<$w} {incr j} {
foreach {r g b} [$image get $j $i] break
set y [expr {int(0.299*$r + 0.587*$g + 0.114*$b)}]
lappend row [rgb $y $y $y]
}
$image put [list $row] -to 0 $i
update idletasks
}
}
proc color2gray2 image {
set i -1
foreach inrow [photo2matrix $image] {
set row {}
foreach pixel $inrow {
foreach {r g b} $pixel break
set y [expr {int(($r + $g + $b)/3.)}]
lappend row [rgb $y $y $y]
}
$image put [list $row] -to 0 [incr i]
update idletasks
}
}
将灰度级分类为非真实颜色的实验
proc gray2color image {
set i -1
set colors {black darkblue blue purple red orange yellow white}
set n [llength $colors]
foreach inrow [photo2matrix $image] {
set row {}
foreach pixel $inrow {
set index [expr {[lindex $pixel 0]*$n/256}]
lappend row [lindex $colors $index]
}
$image put [list $row] -to 0 [incr i]
update idletasks
}
}
proc grayWedge image {
$image blank
for {set i 0} {$i<256} {incr i} {
$image put [rgb $i $i $i] -to $i 0 [expr {$i+1}] 127
}
}
许多算法非常相似,仅在中心的一些命令上有区别。因此我将它们通用化,它们接受一个函数名称,该函数名称应用于每个像素 rgb,或者一对像素 rgb。它们由一个别名实例化,该别名将函数巧妙地设置为 lambda
proc generic_1 {f target source} {
set w [image width $source]
set h [image height $source]
for {set i 0} {$i<$h} {incr i} {
set row {}
for {set j 0} {$j<$w} {incr j} {
foreach {r g b} [$source get $j $i] break
lappend row [rgb [$f $r] [$f $g] [$f $b]]
}
$target put [list $row] -to 0 $i
update idletasks
}
}
alias invert generic_1 [lambda x {expr {255-$x}}]
alias contrast+ generic_1 [lambda x {clip [expr {128+($x-128)*1.25}]}]
alias contrast- generic_1 [lambda x {clip [expr {128+($x-128)*0.8}]}]
proc generic_2 {f target with} {
set w [image width $target]
set h [image height $target]
for {set i 0} {$i<$h} {incr i} {
set row {}
for {set j 0} {$j<$w} {incr j} {
foreach {r g b} [$target get $j $i] break
foreach {r1 g1 b1} [$with get $j $i] break
lappend row [rgb [$f $r $r1] [$f $g $g1] [$f $b $b1]]
}
$target put [list $row] -to 0 $i
update idletasks
}
}
alias blend generic_2 [lambda {a b} {expr {($a+$b)/2}}]
alias difference generic_2 [lambda {a b} {expr {255-abs($a-$b)}}]
直方图是当前图像中每个颜色值出现的次数,分别针对红色、绿色和蓝色。对于灰度图像,显示的“曲线”应该完全重叠,因此您只能看到最后绘制的蓝色点。
proc histogram {image {channel 0}} {
set w [image width $image]
set h [image height $image]
for {set i 0} {$i<256} {incr i} {set hist($i) 0}
for {set i 0} {$i<$h} {incr i} {
for {set j 0} {$j<$w} {incr j} {
incr hist([lindex [$image get $j $i] $channel])
}
}
set res {}
for {set i 0} {$i<256} {incr i} {lappend res $hist($i)}
set res
}
proc drawHistogram {target input} {
$target blank
set a [expr {6000./([image height $input]*[image width $input])}]
foreach color {red green blue} channel {0 1 2} {
set i -1
foreach val [histogram $input $channel] {
$target put $color -to [incr i] \
[clip [expr {int(128-$val*$a)}]]
}
update idletasks
}
}
演示 UI
if {[file tail [info script]] eq [file tail $argv0]} {
package require Img ;# for JPEG etc.
proc setFilter {w matrix} {
$w delete 1.0 end
foreach row $matrix {$w insert end [join $row \t]\n}
set ::info "Click 'Apply' to use this filter"
}
label .title -text TkPhotoLab -font {Helvetica 14 italic} -fg blue
label .( -text ( -font {Courier 32}
set txt [text .t -width 20 -height 3]
setFilter .t {{0 -1 0} {-1 5 -1} {0 -1 0}}
label .) -text ) -font {Courier 32}
button .c -text C -command {setFilter .t {{0 0 0} {0 0 0} {0 0 0}}}
grid .title .( .t .) .c -sticky news
button .apply -text Apply -command applyConv
grid x ^ ^ ^ .apply -sticky ew
grid [label .0 -textvar info] - - -sticky w
grid [label .1] - [label .2] - - -sticky new
proc loadImg { {fn ""}} {
if {$fn==""} {set fn [tk_getOpenFile]}
if {$fn != ""} {
cd [file dirname [file join [pwd] $fn]]
set ::im1 [image create photo -file $fn]
.1 config -image $::im1
set ::im2 [image create photo]
.2 config -image $::im2
$::im2 copy $::im1 -shrink
set ::info "Loaded image 1 from $fn"
}
}
proc saveImg { {fn ""}} {
if {$fn==""} {set fn [tk_getSaveFile]}
if {$fn != ""} {
$::im2 write $fn -format JPEG
set ::info "Saved image 2 to $fn"
}
}
proc applyConv {} {
set ::info "Convolution running, have patience..."
set t0 [clock clicks -milliseconds]
convolute $::im1 $::im2 [split [$::txt get 1.0 end] \n]
set dt [expr {([clock click -milliseconds]-$t0)/1000.}]
set ::info "Ready after $dt sec"
}
一个用于简化菜单创建的小包装器 - 请参见下面的用法
proc m+ {head name {cmd ""}} {
if {![winfo exists .m.m$head]} {
.m add cascade -label $head -menu [menu .m.m$head -tearoff 0]
}
if [regexp ^-+$ $name] {
.m.m$head add separator
} else {.m.m$head add command -label $name -comm $cmd}
}
. config -menu [menu .m] m+ File Open.. loadImg m+ File Save.. saveImg m+ File --- m+ File Exit exit
m+ Edit Blend {blend $im2 $im1}
m+ Edit Difference {difference $im2 $im1}
m+ Edit ---
m+ Edit Negative {invert $im2 $im1}
m+ Edit Contrast+ {contrast+ $im2 $im1}
m+ Edit Contrast- {contrast- $im2 $im1}
m+ Edit ---
m+ Edit Graylevel {$im2 copy $im1 -shrink; color2gray $im2}
m+ Edit Graylevel2 {$im2 copy $im1 -shrink; color2gray2 $im2}
m+ Edit "Add Noise" {
generic_1 [lambda x {expr {rand()<.01? int(rand()*255):$x}}] $im2 $im1
}
m+ Edit gray2color {$im2 copy $im1 -shrink; gray2color $im2}
m+ Edit Octary {generic_1 [lambda x {expr {$x>127? 255:0}}] $im2 $im1}
m+ Edit ---
m+ Edit HoriMirror {$im2 copy $im1 -shrink -subsample -1 1}
m+ Edit VertMirror {$im2 copy $im1 -shrink -subsample 1 -1}
m+ Edit "Upside down" {$im2 copy $im1 -shrink -subsample -1 -1}
m+ Edit ---
m+ Edit "Zoom x 2" {$im2 copy $im1 -shrink -zoom 2}
m+ Edit "Zoom x 3" {$im2 copy $im1 -shrink -zoom 3}
m+ Options "Accept (1<-2)" {$im1 copy $im2 -shrink}
m+ Options ---
m+ Options "Gray wedge" {grayWedge $im2}
m+ Options Histogram {drawHistogram $im2 $im1}
m+ Filter Clear {setFilter .t {{0 0 0} {0 0 0} {0 0 0}}}
m+ Filter ---
m+ Filter Blur0 {setFilter .t {{1 1 1} {1 0 1} {1 1 1}}}
m+ Filter Blur1 {setFilter .t {{1 1 1} {1 1 1} {1 1 1}}}
m+ Filter Gauss2 {setFilter .t {{1 2 1} {2 4 2} {1 2 1}}}
m+ Filter ---
m+ Filter Laplace5 {setFilter .t {{0 -1 0} {-1 5 -1} {0 -1 0}}}
m+ Filter Laplace9 {setFilter .t {{-1 -1 -1} {-1 9 -1} {-1 -1 -1}}}
m+ Filter LaplaceX {setFilter .t {{1 -2 1} {-2 5 -2} {1 -2 1}}}
m+ Filter ---
m+ Filter Emboss {setFilter .t {{2 0 0} {0 -1 0} {0 0 -1}}}
m+ Filter HoriEdge {setFilter .t {{-1 -1 -1} {0 0 0} {1 1 1}}}
m+ Filter VertEdge {setFilter .t {{-1 0 1} {-1 0 1} {-1 0 1}}}
m+ Filter SobelH {setFilter .t {{1 2 1} {0 0 0} {-1 -2 -1}}}
m+ Filter SobelV {setFilter .t {{1 0 -1} {2 0 -2} {1 0 -1}}}
bind . <Escape> {exec wish $argv0 &; exit}
bind . <F1> {console show}
loadImg aaa.jpg
}








