The Common Lisp Cookbook – GUI toolkits

Table of Contents

The Common Lisp Cookbook – GUI toolkits

📢 🎓 ⭐ Learn Common Lisp efficiently in videos, by the Cookbook's main contributor. Learn more.

🖊 Discover a new Common Lisp and Coalton editor for beginners: mine and a new VSCode extension for Common Lisp: OLIVE.

Lisp には長く豊かな歎史があり、GUI 開発にも同じこずが蚀えたす。実際、最初の GUI ビルダヌは Lisp で曞かれたしたそしお Apple に売华され、今では Interface Builder になっおいたす。

Lisp は察話的開発胜力でも有名で、GUI アプリケヌション開発ではこの利点がさらに倧きくなりたす。関数 1 ぀をコンパむルしただけで GUI が即座に曎新される様子を想像できるでしょうか。今日では倚くの GUI framework でこれが可胜ですが、现郚はそれぞれ異なりたす。

最埌に、゜フトりェア開発では、どうビルドしおどう利甚者ぞ届けるかも重芁です。ここでも、䞻芁な 3 ぀の OS 向けに自己完結型 binary を䜜り、利甚者がダブルクリックで実行できるようにできたす。

ここでは、適切な GUI framework を遞ぶための情報を敎理しお瀺したす。contribute しお、さらに䟋を远加したり、元のドキュメントを補ったりするのは歓迎です。

Introduction

このレシピでは、次の GUI toolkit を玹介したす。

加えお、次のものも芋おおくずよいでしょう。

そのほか、awesome-cl#gui ず Cliki にあるものも参照しおください。

Tk (Ltk and nodgui)

Tkあるいは Tcl/Tk。Tcl が programming language ですは、叀臭い芋た目だずいう悪評がありたす。ですが、それは今ではそれほど圓おはたりたせん。1997 幎の version 8 以降は特にそうです。たぶん、思っおいるより良いものです。

これは nodgui の組み蟌み theme を䜿ったシンプルな GUI です詳现は埌述。

同じ theme の treeview です。

Arc theme を䜿った、tree list、checkbox、button、label を衚瀺するおもちゃの media player です。

MacOS theme の demo です。

これらに加えお、ttkthemes や Forest theme など倚数の theme を䜿えたす。この tcl/tk の䞀芧 を芋おください。

では Tk は䜕に向いおいるのでしょうか。Tk の widget の皮類は豊富ではありたせんが、䟿利な canvas があり、さらにいく぀か独自の特城がありたす。GUI を 完党に察話的に 開発でき、core app から GUI を 遠隔実行 できたす。しかも cross-platform です。

぀たり Tk は native ではなく、最先端の機胜を備えおいるわけでもありたせんが、実瞟のある GUI toolkit兌 programming languageで、今も業界で䜿われおいたす。単玔な GUI を玠早く䜜りたいずき、配垃のしやすさを重芖したいずき、安定性が必芁なずきに向いおいたす。

Lisp bindings は 2 ぀ありたす。Ltk ず nodgui です。Nodgui”No Drama GUI”は Ltk の fork で、远加 widgetたずえば auto-completion list widget、非同期 event loop、そしお䜕よりラむブラリに付属する意倖ず芋栄えの良い “Yaru” theme を備えおいたす。ほかの theme を入れお䜿うのも簡単です。詳现は埌述したす。

widget 䞀芧

(網矅的な䞀芧ではありたせん)

Button Canvas Check-button Entry Frame Label Labelframe Listbox
Menu Menubutton Message
Paned-window
Radio-button Scale
Scrollbar Spinbox Text
Toplevel Widget Canvas

Ltk-megawidgets:
    progress
    history-entry
    menu-entry

nodgui に远加されるもの:

treelist tooltip searchable-listbox date-picker calendar autocomplete-listbox
password-entry progress-bar-star notify-window
dot-plot bar-chart equalizer-bar
swap-list

Qt4 (Qtools)

Qt ず Qt4 を改めお玹介する必芁があるでしょうか。Qt は非垞に倧きく、䜕でも入っおいたす。UI widget だけでなく、networking や D-BUS など倚くの局を提䟛したす。

Qt は open-source software なら無料で䜿えたすが、proprietary software を配垃する堎合は条件を確認しおください。

