Win32下的Perl,无用的select,停滞的Tk,结束吧....
bugd
无论使界面还是内部功能的设计,大约是完成了一半了。今天完成了几个对条目状态进行判断和提示的函数,另外由于数据库是
sqlite
的,所以不得不写一个件事程序用于事务管理,来保证数据的更新。感觉这种
CS
形式的程序,最好还是不要用这种没有自带网络服务的数据库,实在是很麻烦。虽然现在用得还好,我是打算换成
pgsql
了。还好是用
DBI
写的,只需要修改一个数据库连接就行了。在数据库方面,
dump
一下应该没问题,我用的数据类型还是比较普通的。
在
bugd
中,总是会需要弹出一些信息框来收集一些用户选项,其中不乏类似于
combobox
这样提供一串信息的。针对不同的弹出框,应该有不同的信息来填充。更一般的说,针对不同的框,应该有其特有的一组数据。为了区别这些框,不得不维护一个全局变量来存储显示在最外层的
toplevel
的路径,在其退出之后再被重新赋予下一个层次的
toplevel
,直至到“
.
”。这不是一个好办法,因为为了存储这一系列的变化,必须对应数量可观的全局变量,至少两个:一个前一次,一个当前,而这仅仅是应对单层次弹出的情况。
其实很容易想到的一个办法是维护一个堆栈,一旦有新的
toplevel
出现,就被压入堆栈,销毁的时候在
pop
出来。我们不需要为此修改每一个
toplevel
的构造函数,只需要建立
<Map>
和
<Destroy>
的事件句柄。举个例子如下:
bind Toplevel <Map> {_tPush %W}
bind Toplevel <Destroy> {_tPop %W}
proc _tPush {w} {
global top_stack
if {[lindex $top_stack 0] ne $w} {
set top_stack [linsert $top_stack 0 $w]
}
}
proc _tPop {w} {
global top_stack
if {[lindex $top_stack 0] eq $w} {
set top_stack [lreplace $top_stack 0 0]
}
}
在写
bugd
的用户控制板块的时候,涉及到了比较多的弹出选项,虽然有差别,但是总体上还是比较类似的。比如说,在左侧的一列是用于说明的
label
,中间是接收输入的
entry
,右侧是可选的通过显示或隐藏错误图标来表示可用性的
label
。从行角度讲,除了信息输入项外,可能有用到
checkbuton
。

