通用 Lisp/外部库/Ltk
Ltk 是一个用于通用 Lisp 的可移植 Tk 绑定集。Tk 是一个图形“工具包”,是一个通过提供控件(例如按钮和下拉菜单等图形元素)使 GUI 构建更容易的库。Tk 最初是作为 Tcl/Tk 的一部分而诞生的,并获得了极大的普及,Tcl/Tk 是一种动态的 Tcl 语言与易于使用的 Tk 工具包配对。由于 Tcl/Tk 的普及,大多数系统上都存在实现,而 Ltk 可以移植到任何这样的系统上。
Ltk 并不是通常意义上的绑定,它实际上是对wishshell 的一个接口,一个解释用于构建窗口的命令的 shell。这使得 Ltk 能够通过向运行wish的外部进程发送简单的命令流来操作,而不是大多数其他 GUI 构建器中所需的链接。虽然向wish解释器传递命令可能会导致程序运行速度变慢,但它使 Ltk 包变得非常容易构建、使用、扩展和调试,并且它消除了对任何 FFI 的需求。这些特性与大多数其他通用 Lisp GUI 构建器形成鲜明对比。
使用 Ltk 构建 GUI 的程序需要三个主要部分。它需要创建控件,将控件放置在窗口上,然后关联控件激活时的操作。Ltk 程序通常具有以下基本格式
(with-ltk ()
(let* (;; Creating widgets
(widget1 (make-instance 'widget))
...more widgets... )
;; Associating actions with widgets
(bind widget1 event (lambda (evt) ...do something...))
;; Placing widgets on the window
(pack widget1) ))
Ltk 的操作基于 CLOS 对象的实例化。要创建任何特定控件,您需要创建一个该类的实例。不需要保留该实例,但如果保留了该实例,则可以用来改变控件的属性。
Ltk 包含以下常见控件:[1]
- 按钮
- 复选框
- 单选按钮
- 文本输入字段和文本编辑框
- 列表框
- 菜单栏
- 框架和滚动框架
- 画布和滚动画布
如果这个列表看起来很短,您并没有错。这些控件将使您能够创建许多有用的程序,但您会发现它们在某些任务(例如下载进度指示器)中不够用。稍后我们将了解如何添加新控件。
使用 Ltk(与 Tk 一样),一旦创建了控件,它就不可见,直到您告诉wish它在窗口中的位置。Ltk 有两个几何管理器,pack和grid. Pack将控件视为盒子,并将其水平或垂直打包。Grid将控件布局在规则的网格上。在这两者中,pack更常用。
与大多数图形界面一样,Ltk 中的所有计算本质上都是事件驱动的。这意味着在您的窗口内发生的任何事件(例如按键或鼠标点击)都会由窗口系统发送到您的程序。然后 Ltk 会查找您通常定义的与该事件关联的处理程序函数,并调用它们。可以通过两种方式建立事件处理程序,通过command槽在许多控件中以及bind函数。该bind函数比该command槽方法更复杂,但bind是一种将函数与事件关联的更通用和更灵活的方法。
(with-ltk ()
(let* ((button (make-instance
'button :text "Press Me!"
:command (lambda ()
(do-msg "Hello World!") ))))
(pack button :pady 30 :padx 30) ))
为了使控件可见,必须将它们放置在窗口上。这可以通过以下三个函数之一来完成:‘place’, ‘packpackgrid’ 或 ‘grid’。每个函数用于不同的情况。在一些简单的情况下,可以通过将放置方法指定为
放置控件
make-instance的关键字参数来放置控件。我们将依次讨论每种方法。place[编辑 | 编辑源代码]
(with-ltk ()
(let ((button (make-instance 'button :text "A Button")))
;; Place BUTTON at x=50, y=40
(place button 50 40 :width 100 :height 50) ))
最基本的方式是在窗口上放置控件,就是指定 x 和 y 位置以及宽度和高度。‘
place’ 函数将为您执行此任务。但是,这通常比需要做的更难(与我们接下来要探讨的方法相比),因此不建议将其用于任何目的。
Pack 几何(defun button-on? (button)
"Tell me if the button is 'on' or not."
(equal (text button) "X") )
(defun toggle-button (b)
"Toggle one button from an X to a Space or a Space to an X."
(if (button-on? b)
;; To change a widget's text, ltk doesn't use CONFIGURE, but
;; instead you just SETF the text
(setf (text b) " ")
(setf (text b) "X") ))
(defun toggle-block (buttons i j n m)
"Change the button, and the buttons neighboring it \(if they are in
bounds)."
(toggle-button (aref buttons i j))
(when (< (1+ i) n) (toggle-button (aref buttons (1+ i) j)))
(when (< (1+ j) m) (toggle-button (aref buttons i (1+ j))))
(when (>= (1- i) 0) (toggle-button (aref buttons (1- i) j)))
(when (>= (1- j) 0) (toggle-button (aref buttons i (1- j)))) )
(defun lights-out (n m)
"Create an N by M lights out game."
(with-ltk ()
(let* ( ;; Create button widgets and store them for later
(buttons
(coerce
(iter (for i below (* n m))
(collect (make-instance 'button
:text " " )))
'vector ))
;; Displace the button vector into a 2D array representing
;; the n by m grid. With this we can reference buttons by
;; a pair of indicies, i and j.
(b-array
(let ((array (make-array (list n m) :displaced-to buttons)))
;; Some iteration stuff to build a random (yet solvable)
;; selection of `X's and spaces.
(iter (for i below n)
(iter (for j below m)
(when (= 1 (random 2))
(toggle-block array i j n m) )))
array )))
(iter
;; Iterate over buttons
(for b in-vector buttons)
;; Keep a numerical index of what button we are on.
(for tot below (* n m))
;; Calculate the indicies, i and j, on the grid
(for i = (mod tot m)) (for j = (floor tot m))
;; Bind a button press on one of the buttons to flip its `X'
;; to a space (or vice verse) and toggle its neighbors as well.
(bind b "<Button-1>"
(let ((i i)
(j j) )
(lambda (evt)
(declare (ignore evt))
(toggle-block b-array j i n m)
;; Check if you have solved the problem
(when (iter (for button in-vector buttons)
(never (button-on? button)) )
(do-msg "You have won!!!" "You a winner") ))))
;; Here, we will use the GRID geometry manager. This makes
;; sense, we are building a grid...
(grid b i j) ))))
[编辑 | 编辑源代码]这是一个视频游戏 Lights Out 的模拟。我们使用geometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
使用 ‘configure这是一个视频游戏 Lights Out 的模拟。我们使用’ 修改控件
用于更改控件的各种属性,例如颜色和字体。bind绑定
来实现这一点,该函数会修改控件的事件处理程序列表。
- Canvas 控件允许您绘制任意矢量图形。修改 Canvas 项目geometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
- Canvas 控件允许您绘制任意矢量图形。[编辑 | 编辑源代码]geometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
- Canvas 控件允许您绘制任意矢量图形。‘geometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
- Canvas 控件允许您绘制任意矢量图形。itemconfiguregeometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
- Canvas 控件允许您绘制任意矢量图形。itemmoveitembinditemdeletegeometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
Ltk 可以在小部件中显示位图。默认情况下,Tk(以及 Ltk)支持GIF, PBM,以及PPM图像,这些图像在如今很少使用。为了使用现代格式,例如PNG和JPEG你需要安装libimgTcl/Tk 的扩展库(例如,在 Ubuntu 中,你需要安装libtk-img软件包)并在使用前要求该软件包(见下面的代码)。
以下代码片段创建一个带有画布对象的 Ltk 窗口。然后它加载由路径名指定的图像filename并将其放置在画布上。画布之外的任何内容都不会绘制。 300x300 像素画布外的任何内容都不会绘制。
(defun display-image (filename)
(with-ltk ()
(format-wish "package require Img")
(let* ((img (make-image))
(c (make-instance 'canvas :height 300 :width 300)) )
;; Pack the canvas
(pack c)
;; Load the image from the file
(image-load img filename)
;; Draw the image on the canvas
(create-image c 0 0 :image img) )))
的返回值create-image是一个画布项目标识符。它可以被保存,以便图像可以被修改(例如,像画布项目修改示例中那样移动)。
位图不限于画布小部件。它们也可以放在按钮上,而不是文本。
(defun click-change (filename1 filename2)
"Make a button that switches between two different images whenever pressed."
(with-ltk ()
(format-wish "package require Img")
(let* ((img (make-image))
;; We specify text here, but it is overridden by the :image keyword
(b (make-instance 'button :text "hello!" :image img
:command
(let ((imgs (make-circular-list 2 :initial-element filename2)))
(setf (car imgs) filename1)
(lambda ()
(setf imgs (cdr imgs))
(image-load img (car imgs)) )))))
;; Load the image from the file
(image-load img filename1)
;; Pack the button
(pack b) )))
线程、冗长计算和 ‘process-eventsgeometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
[编辑 | 编辑源代码]事件驱动系统的基本结构可能如下所示
(loop
(let ((event (get-next-event)))
(case (name-of event)
(:window-resize (funcall *window-resize-handler* event))
(:button-click (funcall *button-click-handler* event))
(:keypress (funcall *keypress-handler* event))
... )))
这被称为事件循环或事件泵。事件循环的目的是将用户执行的操作映射到定义的处理函数。用户执行的任何操作都会导致新的事件被插入队列中。函数get-next-event从队列中取出下一个事件,case 语句将其分派到正确的位置。如果处理程序没有及时返回,这会导致程序无响应。
为了让 Ltk 用于更多应用程序,我们需要避免这种无响应行为的方法,以便我们可以执行诸如添加取消按钮以停止冗长计算等操作。有几种处理此问题的方法;我们将讨论三种方法
- 在 Tk 事件循环中设置计时器事件。
- 定期暂停计算以通过以下方式显式地处理事件process-events.
- 使用 Lisp 实现的内部线程将计算与运行事件循环的线程分离。当然,这只适用于支持线程的 Lisp 实现。
计时器和 ‘aftergeometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
[编辑 | 编辑源代码]在某些情况下,处理程序返回缓慢是因为您正在等待某些事情发生,而不是在计算。在这种情况下,处理程序的正确行为是立即返回,但在返回之前告诉事件循环在稍后重新尝试。这是通过插入计时器事件来完成的,该事件将在您指定的延迟后从队列中取出。计时器事件的设置使用after函数。
‘process-eventsitembindafter-idlegeometry 管理器来组装一个 NxM 的按钮数组,其中每个按钮都处于“开”或“关”状态。为了简单起见,我们将“开”表示为“X”,而“关”则表示无文本。每次点击按钮时,它都会切换按钮及其四个相邻按钮在空白和“X”状态之间的状态。目标是将所有按钮都改为空白。
[编辑 | 编辑源代码]为了使用after,我们的任务必须只是等待(在我们上面的示例中,等待套接字上的输入)并且易于挂起和恢复。如果不是这样,那么您还有另一种选择,process-events。用process-event,它不会插入新的事件并允许 LTK 在此期间处理其他事件,而是要求主循环处理当前正在等待的队列中的所有事件,然后返回并允许您的代码继续执行。
(defun count-down (n)
(with-ltk ()
(let* ((cvs (make-instance 'canvas :height 400 :width 400))
(text (create-text cvs 100 100 "Count down: ")) )
(pack cvs)
(force-focus cvs)
(bind cvs "<q>"
(lambda (evt)
(declare (ignore evt))
(return-from count-down) ))
(iter (for i from n downto 0)
(itemconfigure cvs text
:text (format nil "Count down: ~A" i) )
;; We must explicitly process events since the event loop isn't
;; running yet (not until after the body of WITH-LTK)
(process-events)
;; Pretend this sleep command is actually doing some work...
(sleep 1/100) ))))
类似地,after-idle的行为类似于after,只是它在事件队列被清空后才会被处理,但没有固有的延迟。这具有process-events的基本行为,但它不会在函数中间恢复,而是触发一个新的函数调用。如果您的代码不仅仅是等待,而且易于挂起和恢复,那么它很有用。
使用线程是解决此问题的最有效的方法。但是,这种强大功能也带来了很大的复杂性。所有与线程程序相关的问题都会出现。Ltk 还存在一个额外的陷阱。这with-ltk表单为您的 Ltk 命令设置了一个特殊的环境。但是,动态变量的设计并非词法捕获。这意味着与事件绑定的函数不一定在设置了 Ltk 动态环境的环境中执行(尽管看起来应该如此)。虽然这也会出现在单线程 Ltk 程序中,但在多线程程序中会一直发生。解决方法是强制函数词法闭包到所需的环境,并在函数内部重新创建环境。
Tk(以及 Ltk)支持远程会话。在远程会话中,程序在服务器上运行,但显示发生在其他地方,即客户端计算机上。显示信息以一系列wish命令的形式传输,因此这种系统的效率并不差。对于 X11 用户来说,这与 X11 转发非常类似。
- ↑ 有关受支持小部件的最新完整列表,请检查
ltk.lisp
文件。
- Common Lisp Wiki: LTK
- Ltk 主页 和 文档
- Tcl 编程 — 关于 Tcl/Tk 编程的 Wikibook。