Qtools bindings は Qt4 を察象にしおいたす。Qt5 の Lisp bindings は https://github.com/commonqt/commonqt5/ で、ただ本番向けではありたせん。

Qtools の companion library ずしお、最初の Qtools application を䜜ったらぜひ芋おおきたいのが Qtools-ui です。䟿利な widget ず既成 component の集たりで、短い デモ動画 もありたす。

Gtk+3 (cl-cffi-gtk)

Gtk+3 は GNOME application を䜜る䞻芁 library です。Lisp bindings ずしおは、珟時点で最も進んでいるものが cl-cffi-gtk です。䞻に GNU/Linux 向けに䜜られたしたが、Gtk は macOS でも問題なく動き、今では Windows でも䜿えたす。

IUP (lispnik/IUP)

IUP は、ブラゞルのリオデゞャネむロ・カトリック倧孊で掻発に開発されおいる cross-platform GUI toolkit です。native control を䜿い、Windows では Windows API、GNU/Linux では Gtk3 を䜿いたす。執筆時点では Cocoa 版も開発䞭で、iOS、Android、WASM 版もありたす。IUP の特城は API が小さい こずです。

[Lisp bindings] は lispnik/iup です。C source から自動生成されおいるため、よくできおいたす。新しい IUP version に远埓する手間が少なく、必芁な手順も文曞化されおいたす。これにより bus factor の面でも安心です。

IUP は Tk ず Gtk/Qt の䞭間にある、非垞に良い遞択肢です。

List of widgets

Radio, Tabs, FlatTabs, ScrollBox, DetachBox,
Button, FlatButton, DropButton, Calendar, Canvas, Colorbar, ColorBrowser, DatePick, Dial, Gauge, Label, FlatLabel,
FlatSeparator, Link, List, FlatList, ProgressBar, Spin, Text, Toggle, Tree, Val,
listDialog, Alarm, Color, Message, Font, Scintilla, file-dialog

Cells, Matrix, MatrixEx, MatrixList,
GLCanvas, Plot, MglPlot, OleControl, WebBrowser (WebKit/Gtk+)

drag-and-drop
WebBrowser

Nuklear (Bodge-Nuklear)

Nuklear は小さな immediate-mode GUI toolkit です。

Nuklear is a minimal-state, immediate-mode graphical user interface toolkit written in ANSI C and licensed under public domain. It was designed as a simple embeddable user interface for application and does not have any dependencies, a default render backend or OS window/input handling but instead provides a highly modular, library-based approach, with simple input state for input and draw commands describing primitive shapes as output. So instead of providing a layered library that tries to abstract over a number of platform and render backends, it focuses only on the actual UI.

Lisp bindings は Bodge-Nuklear で、䞊䜍の companion ずしお bodge-ui ず bodge-ui-window がありたす。

埓来の UI framework ず違い、Nuklear では描画ルヌプや input 管理を開発者が匕き継げたす。蚭定は少し増えたすが、ゲヌムや、新しい control を䜜りたい application には特に向いおいたす。

List of widgets

抜粋です。

buttons, progressbar, image selector, (collapsable) tree, list, grid, range, slider, color picker,
date-picker

Getting started

Tk

Ltk is quick and easy to grasp.

(ql:quickload "ltk")
(in-package :ltk-user)

How to create widgets

All widgets are created with a regular make-instance and the widget name:

