About

是的, 我需要一点点上古遗物. 本来打算从 CLIM 2 Spec 开始看, 然后写一个类似的东西 (或者写一个 backend), 但是看了几页之后就直到自己太菜了, (对 GUI 的 API 完全没有什么概念, 更不要说如何去实现了) 所以现在先建立在已有的 McCLIM 或者 clim-tos 的基础上, 快速的做一些简单的 GUI 先. 或者能不能用类似于 CLOG 的框架呢?

这里参考的是 Common Lisp Modules: Artificial Intelligence in the Era of Neural Network and Chaos Theory 一书, 你应该可以在网络上找到它的扫描版. 之后还想要读一点后面的部分.

Interface

(defpackage #:cl-module/plot
  (:use :clim :clim-lisp)
  (:export #:init-plot
           #:plot-fill-rect
           #:plot-size-rect
           #:clear-plot
           #:pen-width
           #:plot-frame-rect
           #:plot-line
           #:show-plot
           #:plot-string
           #:plot-string-bold
           #:plot-string-italic
           #:plot-mouse-down))

(in-package :cl-module/plot)

原文中使用的是 Macintosh Common Lisp (现在我也没法搞到能运行的 MCL 了, 就连模拟器都跑不了… 这让我比较痛苦面具). 所以估计得要重新全部重写了.

我现在使用的是 CLIM-TOS, 理论上你可以用 McCLIM, 但是我的电脑上有一个报错 (Unknown &KEY argument: :MAX-MEASURE-WS). 虽然我的建议是用 McCLIM, 因为它仍在被更新, 并且有相对更加好看的界面, 并且 BUG 也会少一点.

另一个需要注意的事情是: 由于原本的程序是面向于 MCL (大概是 PPC 甚至之前的 Macintosh 机器), 所以对色彩非常的保守 (用的是 5 阶灰度). 所以会做一些简单的修改.

所以简单来说, 就是这个代码是建立在 CLIM 2 标准 (CLIM-TOS), 参考 Common Lisp Modules: Artificial Intelligence in the Era of Neural Network and Chaos Theory 一书中的 Chapter 2 进行构建的.

Implementation

Utils

Plot Frame

(define-application-frame plot ()
  ()
  (:panes 
   (display :application
            :min-width 100 :min-height 100
            :width     400 :height     400))
  (:layouts 
   (:default
    (vertically () display))))

(define-plot-command (com-quit :menu t) ()
  (frame-exit *application-frame*))

(define-plot-command (com-clear :menu t) ()
  (window-clear (get-frame-pane *application-frame* 'display)))

Quick Wrapper Macro

(defmacro with-in-plot-pane ((pane &key (coordinate :origin)) &body body)
  "Bind var `pane' with `*plot-frame*' display pane.
  The `coordinate' could be:
  + `:origin' (default) for top-left corner positioned
  + `:local' for centered positioned"
  (if (eq coordinate :local)
      (alexandria:with-gensyms (w h)
        `(let* ((,pane (get-frame-pane *plot-frame* 'display)))
           (multiple-value-bind (,w ,h)
               (window-inside-size ,pane)
             (with-translation (,pane (round ,w 2) (round ,h 2))
               ,@body))))
      `(let ((,pane (get-frame-pane *plot-frame* 'display)))
         ,@body)))

General Impelement

;; The global plot window object
(defparameter *plot-frame* nil
  "The graphic frame to plot.")

;; Initialize a standard plot window
(defun init-plot (&key (width 400) (height 400))
  "Creates a graphics window, return a window stream."
  (setf *plot-frame*
        (make-application-frame 'plot :width width :height height)))

(defun show-plot (&key (background t) (name "PLOT"))
  "Open the `*plot-frame*' window."
  (unless *plot-frame*
    (warn "Plot not initilized yet, auto inited.")
    (init-plot))
  (flet ((run () (run-frame-top-level *plot-frame*)))
    (if background
        (bt:make-thread #'run
                        :name name
                        :initial-bindings
                        `((*default-server-path* . ',*default-server-path*)))
        (run))))

(defun clear-plot ()
  "Clears the graphics window."
  (window-clear (get-frame-pane *plot-frame* 'display)))

Graph Methods

;; Colors: use CLIM color system

;; Graphing Settings
(defparameter *default-pen-width* 1
  "Default pen witdth (unit by px).")

(defun pen-width (px)
  "Sets the pen drawing width."
  (setf *default-pen-width* px))

;; Graphing Methods
(defun plot-fill-rect (x y xsize ysize color)
  "Fills a rectangle with `color'."
  (with-in-plot-pane (pane)
    (draw-rectangle* pane x y (+ x xsize) (+ y ysize)
                     :ink color :filled t
                     :line-thickness 0)))

(defun plot-size-rect (x y xsize ysize max-width &key (color +black+))
  "Plots a rectangle with `max-width'."
  (let* ((width (min xsize max-width))
         (scale (/ width xsize)))
    (with-in-plot-pane (pane)
      (draw-rectangle* pane x y (+ x (* scale xsize)) (+ y (* scale ysize))
                       :line-thickness 0 :ink color))))

(defun plot-frame-rect (x y xsize ysize
                        &key (color +black+) (pen-width *default-pen-width*))
  "Plots a framed rectangle."
  (with-in-plot-pane (pane)
    (draw-rectangle* pane x y (+ x xsize) (+ y ysize)
                     :line-thickness pen-width
                     :ink color
                     :filled nil)))

(defun plot-line (x1 y1 x2 y2
                  &key (color +black+) (pen-width *default-pen-width*))
  "Plots a line between two points."
  (with-in-plot-pane (pane)
    (draw-line* pane x1 y1 x2 y2
                :line-thickness pen-width
                :ink color)))

(defun plot-string (x y str &key (size 10))
  "Plots a string at position (x y)."
  (with-in-plot-pane (pane)
    (draw-text* pane str x y :text-size size)))

(defun plot-string-bold (x y str &key (size 10) (color +black+))
  "Plots a bold string at posistion (x y). "
  (with-in-plot-pane (pane)
    (draw-text* pane str x y
                :text-face :bold
                :text-size size
                :ink color)))

(defun plot-string-italic (x y str &key (size 10) (color +black+))
  "Plots a italic string at posistion (x y). "
  (with-in-plot-pane (pane)
    (draw-text* pane str x y
                :text-face :italic
                :text-size size
                :ink color)))

Inputs

;; Inputs
(defun plot-mouse-down ()
  "Returns position of mouse click."
  (with-in-plot-pane (pane)
    (tracking-pointer (pane)
      (:pointer-button-press
       (event)
       (return
         (values (pointer-event-x event)
                 (pointer-event-y event)))))))

这里我感觉到 CLIM 的爽了, 实现一些功能随便糊糊就可以实现了, 并且还可以有比较细致的控制… 不过感觉 CLIM 的缺点就是实在是太大了, 没有一个很好的, 非常全面的入门文档来介绍. 明明能够处理的事情也很多, 从底层到高层都可以, 甚至整体的体验还很统一, 可惜没有一个好的推广啊.

虽然有点怀疑我这么糊真的可以吗? 管他呢.

Test

(defun test ()
  (init-plot)
  (show-plot)
  (clear-plot)
  ;; The XQuartz on my macos need some delay time to boot?
  #+darwin
  (sleep 0.1)
  (flet ((gray6 (level) (make-gray-color (/ level 6))))
    (dotimes (i 6)
      (plot-fill-rect  (* i 9) (* i 9) 8 8 (gray6 i))
      (plot-frame-rect (* i 9) (* i 9) 8 8))
    (dotimes (i 100)
      (plot-size-rect (+ 160 (random 200)) (random 100)
                      (1+ (random 10)) (1+ (random 10))
                      (random 5)))
    (dotimes (i 4)
      (plot-string (* i 10) (+ 150 (* i 22))
                   "Mark's plot utilities..."
                   :size (* (1+ i) 5)))
    (plot-string-bold 20 260 "This is a test... of BOLD" :size 20)
    (plot-string-bold 20 300 "This is a test... of ITALIC" :size 30)))

/_img/lisp/cl-module/plot-test.png

啊… 不知道是不是字体缺失导致斜体和粗体没法正常显示, 但是做到目前这样大概就好了吗?

结束

大概就是这样, 你应该可以在 plot.lisp 这里下载全部的代码. 至于 clim-tos, 可以通过 git clone https://gitlab.common-lisp.net/mcclim/gramps-clim2.git, 放在 ~/quicklisp/local-project/ 下, 然后通过 quicklisp 载入.

那么让我们看看之后会有什么样的操作吧.

更新

看到第二章就觉得可能会有点不太够用了, 并且因为平时也需要用, 所以写了一个简单的绘图库 GURAFU.