The Common Lisp Cookbook – GUI ツールキット

Table of Contents

The Common Lisp Cookbook – GUI ツールキット

📢 🎓 ⭐ 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 フレームワークでこれが可能ですが、細部はそれぞれ異なります。

最後に、ソフトウェア開発では、どうビルドしてどう利用者へ届けるかも重要です。ここでも、主要な 3 つの OS 向けに自己完結型バイナリを作り、利用者がダブルクリックで実行できるようにできます。

ここでは、適切な GUI フレームワークを選ぶための情報を整理して示します。contribute して、さらに例を追加したり、元のドキュメントを補ったりするのは歓迎です。

はじめに

このレシピでは、次の GUI ツールキットを紹介します。

加えて、次のものも見ておくとよいでしょう。

そのほか、awesome-cl#guiCliki にあるものも参照してください。

Tk (Ltk and nodgui)

Tk(あるいは Tcl/Tk。Tcl がプログラミング言語です)は、古臭い見た目だという悪評があります。ですが、それは今では(それほど)当てはまりません。1997 年のバージョン 8 以降は特にそうです。たぶん、思っているより良いものです。

これは nodgui の組み込みテーマを使ったシンプルな GUI です(詳細は後述)。

同じテーマの treeview です。

Arc テーマを使った、ツリーリスト、チェックボックス、ボタン、ラベルを表示するおもちゃのメディアプレーヤーです。

MacOS テーマのデモです。

これらに加えて、ttkthemesForest テーマ など多数のテーマを使えます。この tcl/tk の一覧 を見てください。

では Tk は何に向いているのでしょうか。Tk のウィジェットの種類は豊富ではありませんが、便利なキャンバスがあり、さらにいくつか独自の特徴があります。GUI を 完全に対話的に 開発でき、コアアプリから GUI を 遠隔実行 できます。しかもクロスプラットフォームです。

つまり Tk はネイティブではなく、最先端の機能を備えているわけでもありませんが、実績のある GUI ツールキット(兼プログラミング言語)で、今も業界で使われています。単純な GUI を素早く作りたいとき、配布のしやすさを重視したいとき、安定性が必要なときに向いています。

Lisp バインディングは 2 つあります。Ltknodgui です。Nodgui(”No Drama GUI”)は Ltk のフォークで、追加ウィジェット(たとえば自動補完リストウィジェット)、非同期イベントループ、そして何よりライブラリに付属する意外と見栄えの良い “Yaru” テーマを備えています。ほかのテーマを入れて使うのも簡単です。詳細は後述します。

ウィジェット一覧

(網羅的な一覧ではありません)

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 ウィジェットだけでなく、ネットワークや D-BUS など多くの層を提供します。

Qt はオープンソースソフトウェアなら無料で使えますが、プロプライエタリソフトウェアを配布する場合は条件を確認してください。

Qtools バインディングは Qt4 を対象にしています。Qt5 の Lisp バインディングは https://github.com/commonqt/commonqt5/ で、まだ本番向けではありません。

Qtools の付属ライブラリとして、最初の Qtools アプリケーションを作ったらぜひ見ておきたいのが Qtools-ui です。便利なウィジェットと既成コンポーネントの集まりで、短い デモ動画 もあります。

Gtk+3 (cl-cffi-gtk)

Gtk+3GNOME アプリケーションを作る主要ライブラリです。Lisp バインディングとしては、現時点で最も進んでいるものが cl-cffi-gtk です。主に GNU/Linux 向けに作られましたが、Gtk は macOS でも問題なく動き、今では Windows でも使えます。

IUP (lispnik/IUP)

IUP は、ブラジルのリオデジャネイロ・カトリック大学で活発に開発されているクロスプラットフォーム GUI ツールキットです。ネイティブコントロール を使い、Windows では Windows API、GNU/Linux では Gtk3 を使います。執筆時点では Cocoa 版も開発中で、iOS、Android、WASM 版もあります。IUP の特徴は API が小さい ことです。

[Lisp バインディング] は lispnik/iup です。C ソースから自動生成されているため、よくできています。新しい IUP バージョンに追従する手間が少なく、必要な手順も文書化されています。これにより bus factor の面でも安心です。

