关于
text
这个组件,说起来简单,其实却是又相当复杂,今天我从一个带
linum
的
text
出发,管中窥豹,以期抛砖引玉。
使用
emacs
的大多会安装行号显示,便于
debug
时的定位,比较常用的便是
linum.el
。这里的
linum
是
line number
的意思。现在我们来实现这种效果,以达到当发生输入、粘贴、删除或者退格事件时,左侧行号自动匹配实际文本行数。
首先需要设置行号的显示方式,这是最基础但却是很重要的一个决定。倘若是在单
text
组件中使用
tag
实现
linum
,那么有两个问题。第一,无法保证
linum
的只读状态;第二,复制时会包含行号,大多数情况下这是多余的。这里我选择了两个组件的粘合,事实上是将两个
text
拼在一起,当然也可以使用
entry
或者
canvas
,只是前者比较方便,容易保持代码的一致性。在此之前,最好设置一个用于记录最后行号值的变量
linum
,初始化为
1
。
package require Tk
set linum 1
frame .f
text .f.linum -width 1 -bg gray -bd 0
.f.linum insert end "1"
.f.linum configure -state disable
text .f.content -bd 0
pack .f.linum -side left -expand 1 -fill y
pack .f.content -side left -expand 1 -fill
both
pack .f
我们得到了如下图片显示的记事本,当然目前它还不能正常添加或删除行号,需要定义并绑定一系列过程。另外,随着行号的增加,应该相应设置左侧灰带的宽度来正常显示行号。
无论是因为何种文本编辑操作使得文本行数发生改变,在改变行号之前,我们都必须得到文本行的数量。这里有几种办法,比如
count
或者
index
,我们使用后者,顺便对
index
进行介绍。
proc linum {w} {
return [expr int([$w index "end - 1c"])]
}
此处的
index
是“
end
– 1c
”,表示文本的最后的前一个位置。事实上,这就是文本的实际最后位置,只不过因为
end
被定义为“
the character just after the last newline
”,一个不存在字符,所以必须在
end
的位置处再向前减去一个
character
,可以简写为“
- 1c
”。同样的,如果要得到待插入行的最后一个字符的位置,应使用“
insert lineend
”;向前一个字符就是“
insert lineend – 1c
”,如此类推。
现在要切实做一些绑定了,一般地我们使用回车和退格来进行换行,其中回车是必然转换为一个换行符的,而退格则在遇到换行符时才会通过一系列的操作实现换行。还是应该提一下,通过
ascii
码我们可以看到回车并不是换行,换行的码值是
10
,而回车是
13
,所以需要转换;而退格换行的行为,在实际中行数并不会立即发生变化,在退格时先增加一个退格符,再将该退格符连同上一个换行符一起删除。需要注意的是,文本永远在其最后保留一个换行符。
明白了这个细节,接下来的事情就好办了。由于我们还需要响应长按的情况,所以必须绑定是
KeyPress
过程,但无论是
KeyPress
还是
KeyRelease
,当遇到换行时
text
都会在之后做做一些以上描述的小手脚,所以真正的绑定处理过程必须发生在这些小手脚完成之后。这是一个矛盾,但并非不可调和。
after idle incrLinum
在绑定中,使用“
after idle
”的小技巧便可以轻松应对。它让
text
先干完想干的,一旦空下来就立即执行真正的处理过程。
除了回车和退格,还应该增加对一些常用的比如粘贴或删除操作的支持。对于粘贴,通常的快捷键是“
ctrl v
”,对应的事件是“
<Control-Key-v>
”,当然也可以是如
emacs
中的“
ctrl y
”。使用以下的形式可以方便地增加对事件的支持。
foreach keysum {
<KeyPress-Return>
<Control-Key-v>
} {
bind .f.content $keysum {
after
idle incrLinum
}
}
点击下载代码。
另外,添加比如语法着色也并非难事,只是肯定会相当复杂,以后再说了。