在还没有意识到重复性之前,我是一个接着一个地写这些代码的,最后弄得程序巨大而拖沓。因此想做一个抽象,来分离显示界面的逻辑与数据的分离,从而达到仅通过修改数据即可得到预期界面的效果。
首先,需要一个方便可用的
geometry manager
,可以考虑
pack
和
grid
。使用
pack
,除非设置好组件的宽度,不然很难对齐,但不同的组件对于宽度的单位也各不相同,会给抽象增加复杂性,所以在这种明显需要对齐的情况中,我个人比较倾向与使用
grid
。
接着,组件的数据表示要简洁而必要且一目了然,在这点上我花了点功夫,最后还是套用了在
bugd
中用于
C/S
间传递数据的格式。
proc frameData {} {
# widget:state:width:pathname:text
return {
{label:::unl:username entry:readonly:40:une: label::3:veriunl:}
{label:::pwdl:password entry::40:pwde:}
{{checkbutton:::chkadmin:if admin?} {}}
}
}
用
4
个“
:
”来作为分隔符(当然也可以用其他的)的一串信息,对应上面的注解。为了达到扩展的目的,使用空串来说明该位子由前一个可用组件来占据。
在处理到用户管理列表的时候,我还是新建了一个
toplevel
,原打算是要抽象用户自管理程序的,但是似乎有点麻烦,留着以后再做了。其中用到了借个
checkbotton
,是
ttk
版的。从文档里可以看出
ttk
的
checkbutton
省略了不少选项和命令,但反观原生的,也存在诸多不便。首先,它没有直接取得被选择状态数值的命令,只有一个
variable
选项,所以状态的操作必须围绕这个
variable
;第二,还是和这个
variable
有关,挡在过程中生成
checkbutton
而
variable
是非
global
的时候,显示出来的
checkbutton
是无法看出是否被
toggle
的,而反过来一旦被
toggle
上,这个
variable
就自动变成
global
的了。为了不必要的麻烦
,
我的解决办法是,在要使用到
checkbutton
的过程中直接设置
global
变量供
checkbutton
设为
variable
,当包含该
checkbutton
的
toplevel
销毁时一道
destroy
掉
global
变量。
有人想要一个可以远程关机的程序,想了下似乎相当地简单,无非是一个
cs
的
socket
,发一个命令过去,然后调用关机指令“
shutdown -s -t 0
”,于是机器就关掉了。很快就写完了,即使行而已的小玩意儿,转念一想总不好让要被关的机器整天开着个黑乎乎的命令行窗口吧,所以自然想到了要把它做成一个系统服务。
说起来,
pdk
的更新算是挺勤快的,一转眼到
7.3
了。其中有一个功能就是用来创建
win
下的系统服务的,但首先必须要遵循一些规则。
具体的可以在帮助文档里找到,这里只是简要地说明一些必要的东西,并举一个例子来说明。有三个东西时一定要定义的,分别是
$PerlSvc::Name
、
$PerlSvc::DisplayName
和
PerlSvc::startup()
,即服务名称、控制面板中显示的服务名称和主程序(也就是服务启动时要运行的程序)。其他的都是可有可无的,对于这么一个简单的东西,完全没有必要。
package PerlSvc;
our %Config;
sub Startup {
while (ContinueRun()) {
$local_addr=pack('SnC4x8', 2, 32080, 127,0,0,1);
socket(FILE_TRANS_SERV, 2, 1, getprotobyname('tcp')) or die("socket failed for $!");
bind(FILE_TRANS_SERV, $local_addr) or die("bind failed for $!");
listen(FILE_TRANS_SERV,3);
$remote_addr = '';
for(; $remote_addr=accept(FILE_TRANS_CLIENT,FILE_TRANS_SERV); close(FILE_TRANS_CLIENT)){
$in = <FILE_TRANS_CLIENT>;
while (<FILE_TRANS_CLIENT>) {}
if ($in =~ 'shutdown') {
close(FILE_TRANS_CLIENT); close(FILE_TRANS_SERV);
system "shutdown -s -t 0";
}
}
close(FILE_TRANS_SERV);
}
}
sub Pause { }
sub Continue { }
sub Interactive { }
sub Help { }
sub Install {
$Config{ServiceName} = 'MyShutdownService';
$Config{DisplayName} = 'My Shutdown Service';
}
sub Remove {
$Config{ServiceName} = 'MyShutdownService';
}
package main;
1;
在
startup
中,我开了一个监听
socket
,等待接收关机指令,另外最好将socket的建立和注销过程写在一个while循环中,避免不必要的逻辑错误。在
Install
中定义了两个服务名称。
(学生简化版下载)写一个给主持人用的提词软件,包括两个部分。其一是满屏滚词,就是用来将接收到的文字滚动显示到屏幕上的软件,其中用到了两个主要命令和一个socket服务器。第一个命令是“wm attribute PATH
–
fullscreen 1”,用于设置全屏;第二个是“TXT_PATH yview scroll NUM UNITS”用于滚屏,其中NUM是滚过的数量,它可以是一个负数,说明滚动是反向的,UNITS是
NUM
的单位,可以是pixel或者line等。
S
ocket服务器就是将接收来的数据分门别类,用于文字显示或者设置滚动速度和字体。还有一个软件是用来输入文字和设置滚动速度和字体的软件,从socket角度讲就是一个客户端,虽然代码量比前者大,但功能比较简单,
没什么好说的
。(点击下载)
最近一直在写一个文件传阅系统,其中涉及到一个包含显示文件状态的text的notebook。虽然和本文无关,但还是要说一下的是,text最好指定一个height,不然在fill和expand之后,会在notebook中占据显示空间,我在这里用到的数值是7。还有的是,在布局notebook时可以考虑使用panedwindow,它可以缩放组件占据面积的大小。
言归正传,tooltip是用来为比如像工具栏中的按钮提供动态提示的组件。查看源代码,我们可以看到,被bind的Tooltip不是具体关于tooltip本身的,通过bindtag它被添加到需要被显示信息的组件的事件处理队列中。一旦比如Enter或者Leave事件发生,将运行tooltip中相应的函数。
具体用于显示信息的路径名为.__tooltip__的toplevel中只有一个label,有时候我们需要增加一些显示信息,比如图片之类的时候,就显得很不够。我们可以通过在.__tooltip__中增加一个canvas或者text来实现。对于原存在的label,我建议最多做一些configure,将其的anchor、bd、bg分别设为w、0、#ffffff,它便如个title一样,不会对显示效果产生太大影响。
但是还是存在一些局限,例如同时存在需要显示原状和增加了现实效果这两种tip时,上面的这种办法就无能为力了,因为.__tooltip__是被共享的,修改就意味着所有需要显示tip的组件都将为此使用新的风格。所以,可以确定的是,我们需要有两种显示状态。解决的办法按照深度可分为三种:第一种,在原先的.__tooltip__中针对不同的组件来withdrawn新增的效果,这需要做出一些判断,甚至在构造函数中增加一个选项;第二,在tooltip的hash变量G中除了TOPLEVEL之外,再增加一个例如TOPLEVEL_2这样的key值,也就是说再造一个专门用于显示特殊效果的组件,当然判断还是需要的;第三种也是最简单的一种,重建一个用来显示特殊效果tip的package,说起来好像很难,其实只是替换tooltip和Tooltip这两个关键词即可。
最近一直在研究Tcl/Tk,而且有点乐不思蜀了,这里的“蜀”说的是Perl,因为一直用Perl的缘故,现在倒是有点冷落它了。介绍一下最近实现的一个dateentry,其实借鉴了很多iwidgets::dateentry的思路,但是由于该package年代久远,现在看来却是有点惨不忍睹,所以不得不重新写一个。
以上就是最终的控件样式,因为比较喜欢苹果的风格,所以总体上以白色为主,比较清新。代码的实现上,主要是解决一下几个问题。
首先,如何布局,因为使用canvas的缘故,所有的内部组件必须使用x、y坐标的形式放置,如此一来就不得不小心地控制所有组件的位置。
第二,其中的日期按钮都是复用的,也就是说某个月中产生的日期按钮在下一个月份更新时并不会被注销,而是改变了text。这么一来解决了组件路径冲突的问题,而8.6中将可能可以使用%来自动生成随机数。
第三,日历的显示,使用了overrideredirect来隐藏了toplevel的边框。使用geometry,并根据dateentry的label位置,来定位日历,使其能够紧贴控件显示。
第四,点击除日历外的任意位置,都会注销日历。这么作完全是为了符合windows使用的习惯,当然这样也很方便。需要使用“grub -global $toplevel”将事件的捕获限定在当前显示的日历中,然后通过判定鼠标位置是否在日历内,来决定是否该注销日历。
这是一个示例,并没有打包,使用的时候,直接运行即可,但需要安装tcltk解析器。如需应用到具体程序中可电邮我,独家发布,欢迎使用。
download
用
TclTk
重写了视频录制程序,其中有些细节不便在前一篇《
Perl
程序转换为
Tcl/Tk
实例一则》中细述,因为它主要是讲用两种语言来实现的差别。另外,后来我又对程序做了一些修改,所以不得不再费力写下来了。
首先对于在
Tk
中使用到的命令,因为循环的关系,我们不需要将它放置在
Tk
界面代码之前。但是如果在循环之外使用,那么就只能在用到什么之前,就定义什么。这是一个细节,还是要注意一下,免得调试的时候手忙脚乱。
最主要提到的有两方面,其一是关于如何正确关闭管道。如果只是简单地
close
掉管道,除非子进程会自动关闭,不然可能一直运行下去。比如:
catch {open "|ping 127.0.0.1 -t"} output
close $output
(使用
catch
来捕捉错误,阻止烦人的错误提示)
打开任务管理器,你会看见
ping
进程依然在执行,因此这并没有达到彻底关闭的效果,这在
Perl
里也是一样的。需要显示地将进程关闭掉,我们可以使用一下的命令:
catch {exec taskkill /F /PID [pid $pipe]}
在这里,调用了系统命令
taskkill
来关闭进程,
pid
的作用是返回一个管道子进程的
pid
值,如果没有该进程将返回一个错误,并被外围的
catch
捕获。
另外的一个是关于如何输出的问题,我们使用
gets $pipe line
来得到管道的输出,之后通过一定的过滤,最终显示到
Text
上,我用两种结尾的输出试验了一下。用“
\r
”的时候,
cpu
使用率一度达到
100%
,最终程序僵死,甚至连子进程都无法正常退出。用“
\n
”则恢复正常了,
cpu
使用率很低,亦不会出现阻塞。另外,为了不输出空行,做了一些必要的检测:
set line [string trim $line]
if [string compare $line {}] {
$log insert end [string trim $line]\n
$log see end
}
每种语言都有其擅长和不足的地方,在某项工作甚至工作的某一部分中选择合适的语言往往可以达到事半功倍的效果。所以,多掌握一些知识是很有必要的,当然了也可以用一些大语言,比如说
C
或者
Java
,它们几乎是可以通吃一切问题的,但是付出的代价一样高昂,一些可以用几行
Perl
或者
Python
实现的功能,换作
Java
可能需要花费几倍的工作量,所以这里就不谈了。
在很久以前,为了方便工作人员录制电视节目,我用
Perl
写了一个基于
Tk
的设置界面。界面如下图:
大致上就是设置一个时间,然后在该时间点自动启动一个录制程序。当时由于时间仓促,本着够用就好的精神,很快就写好了,所以难免留下一些
bug
。比如说设置完之后,没有将按钮重置为退出。这还是小事,最主要的还是一旦录制程序启动,整个界面就被阻塞,这是
windows
下
Perl
的老毛病了,几乎无药可救。
其实即便如此,这个程序还是可以顺利使用的,但是作为一个解决长时间运算阻塞界面的办法,将它移植到
Tcl
上,还是有一些意义的。在上一篇随笔中,我大致讲到了
filevent
和
select
在
windows
下的差别,正好可以在这里验证一下,同时通过移植的过程,也可以更清楚地了解和区分两种语言在某些功能上的不同实现办法。最终实现的工具如下图
:
这里将代码拆分开来进行分析。
首先是界面部分,这一部分其实是最简单的,所以在实现上几乎一样,只不过是语法上的差别而已。有一点值得注意的是
Tcl
中是不需要的
mainloop
这样的命令来显式地启动主循环。
第二,因为定时启动需要计算启动时间与当前事件的差值,所以需要处理一些时间相关的参数。
Perl
中可以使用
localtime
来得到年份、月、日和秒等共
9
个值,供组合使用。在
Tcl
中,有一个
clock
命令,通过
format
可以实现类似的功能,感觉上
Tcl
的更方便一些。相同的感觉也处理时间差值时也存在,因为
Perl
中需要拆分一个某种形式的时间表达式,提取必要参数给
timelocal
计算总秒数,而
Tcl
的
clock
有
scan
可以直接生成。
第三,在像
Perl
的
BrowseEntry
和
Tcl
的
ComboBox
的下拉框中,经常会插入一串连续的数字。
Perl
可以使用形如“(
1..10
)”来表示
1-10
这
10
个数字,但是在
Tcl
里没有类似的命令,所以当遇到大数组表达式的时候,最好是写一个方法来模拟
Perl
中的这种功能。
proc listOfNum {from to {step 1}} {
set lst [list ]
set index $from
while {$index <= $to} {
lappend lst $index
set index [expr $index + $step]
}
return $lst
}
第四,为了确保录制任务在制定的时间点启动,需要有检查是否超时。在
Tcl
里这个实现因为有
after
变得很轻松,只要指定一个需要等待时间戳,
Tcl
就会在过晚这段时间之后启动需要执行的指令,而
Perl
就麻烦多了。在
Tk
的
mainloop
中,使用
repeat(1000, sub {…})
使其每隔一秒钟执行一次检查,看是否超时。一旦检查到超时则停止检查并启动录制;若没有则提示继续下次检查。
第五,在如何启动录制这点上,两者使用了不同的手段。
Tcl
使用管道来启动进程,使得自身不被阻塞(不过在使用producer的时候还是会阻塞,用ping就没问题,这个很奇怪),并且使用
filevent
监测打开句柄的状态。而
Perl
因为不可靠的
select
,我们只能使用
system
,通过阻塞的办法来获知任务是否完成,这也是一个无可奈何的事情了。
(代码下载)