IUP は Tk と Gtk/Qt の中間にある、非常に良い選択肢です。

ウィジェット一覧

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 ツールキットです。

Nuklear は、ANSI C で書かれ、パブリックドメインでライセンスされた、最小限の状態しか持たないイミディエイトモードのグラフィカルユーザインターフェイスツールキットです。アプリケーションに組み込めるシンプルなユーザインターフェイスとして設計されており、依存関係も、デフォルトのレンダリングバックエンドも、OS のウィンドウ/入力処理も持ちません。代わりに、入力用のシンプルな入力状態と、出力としてプリミティブな図形を記述する描画コマンドを備えた、高度にモジュール化されたライブラリベースの方式を提供します。つまり、数多くのプラットフォームやレンダリングバックエンドを抽象化しようとする階層化されたライブラリを提供するのではなく、実際の UI だけに集中しています。

Lisp バインディングは Bodge-Nuklear で、上位の付属として bodge-uibodge-ui-window があります。

従来の UI フレームワークと違い、Nuklear では描画ループや入力管理を開発者が引き継げます。設定は少し増えますが、ゲームや、新しいコントロールを作りたいアプリケーションには特に向いています。

ウィジェット一覧

抜粋です。

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

使ってみる

Tk

Ltk はすぐに使えて、つかみやすいものです。

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

ウィジェットの作り方

ウィジェットはすべて、通常の make-instance とウィジェット名で作ります。

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

これにより、デフォルトのシンボル補完で Ltk をたどっていけます。

メインループの始め方

たいていのバインディングと同じく、GUI 関連のコードはメインループを扱うマクロ(ここでは with-ltk)の中で始める必要があります。

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

ウィジェットの表示の仕方

ウィジェットを作ったら、レイアウトに配置する必要があります。Tk にはいくつか配置方法がありますが、今使うべきなのは grid です。grid はウィジェット、列、行、いくつかの optional パラメータを引数に取る関数です。

通常の Lisp コードと同じく、関数のシグネチャはエディタに出ます。これも Ltk をたどりやすくしてくれます。

ボタンを表示する例です。

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

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

イベントに反応する

多くのウィジェットには :command 引数があり、ウィジェットのイベントが発生したときに実行される lambda を受け取ります。ボタンの場合はクリック時です。

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

対話的開発

(start-wish) で Tk プロセスを background で起動すると、ウィジェットを作って grid に置く作業を対話的に行えます。

詳しくは ドキュメント を見てください。

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

Nodgui

Nodgui のデモを試すには次のようにします。

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

見た目のよいテーマでデモを読み込むなら、次のようにします。

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

or

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

Nodgui UI theme

nodgui に付属する “yaru” テーマを使うには、単純に次のようにします。

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

or

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

or

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

別の tcl テーマをインストールして読み込むこともできます。たとえば Forest ttk テーマttkthemes を clone します。project ディレクトリは次のようになります。

yourgui.asd
yourgui.lisp
ttkthemes/

ttkthemes/ の中では png/ ディレクトリにテーマがあり、それ以外は現時点で未対応です。

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

nodgui で .tcl ファイルを読み込み、このテーマを使うよう指示します。

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

これで終わりです。アプリケーションは新しい、そこそこ見栄えのよい GUI テーマを使うようになります。

Qt4

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

残りを入れるメインウィジェットを作ります。

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

このメインウィジェットの中に入力フィールドとボタンを作ります。

(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 イベントには反応していません。

イベントに反応する

Qt でイベントに反応するにはシグナルとスロットを使います。スロット はシグナルを受け取る、あるいはシグナルに “接続する” 関数で、シグナル はイベントの運び手です。

ウィジェットはすでに自前のシグナルを送っています。たとえばボタンは “pressed” イベントを送ります。そのため、たいていはそれに接続するだけで済みます。

ただし、必要なら独自のシグナル群を作ることもできます。

組み込みイベント

go-buttonpressedreturn-pressed イベントに接続し、メッセージボックスを表示します。

(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))

独自イベント

上と同じ機能を実装しますが、説明のためにボタンクリック時に発火する name-set という独自シグナルを作ります。

まずシグナルを定義します。これは main-window の中で行い、型は string です。

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

