跳转到内容

通用 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

[编辑 | 编辑源代码]

使用 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 有两个几何管理器,packgrid. 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 几何

[编辑 | 编辑源代码]grid网格几何

(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绑定

[编辑 | 编辑源代码]

在 Tk 的意义上,绑定是将操作与事件关联起来。我们通过调用函数

来实现这一点,该函数会修改控件的事件处理程序列表。

Canvas 控件

[编辑 | 编辑源代码]
  • 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”状态之间的状态。目标是将所有按钮都改为空白。

itemlower

[编辑 | 编辑源代码]

Ltk 可以在小部件中显示位图。默认情况下,Tk(以及 Ltk)支持GIF, PBM,以及PPM图像,这些图像在如今很少使用。为了使用现代格式,例如PNGJPEG你需要安装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 用于更多应用程序,我们需要避免这种无响应行为的方法,以便我们可以执行诸如添加取消按钮以停止冗长计算等操作。有几种处理此问题的方法;我们将讨论三种方法

  1. 在 Tk 事件循环中设置计时器事件。
  2. 定期暂停计算以通过以下方式显式地处理事件process-events.
  3. 使用 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 应用程序

[编辑 | 编辑源代码]

使用线程是解决此问题的最有效的方法。但是,这种强大功能也带来了很大的复杂性。所有与线程程序相关的问题都会出现。Ltk 还存在一个额外的陷阱。这with-ltk表单为您的 Ltk 命令设置了一个特殊的环境。但是,动态变量的设计并非词法捕获。这意味着与事件绑定的函数不一定在设置了 Ltk 动态环境的环境中执行(尽管看起来应该如此)。虽然这也会出现在单线程 Ltk 程序中,但在多线程程序中会一直发生。解决方法是强制函数词法闭包到所需的环境,并在函数内部重新创建环境。

远程 Ltk 会话

[编辑 | 编辑源代码]

Tk(以及 Ltk)支持远程会话。在远程会话中,程序在服务器上运行,但显示发生在其他地方,即客户端计算机上。显示信息以一系列wish命令的形式传输,因此这种系统的效率并不差。对于 X11 用户来说,这与 X11 转发非常类似。

Ltk 内部结构

[编辑 | 编辑源代码]

扩展 Ltk

[编辑 | 编辑源代码]
  1. 有关受支持小部件的最新完整列表,请检查 ltk.lisp 文件。

进一步阅读

[编辑 | 编辑源代码]
华夏公益教科书