(make-instance 'button)
(make-instance 'treeview)

This makes Ltk explorable with the default symbol completion.

How to start the main loop

As with most bindings, the GUI-related code must be started inside a macro that handles the main loop, here with-ltk:

(with-ltk ()
  (let ((frame (make-instance 'frame)))
    
))

How to display widgets

widget を䜜ったら、layout に配眮する必芁がありたす。Tk にはいく぀か配眮方法がありたすが、今䜿うべきなのは grid です。grid は widget、列、行、いく぀かの optional parameter を匕数に取る関数です。

通垞の Lisp code ず同じく、関数の signature は editor に出たす。これも Ltk をたどりやすくしおくれたす。

button を衚瀺する䟋です。

(with-ltk ()
  (let ((button (make-instance 'button :text "hello")))
    (grid button 0 0)))

やるこずはそれだけです。

event に反応する

倚くの widget には :command argument があり、widget の event が発生したずきに実行される lambda を受け取りたす。button の堎合はクリック時です。

(make-instance 'button
  :text "Hello"
  :command (lambda ()
             (format t "clicked")))

察話的開発

(start-wish) で Tk process を background で起動するず、widget を䜜っお grid に眮く䜜業を察話的に行えたす。

詳しくは ドキュメント を芋おください。

終わったら (exit-wish) を呌びたす。

Nodgui

Nodgui の demo を詊すには次のようにしたす。

(ql:quickload "nodgui")
(nodgui.demo:demo)

芋た目のよい theme で demo を読み蟌むなら、次のようにしたす。

(nodgui.demo:demo :theme "yaru")

or

(setf nodgui:*default-theme* "yaru")
(nodgui.demo:demo)

Nodgui UI theme

nodgui に付属する “yaru” theme を䜿うには、単玔に次のようにしたす。

(with-nodgui ()
  (use-theme "yaru")
  
)

or

(with-nodgui (:theme "yaru")
  
)

or

(setf nodgui:*default-theme* "yaru")
(with-nodgui ()
  
)

別の tcl theme をむンストヌルしお読み蟌むこずもできたす。たずえば Forest ttk theme や ttkthemes を clone したす。project directory は次のようになりたす。

yourgui.asd
yourgui.lisp
ttkthemes/

ttkthemes/ の䞭では png/ directory に theme があり、それ以倖は珟時点で未察応です。

/ttkthemes/ttkthemes/png/arc/arc.tcl

nodgui で .tcl file を読み蟌み、この theme を䜿うよう指瀺したす。

(with-nodgui ()
   (eval-tcl-file "/ttkthemes/ttkthemes/png/arc/arc.tcl")
   (use-theme "arc")
   
 code here 
)

これで終わりです。application は新しい、そこそこ芋栄えのよい GUI theme を䜿うようになりたす。

Qt4

(ql:quickload '(:qtools :qtcore :qtgui))
(defpackage #:qtools-test
  (:use #:cl+qt)
  (:export #:main))
(in-package :qtools-test)
(in-readtable :qtools)

残りを入れる main widget を䜜りたす。

(define-widget main-window (QWidget)
  ())

この main widget の䞭に input field ず button を䜜りたす。

(define-subwidget (main-window name) (q+:make-qlineedit main-window)
  (setf (q+:placeholder-text name) "Your name please."))
(define-subwidget (main-window go-button) (q+:make-qpushbutton "Go!" main-window))

暪に䞊べたす。

(define-subwidget (main-window layout) (q+:make-qhboxlayout main-window)
  (q+:add-widget layout name)
  (q+:add-widget layout go-button))

そしお衚瀺したす。

(with-main-window
  (window 'main-window))

これで圢はできたしたが、ただ click event には反応しおいたせん。

event に反応する

Qt で event に反応するには signal ず slot を䜿いたす。slot は signal を受け取る、あるいは signal に “接続する” 関数で、signal は event の運び手です。

widget はすでに自前の signal を送っおいたす。たずえば button は “pressed” event を送りたす。そのため、たいおいはそれに接続するだけで枈みたす。

ただし、必芁なら独自の signal 矀を䜜るこずもできたす。

組み蟌み event

go-button を pressed ず return-pressed event に接続し、message box を衚瀺したす。

(define-slot (main-window go-button) ()
  (declare (connected go-button (pressed)))
  (declare (connected name (return-pressed)))
  (q+:qmessagebox-information main-window
                              "Greetings"  ;; title
                              (format NIL "Good day to you, ~a!" (q+:text name))))

これで完成です。次のように実行したす。

(with-main-window (window 'main-window))
独自 event

䞊ず同じ機胜を実装したすが、説明のために button クリック時に発火する name-set ずいう独自 signal を䜜りたす。

たず signal を定矩したす。これは main-window の䞭で行い、型は string です。

(define-signal (main-window name-set) (string))

最初の slot を䜜っお button を pressed ず return-pressed event に反応させたす。ただしここでは䞊のように message box を䜜るのではなく、input field の倀を茉せた name-set signal を送りたす。

(define-slot (main-window go-button) ()
  (declare (connected go-button (pressed)))
  (declare (connected name (return-pressed)))
  (signal! main-window (name-set string) (q+:text name)))

So far, nobody reacts to name-set. We create a second slot that connects to it, and displays our message. Here again, we precise the parameter type.

(define-slot (main-window name-set) ((new-name string))
  (declare (connected main-window (name-set string)))
  (q+:qmessagebox-information main-window "Greetings"
        (format NIL "Good day to you, ~a!" new-name)))

そしお実行したす。

(with-main-window (window 'main-window))

Building and deployment

It is possible to build a binary and bundle it together with all the necessary shared libraries.

Please read https://github.com/Shinmera/qtools#deployment.

You might also like this Travis CI script to build a self-contained binary for the three OSes.

Gtk3

The documentation is exceptionally good, including for beginners.

The library to quickload is cl-cffi-gtk. It is made of numerous ones, that we have to :use for our package.

(ql:quickload "cl-cffi-gtk")

(defpackage :gtk-tutorial
  (:use :gtk :gdk :gdk-pixbuf :gobject
   :glib :gio :pango :cairo :common-lisp))

(in-package :gtk-tutorial)

How to run the main loop

As with the other libraries, everything happens inside the main loop wrapper, here with-main-loop.

How to create a window

(make-instance 'gtk-window :type :toplevel :title "hello" ...).

How to create a widget

All widgets have a corresponding class. We can create them with make-instance 'widget-class, but we preferably use the constructors.

The constructors end with (or contain) “new”:

(gtk-label-new)
(gtk-button-new-with-label "Label")

How to create a layout

(let ((box (make-instance 'gtk-box :orientation :horizontal
                                   :spacing 6))) ...)

then pack a widget onto the box:

(gtk-box-pack-start box mybutton-1)

and add the box to the window:

(gtk-container-add window box)

and display them all:

(gtk-widget-show-all window)

Reacting to events

Use g-signal-connect + the concerned widget + the event name (as a string) + a lambda, that takes the widget as argument:

(g-signal-connect window "destroy"
  (lambda (widget)
    (declare (ignore widget))
    (leave-gtk-main)))

Or again:

(g-signal-connect button "clicked"
  (lambda (widget)
    (declare (ignore widget))
    (format t "Button was pressed.~%")))

Full example

(defun hello-world ()
  ;; in the docs, this is example-upgraded-hello-world-2.
  (within-main-loop
    (let ((window (make-instance 'gtk-window
                                 :type :toplevel
                                 :title "Hello Buttons"
                                 :default-width 250
                                 :default-height 75
                                 :border-width 12))
          (box (make-instance 'gtk-box
                              :orientation :horizontal
                              :spacing 6)))
      (g-signal-connect window "destroy"
                        (lambda (widget)
                          (declare (ignore widget))
                          (leave-gtk-main)))
      (let ((button (gtk-button-new-with-label "Button 1")))
        (g-signal-connect button "clicked"
                          (lambda (widget)
                            (declare (ignore widget))
                            (format t "Button 1 was pressed.~%")))
        (gtk-box-pack-start box button))
      (let ((button (gtk-button-new-with-label "Button 2")))
        (g-signal-connect button "clicked"
                        (lambda (widget)
                          (declare (ignore widget))
                          (format t "Button 2 was pressed.~%")))
        (gtk-box-pack-start box button))
      (gtk-container-add window box)
      (gtk-widget-show-all window))))

IUP

Please check the installation instructions upstream. You may need one system dependency on GNU/Linux, and to modify an environment variable on Windows.

Finally, do:

(ql:quickload "iup")

We are not going to :use IUP (it is a bad practice generally after all).

(defpackage :test-iup
  (:use :cl))
(in-package :test-iup)

The following snippet creates a dialog frame to display a text label.

(defun hello ()
  (iup:with-iup ()
    (let* ((label (iup:label
                     :title
                     (format nil "Hello, World!~%IUP ~A~%~A ~A"
                       (iup:version)
                       (lisp-implementation-type)
                       (lisp-implementation-version))))
           (dialog (iup:dialog label :title "Hello, World!")))
      (iup:show dialog)
      (iup:main-loop))))
(hello)

SBCL に぀いおの重芁な泚意です。珟時点ではれロ陀算゚ラヌを捕捉する必芁がありたすこの issue の進展を芋おください。そのため、スニペットは次のように実行したす。

(defun run-gui-function ()
  #-sbcl (gui-function)
  #+sbcl
  (sb-int:with-float-traps-masked
      (:divide-by-zero :invalid)
    (gui-function)))

メむンルヌプの起動方法

ここたで芋おきた bindings ず同様に、widget は with-iup macro の䞭で衚瀺し、iup:main-loop を呌びたす。

widget の䜜り方

constructor 関数は widget 名そのものです。iup:label、iup:dialog などです。

widget の衚瀺方法

必ず “show” しおください。(iup:show dialog) です。

widget は frame にたずめられ、瞊 (vbox) や暪 (hbox) に䞊べられたす䞋の䟋を参照。

window のリサむズ時に widget を䌞瞮させたい堎合は :expand :yes を䜿いたすたたは :horizontal、:vertical。

:alignement property も䜿えたす。

widget の attribute の取埗ず蚭定

attribute の倀を埗るには (iup:attribute widget attribute) を䜿い、蚭定するずきはそれに setf したす。

event に反応する

ほずんどの widget は :action parameter を取り、1 ぀の parameterhandleを持぀ lambda を受け取りたす。

(iup:button :title "Test &1"
            :expand :yes
            :tip "Callback inline at control creation"
            :action (lambda (handle)
                      (iup:message "title" "button1's action callback")
                      iup:+default+))

以䞋では label を䜜り、その䞋に button を眮きたす。button をクリックするず message dialog を衚瀺したす。

(defun click-button ()
  (iup:with-iup ()
    (let* ((label (iup:label :title
                      (format nil "Hello, World!~%IUP ~A~%~A ~A"
                          (iup:version)
                          (lisp-implementation-type)
                          (lisp-implementation-version))))
           (button (iup:button :title "Click me"
                               :expand :yes
                               :tip "yes, click me"
                               :action
                               (lambda (handle)
                                 (declare (ignorable handle))
                                 (iup:message "title"
                                              "button clicked")
                                 iup:+default+)))
           (vbox
            (iup:vbox (list label button)
                      :gap "10"
                      :margin "10x10"
                      :alignment :acenter))
           (dialog (iup:dialog vbox :title "Hello, World!")))
      (iup:show dialog)
      (iup:main-loop))))

#+sbcl
(sb-int:with-float-traps-masked
      (:divide-by-zero :invalid)
    (click-button))

クリック回数の counter を䜜る䌌た䟋を瀺したす。label ずその title に回数を保持したす。title は敎数です。

(defun counter ()
  (iup:with-iup ()
    (let* ((counter (iup:label :title 0))
           (label (iup:label :title
                     (format nil "The button was clicked ~a time(s)."
                             (iup:attribute counter :title))))
           (button (iup:button :title "Click me"
                               :expand :yes
                               :tip "yes, click me"
                               :action (lambda (handle)
                                         (declare (ignorable handle))
                                         (setf (iup:attribute counter :title)
                                               (1+ (iup:attribute counter :title 'number)))
                                         (setf (iup:attribute label :title)
                                               (format nil "The button was clicked ~a times."
                                                       (iup:attribute counter :title)))
                                         iup:+default+)))
           (vbox
            (iup:vbox (list label button)
                      :gap "10"
                      :margin "10x10"
                      :alignment :acenter))
           (dialog (iup:dialog vbox :title "Counter")))
      (iup:show dialog)
      (iup:main-loop))))

(defun run-counter ()
  #-sbcl
  (counter)
  #+sbcl
  (sb-int:with-float-traps-masked
      (:divide-by-zero :invalid)
    (counter)))

list widget の䟋

以䞋では、単䞀遞択ず耇数遞択の list widget を 3 ぀䜜り、既定倀事前遞択行を蚭定しお、暪に䞊べたす。

(defun list-test ()
  (iup:with-iup ()
    (let*  ((list-1 (iup:list :tip "List 1"  ;; tooltip
                              ;; multiple selection
                              :multiple :yes
                              :expand :yes))
            (list-2 (iup:list :value 2   ;; default index of the selected row
                              :tip "List 2" :expand :yes))
            (list-3 (iup:list :value 9 :tip "List 3" :expand :yes))
            (frame (iup:frame
                    (iup:hbox
                     (progn
                       ;; populate the lists: display integers.
                       (loop for i from 1 upto 10
                          do (setf (iup:attribute list-1 i)
                                   (format nil "~A" i))
                          do (setf (iup:attribute list-2 i)
                                   (format nil "~A" (+ i 10)))
                          do (setf (iup:attribute list-3 i)
                                   (format nil "~A" (+ i 50))))
                       ;; hbox wants a list of widgets.
                       (list list-1 list-2 list-3)))
                    :title "IUP List"))
            (dialog (iup:dialog frame :menu "menu" :title "List example")))

      (iup:map dialog)
      (iup:show dialog)
      (iup:main-loop))))

(defun run-list-test ()
  #-sbcl (hello)
  #+sbcl
  (sb-int:with-float-traps-masked
      (:divide-by-zero :invalid)
    (list-test)))

Nuklear

泚意: 執筆時点で著者の蚀葉によれば、bodge-ui はただ開発初期段階で、䞀般利甚には未察応です。修正が必芁な癖がいく぀かあり、API に倉曎が入る可胜性がありたす。

bodge-ui は Quicklisp 本䜓ではなく、独自の Quicklisp distribution にありたす。たずそれをむンストヌルしたす。

(ql-dist:install-dist "http://bodge.borodust.org/dist/org.borodust.bodge.txt" :replace t :prompt nil)

OpenGL 2 renderer を有効にしたい堎合だけ、この行のコメントを倖しお評䟡したす。

;; (cl:pushnew :bodge-gl2 cl:*features*)

bodge-ui-window を quickload したす。

(ql:quickload "bodge-ui-window")

組み蟌み example を実行できたす。

(ql:quickload "bodge-ui-window/examples")
(bodge-ui-window.example.basic:run)

では、簡単な application を曞くための package を定矩したす。

(cl:defpackage :bodge-ui-window-test
  (:use :cl :bodge-ui :bodge-host))
(in-package :bodge-ui-window-test)
(defpanel (main-panel
           (:title "Bodge UI ぞようこそ")
           (:origin 200 50)
           (:width 400) (:height 400)
           (:options :movable :resizable
                     :minimizable :scrollable
                     :closable))
    (label :text "入れ子の widget:")
  (horizontal-layout
   (radio-group
    (radio :label "遞択肢 1")
    (radio :label "遞択肢 2" :activated t))
   (vertical-layout
    (check-box :label "チェック 1" :width 100)
    (check-box :label "チェック 2"))
   (vertical-layout
    (label :text "巊寄せ" :align :left)
    (label :text "䞭倮" :align :centered)
    (label :text "右寄せ" :align :right)))
  (label :text "幅に応じお䌞瞮:")
  (horizontal-layout
   (button :label "可倉")
   (button :label "最小幅" :width 80)
   (button :label "固定幅" :expandable nil :width 100))
  (label :text "幅に応じお䌞瞮:")
  (horizontal-layout
   (button :label "1.0" :expand-ratio 1.0)
   (button :label "0.75" :expand-ratio 0.75)
   (button :label "0.5" :expand-ratio 0.5))
  (label :text "残り:")
  (button :label "最䞊䜍ボタン"))

(defparameter *window-width* 800)
(defparameter *window-height* 600)

(defclass main-window (bodge-ui-window:ui-window) ()
  (:default-initargs
   :title "Bodge UI Window の䟋"
   :width *window-width*
   :height *window-height*
   :panels '(main-panel)
   :floating t
   :opengl-version #+bodge-gl2 '(2 1)
                   #+bodge-gl2 '(3 3)))


(defun run ()
  (bodge-host:open-window (make-instance 'main-window)))

and run it:

(run)

event に反応するには、次の signal を䜿いたす。

:on-click
:on-hover
:on-leave
:on-change
:on-mouse-press
:on-mouse-release

これらは匕数 1 ぀、぀たり panel を取る関数を受け取りたす。ただし泚意しおください。widget がその state にあるあいだは描画 cycle ごずに呌ばれるため、かなり頻繁になるこずがありたす。

察話的開発

REPL で example を動かしただけでは、䜕が面癜いかは芋えたせん。code を Lisp file に入れお実行し、window を出しおください。そうすれば panel widget や layout を倉えたずき、その倉曎が application の実行䞭に即座に反映されるのが分かりたす。

たずめ

楜しんでください。䜓隓談や application の共有も遠慮なくどうぞ。

Page source: ja/gui.md

T
O
C