最初のスロットを作ってボタンを pressedreturn-pressed イベントに反応させます。ただしここでは上のようにメッセージボックスを作るのではなく、入力フィールドの値を載せた name-set シグナルを送ります。

(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)))

ここまでのところ、name-set に反応するものはありません。そこで、これに接続してメッセージを表示する 2 つ目の slot を作ります。ここでもパラメータの型を明示します。

(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))

ビルドと配布

バイナリをビルドし、必要な共有ライブラリすべてと一緒にまとめて配布できます。

https://github.com/Shinmera/qtools#deployment を読んでください。

3 つの OS 向けに自己完結型のバイナリをビルドする この Travis CI スクリプト も参考になるでしょう。

Gtk3

ドキュメントは、初心者向けも含めて非常によくできています。

quickload するライブラリは cl-cffi-gtk です。これは数多くのライブラリから成り、自分のパッケージで :use する必要があります。

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

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

(in-package :gtk-tutorial)

メインループの実行の仕方

ほかのライブラリと同じく、すべてはメインループのラッパー(ここでは with-main-loop)の中で起こります。

ウィンドウの作り方

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

ウィジェットの作り方

ウィジェットにはすべて対応するクラスがあります。make-instance 'widget-class で作れますが、できればコンストラクタを使うのが望ましいです。

コンストラクタは “new” で終わります(または “new” を含みます)。

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

layout の作り方

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

続いてウィジェットをボックスに詰め込みます。

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

そしてボックスをウィンドウに追加します。

(gtk-container-add window box)

最後にすべてを表示します。

(gtk-widget-show-all window)

イベントに反応する

g-signal-connect +対象のウィジェット+イベント名(文字列)+ウィジェットを引数に取る lambda、という形で使います。

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

あるいは、もうひとつ例を挙げます。

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

完全な例

(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

インストール手順は上流のドキュメントで確認してください。GNU/Linux ではシステム依存パッケージが 1 つ必要になることがあり、Windows では環境変数を変更する必要があるかもしれません。

最後に、次を実行します。

(ql:quickload "iup")

IUP は :use しません(そもそも一般的によくない習慣です)。

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

次のスニペットは、テキストラベルを表示するダイアログフレームを作ります。

(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)))

メインループの起動方法

ここまで見てきたバインディングと同様に、ウィジェットは with-iup マクロの中で表示し、iup:main-loop を呼びます。

ウィジェットの作り方

コンストラクタ関数はウィジェット名そのものです。iup:labeliup:dialog などです。

ウィジェットの表示方法

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

ウィジェットは frame にまとめられ、縦 (vbox) や横 (hbox) に並べられます(下の例を参照)。

ウィンドウのリサイズ時にウィジェットを伸縮させたい場合は :expand :yes を使います(または :horizontal:vertical)。

:alignement property も使えます。

widget の属性の取得と設定

属性の値を得るには (iup:attribute widget attribute) を使い、設定するときはそれに setf します。

イベントに反応する

ほとんどのウィジェットは :action パラメータを取り、1 つのパラメータ(ハンドル)を持つ 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+))

以下ではラベルを作り、その下にボタンを置きます。ボタンをクリックするとメッセージ 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))

クリック回数のカウンターを作る似た例を示します。ラベルとその 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)))

リストウィジェットの例

以下では、単一選択と複数選択のリストウィジェットを 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")

組み込み例を実行できます。

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

では、簡単なアプリケーションを書くためのパッケージを定義します。

(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)))

そして実行します。

(run)

イベントに反応するには、次のシグナルを使います。

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

これらは引数 1 つ、つまりパネルを取る関数を受け取ります。ただし注意してください。ウィジェットがその状態にあるあいだは描画 cycle ごとに呼ばれるため、かなり頻繁になることがあります。

対話的開発

REPL で例を動かしただけでは、何が面白いかは見えません。コードを Lisp ファイルに入れて実行し、ウィンドウを出してください。そうすればパネルウィジェットやレイアウトを変えたとき、その変更がアプリケーションの実行中に即座に反映されるのが分かります。

まとめ

楽しんでください。体験談やアプリケーションの共有も遠慮なくどうぞ。

Page source: ja/gui.md

T
O
C