1
0
Fork 0
cl-sites/lispcookbook.github.io/cl-cookbook/gui.html

1202 lines
47 KiB
HTML
Raw Normal View History

2023-10-25 11:23:21 +02:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="generator" content=
"HTML Tidy for HTML5 for Linux version 5.2.0">
<title>GUI toolkits</title>
<meta charset="utf-8">
<meta name="description" content="A collection of examples of using Common Lisp">
<meta name="viewport" content=
"width=device-width, initial-scale=1">
<link rel="icon" href=
"assets/cl-logo-blue.png"/>
<link rel="stylesheet" href=
"assets/style.css">
<script type="text/javascript" src=
"assets/highlight-lisp.js">
</script>
<script type="text/javascript" src=
"assets/jquery-3.2.1.min.js">
</script>
<script type="text/javascript" src=
"assets/jquery.toc/jquery.toc.min.js">
</script>
<script type="text/javascript" src=
"assets/toggle-toc.js">
</script>
<link rel="stylesheet" href=
"assets/github.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<h1 id="title-xs"><a href="index.html">The Common Lisp Cookbook</a> &ndash; GUI toolkits</h1>
<div id="logo-container">
<a href="index.html">
<img id="logo" src="assets/cl-logo-blue.png"/>
</a>
<div id="searchform-container">
<form onsubmit="duckSearch()" action="javascript:void(0)">
<input id="searchField" type="text" value="" placeholder="Search...">
</form>
</div>
<div id="toc-container" class="toc-close">
<div id="toc-title">Table of Contents</div>
<ul id="toc" class="list-unstyled"></ul>
</div>
</div>
<div id="content-container">
<h1 id="title-non-xs"><a href="index.html">The Common Lisp Cookbook</a> &ndash; GUI toolkits</h1>
<!-- Announcement we can keep for 1 month or more. I remove it and re-add it from time to time. -->
2024-05-15 18:18:38 +02:00
<!-- <p class="announce"> -->
<!-- 📢 🤶 ⭐ -->
<!-- <a style="font-size: 120%" href="https://www.udemy.com/course/common-lisp-programming/?couponCode=LISPY-XMAS2023" title="This course is under a paywall on the Udemy platform. Several videos are freely available so you can judge before diving in. vindarel is (I am) the main contributor to this Cookbook."> Discover our contributor's Lisp course with this Christmas coupon.</a> -->
<!-- <strong> -->
<!-- Recently added: 18 videos on MACROS. -->
<!-- </strong> -->
<!-- <a style="font-size: 90%" href="https://github.com/vindarel/common-lisp-course-in-videos/">Learn more</a>. -->
<!-- </p> -->
<p class="announce">
📢 New videos: <a href="https://www.youtube.com/watch?v=h_noB1sI_e8">web dev demo part 1</a>, <a href="https://www.youtube.com/watch?v=xnwc7irnc8k">dynamic page with HTMX</a>, <a href="https://www.youtube.com/watch?v=Zpn86AQRVN8">Weblocks demo</a>
</p>
2023-10-25 11:23:21 +02:00
<p class="announce-neutral">
📕 <a href="index.html#download-in-epub">Get the EPUB and PDF</a>
</p>
<div id="content"
<p>Lisp has a long and rich history and so does the development of
Graphical User Interfaces in Lisp. In fact, the first GUI builder was
written in Lisp (and sold to Apple. It is now Interface Builder).</p>
<p>Lisp is also famous and unrivalled for its interactive development
capabilities, a feature even more worth having to develop GUI
applications. Can you imagine compiling one function and seeing your
GUI update instantly? We can do this with many GUI frameworks today,
even though the details differ from one to another.</p>
<p>Finally, a key part in building software is how to build it and ship
it to users. Here also, we can build self-contained binaries, for
the three main operating systems, that users can run with a double
click.</p>
<p>We aim here to give you the relevant information to help you choose
the right GUI framework and to put you on tracks. Dont hesitate to
<a href="https://github.com/LispCookbook/cl-cookbook/issues/">contribute</a>, to
send more examples and to furnish the upstream documentations.</p>
<h2 id="introduction">Introduction</h2>
<p>In this recipe, well present the following GUI toolkits:</p>
<ul>
<li><a href="https://www.tcl.tk">Tk</a> with <a href="http://www.peter-herth.de/ltk/ltkdoc/">Ltk</a> and <a href="https://notabug.org/cage/nodgui">nodgui</a></li>
<li><a href="https://doc.qt.io/archives/qt-4.8/index.html">Qt4</a> with <a href="https://github.com/Shinmera/qtools">Qtools</a></li>
<li><a href="http://webserver2.tecgraf.puc-rio.br/iup/">IUP</a> with <a href="https://github.com/lispnik/iup/">lispnik/iup</a></li>
<li><a href="https://www.gtk.org/">Gtk3</a> with <a href="https://github.com/Ferada/cl-cffi-gtk/">cl-cffi-gtk</a>
<ul>
<li>if you want Gtk4 bindings, see <a href="https://github.com/bohonghuang/cl-gtk4">cl-gtk4</a>. They are new bindings, released in September, 2022.</li>
</ul>
</li>
<li><a href="https://github.com/Immediate-Mode-UI/Nuklear">Nuklear</a> with <a href="https://github.com/borodust/bodge-nuklear">Bodge-Nuklear</a></li>
</ul>
<p>In addition, you might want to have a look to:</p>
<ul>
<li>the <a href="http://www.lispworks.com/products/capi.html">CAPI</a> toolkit (Common Application Programming Interface),
which is proprietary and made by LispWorks. It is a complete and cross-platform
toolkit (Windows, Gtk+, Cocoa), very praised by its users. LispWorks
also has <a href="http://www.lispworks.com/products/lw4mr.html">iOS and Android
runtimes</a>. Example
software built with CAPI include <a href="https://scorecloud.com/">ScoreCloud</a>. It is possible to
try it with the LispWorks free demo.</li>
<li><a href="https://franz.com/products/allegro-common-lisp/acl_ide.lhtml">Allegro CLs IDE and Common Graphics windowing system</a> (proprietary): Allegros IDE is a general environment for developing applications. It works in concert with a windowing system called Common Graphics. The IDE is available for Allegro CLs Microsoft Windows, on Linux platforms, Free BSD and on the Mac.
<ul>
<li>NEW! 🎉 since Allegro CL 10.1 (released in March of 2022), the IDE, and the Common Graphics GUI toolkit, runs in the browser. It is called <a href="https://franz.com/ftp/pri/acl/cgjs/doc.html">CG/JS</a>.</li>
</ul>
</li>
<li><a href="https://ccl.clozure.com/docs/ccl.html#the-objective-c-bridge">CCLs built-in Cocoa
interface</a>,
used to build applications such as <a href="https://opusmodus.com/">Opusmodus</a>.</li>
<li>Clozure CLs built-in <a href="https://ccl.clozure.com/docs/ccl.html#the-objective-c-bridge">Objective-C bridge</a> and <a href="https://github.com/plkrueger/CocoaInterface/">CocoaInterface</a>, a Cocoa interface for CCL. Build Cocoa user interface windows dynamically using Lisp code and bypass the typical Xcode processes.
<ul>
<li>the bridge is good at catching ObjC errors and turning them into Lisp errors, so one can have an iterative REPL-based development cycle for a macOS GUI application.</li>
</ul>
</li>
<li><a href="https://common-lisp.net/project/mcclim/">McCLIM</a> and <a href="https://github.com/earl-ducaine/cl-garnet">Garnet</a> are toolkit in 100% Common Lisp. McClim even has <a href="https://techfak.de/~jmoringe/mcclim-broadway-7.ogv">a prototype</a> running in the browser with the Broadway protocol and Garnet has an ongoing interface to Gtk.</li>
<li><a href="https://github.com/Shirakumo/alloy">Alloy</a>, another very new toolkit in 100% Common Lisp, used for example in the <a href="https://github.com/shinmera/kandria">Kandria</a> game.</li>
<li><a href="https://gitlab.com/eql">eql, eql5, eql5-android</a>, embedded Qt4 and Qt5 Lisp, embedded in ECL, embeddable in Qt. Port of EQL5 to the Android platform.</li>
<li>this <a href="https://github.com/defunkydrummer/abcl-jazz">demo using Java Swing from ABCL</a></li>
<li><a href="https://github.com/mifpasoti/Gtk-Demos">examples of using Gtk without C files with SBCL</a>, as well as GTK-server.</li>
<li>and, last but not least, <a href="http://ceramic.github.io/">Ceramic</a>, to ship a cross-platform web app with Electron.</li>
</ul>
<p>as well as the other ones listed on <a href="https://github.com/CodyReichert/awesome-cl#Gui">awesome-cl#gui</a> and <a href="https://www.cliki.net/GUI">Cliki</a>.</p>
<h3 id="tk-ltk-and-nodgui">Tk (Ltk and nodgui)</h3>
<p><a href="https://www.tcl.tk">Tk</a> (or Tcl/Tk, where Tcl is the programming language) has the
infamous reputation of having an outdated look. This is not (so) true
anymore since its version 8 of 1997 (!). It is probably better than
you think.</p>
<p>This is a simple GUI with nodguis built-in theme (more on that below):</p>
<p><img src="assets/gui/nodgui-feet2meters-yaru.png" alt="" /></p>
<p>This is a treeview, with the same theme:</p>
<p><img src="assets/gui/nodgui-treeview-yaru.png" alt="" /></p>
<p>A toy mediaplayer, showing a tree list, checkboxes, buttons and labels, with the Arc theme:</p>
<p><img src="assets/gui/mediaplayer-nodgui-arc.png" alt="" /></p>
<p>This is a demo with a Macos theme:</p>
<p><img src="assets/gui/ltk-on-macos.png" alt="" /></p>
<p>In addition to those, we can use many of the <a href="https://ttkthemes.readthedocs.io/en/latest/themes.html">ttkthemes</a>, the <a href="https://github.com/rdbende/Forest-ttk-theme">Forest theme</a>, and more. See <a href="https://wiki.tcl-lang.org/page/List+of+ttk+Themes">this tcl/tk list</a>.</p>
<p>But what is Tk good for? Tk doesnt have a great choice of widgets, but it has a useful canvas,
and it has a couple of unique features: we can develop a graphical
interface <strong>fully interactively</strong> and we can run the GUI <strong>remotely</strong>
from the core app. It is also cross-platform.</p>
<p>So, Tk isnt native and doesnt have the most advanced features,
but it is a used and proven GUI toolkit (and
programming language) still used in the industry. It can be a great
choice to quickly create simple GUIs, to leverage its ease of deployment, or
when stability is required.</p>
<p>There are two Lisp bindings: <a href="http://www.peter-herth.de/ltk/ltkdoc/">Ltk</a> and <a href="https://notabug.org/cage/nodgui">nodgui</a>. Nodgui
(“No Drama GUI”) is a fork of Ltk, with added widgets (such as an
auto-completion list widget), an asynchronous event loop and, what we
really enjoy, the surprisingly nice-looking “Yaru” theme that comes
with the library. It is also very easy to install and use any other theme of
our choice, see below.</p>
<ul>
<li><strong>Tk is Written in</strong>: Tcl</li>
<li>
<p><strong>Portability</strong>: cross-platform (Windows, macOS, Linux).</p>
</li>
<li>
<p><strong>Widgets</strong>: this is not the fort of Tk. It has a <strong>small set</strong> of
default widgets, and misses important ones, for example a date picker. We
can find some in extensions (such as in <strong>Nodgui</strong>), but they dont
feel native, at all. The calendar is brought by a Tk extension and looks better.</p>
</li>
<li>
<p><strong>Interactive development</strong>: very much.</p>
</li>
<li>
<p><strong>Graphical builder</strong>: no</p>
</li>
<li><strong>Other features</strong>:
<ul>
<li><strong>remote execution</strong>: the connection between Lisp and Tcl/Tk is
done via a stream. It is thus possible to run the Lisp program on
one computer, and to display the GUI on another one. The only
thing required on the client computer is tcl/tk installed and the
remote.tcl script. See <a href="http://www.peter-herth.de/ltk/ltkdoc/node46.html">Ltk-remote</a>.</li>
</ul>
</li>
<li><strong>Bindings documentation</strong>: short but complete. Nodgui too.</li>
<li><strong>Bindings stability</strong>: very stable</li>
<li><strong>Bindings activity</strong>: low for Ltk (mostly maintenance), active for nodgui (new features).</li>
<li><strong>Licence</strong>: Tcl/Tk is BSD-style, Ltk is LGPL.</li>
<li>Example applications:
<ul>
<li><a href="https://notabug.org/cage/fulci/">Fulci</a> - a program to organise your movie collections.</li>
<li><a href="https://github.com/mijohnson99/ltk-small-games">Ltk small games</a> - snake and tic-tac-toe.</li>
<li><a href="https://github.com/VitoVan/cl-pkr">cl-pkr</a> - a cross-platform color picker.</li>
<li><a href="https://github.com/vindarel/cl-torrents">cl-torrents</a> - searching torrents on popular trackers. CLI, readline and a simple Tk GUI.</li>
</ul>
</li>
<li>More examples:
<ul>
<li><a href="https://peterlane.netlify.app/ltk-examples/">https://peterlane.netlify.app/ltk-examples/</a>: LTk examples for the <a href="https://tkdocs.com/tutorial/index.html">tkdocs</a> tutorial.</li>
<li><a href="https://peterlane.netlify.app/ltk-plotchart/">LTk Plotchart</a> - A wrapper around the tklib/plotchart library to work with LTk. This includes over 20 different chart types (xy-plots, gantt charts, 3d-bar charts etc…).</li>
</ul>
</li>
</ul>
<p><strong>List of widgets</strong></p>
<p>(please dont suppose the list is exhaustive)</p>
<pre><code>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
</code></pre>
<p>nodgui adds:</p>
<pre><code>treelist tooltip searchable-listbox date-picker calendar autocomplete-listbox
password-entry progress-bar-star notify-window
dot-plot bar-chart equalizer-bar
swap-list
</code></pre>
<h3 id="qt4-qtools">Qt4 (Qtools)</h3>
<p>Do we need to present Qt and <a href="https://doc.qt.io/archives/qt-4.8/index.html">Qt4</a>? Qt is huge and contains
everything and the kitchen sink. Qt not only provides UI widgets, but
numerous other layers (networking, D-BUS…).</p>
<p>Qt is free for open-source software, however youll want to check the
conditions to ship proprietary ones.</p>
<p>The <a href="https://github.com/Shinmera/qtools">Qtools</a> bindings target Qt4. The Qt5 Lisp bindings are <a href="https://lispcookbook.github.io/cl-cookbook/in&#32;the&#32;works">https://github.com/commonqt/commonqt5/</a> and not ready for prime time..</p>
<p>A companion library for Qtools, that youll want to check out once you
made your first Qtool application, is
<a href="https://github.com/Shinmera/qtools-ui">Qtools-ui</a>, a collection of
useful widgets and pre-made components. It comes with short
<a href="https://www.youtube.com/playlist?list=PLkDl6Irujx9Mh3BWdBmt4JtIrwYgihTWp">demonstrations
videos</a>.</p>
<!-- possible future: gobject-introspection -->
<ul>
<li><strong>Framework written in</strong>: C++</li>
<li><strong>Framework Portability</strong>: multi-platform, Android, embedded systems, WASM.</li>
<li>
<p><strong>Bindings Portability</strong>: Qtools runs on x86 desktop platforms on Windows, macOS and GNU/Linux.</p>
</li>
<li>
<p><strong>Widgets choice</strong>: large.</p>
</li>
<li>
<p><strong>Graphical builder</strong>: yes.</p>
</li>
<li>
<p><strong>Other features</strong>: Web browser, a lot more.</p>
</li>
<li><strong>Bindings documentation</strong>: lengthy explanations, a few examples. Prior Qt knowledge is required.</li>
<li><strong>Bindings stability</strong>: stable</li>
<li><strong>Bindings activity</strong>: active</li>
<li><strong>Qt Licence</strong>: both commercial and open source licences.</li>
<li>Example applications:
<ul>
<li>https://github.com/Shinmera/qtools/tree/master/examples</li>
<li>https://github.com/Shirakumo/lionchat</li>
<li>https://github.com/shinmera/halftone - a simple image viewer</li>
</ul>
</li>
</ul>
<h3 id="gtk3-cl-cffi-gtk">Gtk+3 (cl-cffi-gtk)</h3>
<p><a href="https://www.gtk.org/">Gtk+3</a> is the primary library used to build <a href="https://www.gnome.org/">GNOME</a>
applications. Its (currently most advanced) lisp bindings is
<a href="https://github.com/Ferada/cl-cffi-gtk/">cl-cffi-gtk</a>. While primarily created for GNU/Linux, Gtk
works fine under macOS and can now also be used on Windows.</p>
<ul>
<li><strong>Framework written in</strong>: C</li>
<li>
<p><strong>Portability</strong>: GNU/Linux and macOS, also Windows.</p>
</li>
<li>
<p><strong>Widgets choice</strong>: large.</p>
</li>
<li><strong>Graphical builder</strong>: yes: Glade.</li>
<li>
<p><strong>Other features</strong>: web browser (WebKitGTK)</p>
</li>
<li><strong>Bindings documentation</strong>: very good: http://www.crategus.com/books/cl-gtk/gtk-tutorial.html</li>
<li><strong>Bindings stability</strong>: stable</li>
<li><strong>Bindings activity</strong>: low activity, active development.</li>
<li><strong>Licence</strong>: LGPL</li>
<li>Example applications:
<ul>
<li>an <a href="https://github.com/ralph-schleicher/atmosphere-calculator">Atmosphere Calculator</a>, built with Glade.</li>
</ul>
</li>
<li>more documentation and examples:
<ul>
<li><a href="https://dev.to/goober99/learn-common-lisp-by-example-gtk-gui-with-sbcl-5e5c">Learn Common Lisp by Example: GTK GUI with SBCL</a></li>
</ul>
</li>
</ul>
<h3 id="iup-lispnikiup">IUP (lispnik/IUP)</h3>
<p><a href="http://webserver2.tecgraf.puc-rio.br/iup/">IUP</a> is a cross-platform GUI toolkit actively developed
at the PUC university of Rio de Janeiro, Brazil. It uses <strong>native
controls</strong>: the Windows API for Windows, Gtk3 for GNU/Linux. At the
time of writing, it has a Cocoa port in the works (as well as iOS,
Android and WASM ones). A particularity of IUP is its <strong>small API</strong>.</p>
<p>The Lisp bindings are <a href="https://github.com/lispnik/iup/">lispnik/iup</a>. They are nicely
done in that they are automatically generated from the C sources. They
can follow new IUP versions with a minimal work and the required steps
are documented. All this gives us good guarantee over the bus
factor.</p>
<p>IUP stands as a great solution in between Tk and Gtk or Qt.</p>
<ul>
<li><strong>Framework written in</strong>: C (official API also in Lua and LED)</li>
<li>
<p><strong>Portability</strong>: Windows and Linux, work started for
Cocoa, iOS, Android, WASM.</p>
</li>
<li>
<p><strong>Widgets choice</strong>: medium. Includes a web browser window (WebkitGTK on Linux, IEs WebBrowser on Windows).</p>
</li>
<li>
<p><strong>Graphical builder</strong>: yes: <a href="http://webserver2.tecgraf.puc-rio.br/iup/en/iupvisualled.html">IupVisualLED</a></p>
</li>
<li>
<p><strong>Other features</strong>: OpenGL, Web browser (WebKitGTK on GNU/Linux), plotting, Scintilla text editor</p>
</li>
<li><strong>Bindings documentation</strong>: good examples and good readme, otherwise low.</li>
<li><strong>Bindings stability</strong>: alpha (but fully generated and working nicely).</li>
<li><strong>Bindings activity</strong>: low but steady, and reactive to new IUP versions.</li>
<li><strong>Licence</strong>: IUP and the bindings are MIT licenced.</li>
</ul>
<p><strong>List of widgets</strong></p>
<pre><code>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
</code></pre>
<!-- editor's note: found missing a list view with columns. -->
<p><img src="assets/iup-demo.png" alt="" /></p>
<h3 id="nuklear-bodge-nuklear">Nuklear (Bodge-Nuklear)</h3>
<p><a href="https://github.com/Immediate-Mode-UI/Nuklear">Nuklear</a> is a small <a href="https://en.wikipedia.org/wiki/Immediate_mode_GUI">immediate-mode</a> GUI toolkit:</p>
<blockquote>
<p><a href="https://github.com/Immediate-Mode-UI/Nuklear">Nuklear</a> 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.</p>
</blockquote>
<p>its Lisp binding is <a href="https://github.com/borodust/bodge-nuklear">Bodge-Nuklear</a>, and its higher level companions <a href="https://github.com/borodust/bodge-ui">bodge-ui</a> and <a href="https://github.com/borodust/bodge-ui-window">bodge-ui-window</a>.</p>
<p>Unlike traditional UI frameworks, Nuklear allows the developer to take
over the rendering loop or the input management. This might require
more setup, but it makes Nuklear particularly well suited for games,
or for applications where you want to create new controls.</p>
<ul>
<li><strong>Framework written in</strong>: ANSI C, single-header library.</li>
<li>
<p><strong>Portability</strong>: where C runs. Nuklear doesnt contain
platform-specific code. No direct OS or window handling is done in
Nuklear. Instead <em>all input state has to be provided by platform
specific code</em>.</p>
</li>
<li>
<p><strong>Widgets choice</strong>: small.</p>
</li>
<li>
<p><strong>Graphical builder</strong>: no.</p>
</li>
<li>
<p><strong>Other features</strong>: fully skinnable and customisable.</p>
</li>
<li><strong>Bindings stability</strong>: stable</li>
<li><strong>Bindings activity</strong>: active</li>
<li><strong>Licence</strong>: MIT or Public Domain (unlicence).</li>
<li>Example applications:
<ul>
<li><a href="https://github.com/borodust/trivial-gamekit">Trivial-gamekit</a></li>
<li><a href="https://github.com/thicksteadTHpp/Obvius/">Obvius</a> - a resurrected image processing library.</li>
<li><a href="https://github.com/borodust/notalone">Notalone</a> - an autumn 2017 Lisp Game Jam entry.</li>
</ul>
</li>
</ul>
<p><strong>List of widgets</strong></p>
<p>Non-exhaustive list:</p>
<pre><code>buttons, progressbar, image selector, (collapsable) tree, list, grid, range, slider, color picker,
date-picker
</code></pre>
<p><img src="assets/gui/nuklear.png" alt="" /></p>
<h2 id="getting-started">Getting started</h2>
<h3 id="tk">Tk</h3>
<p>Ltk is quick and easy to grasp.</p>
<pre><code class="language-lisp">(ql:quickload "ltk")
(in-package :ltk-user)
</code></pre>
<p><strong>How to create widgets</strong></p>
<p>All widgets are created with a regular <code>make-instance</code> and the widget name:</p>
<pre><code class="language-lisp">(make-instance 'button)
(make-instance 'treeview)
</code></pre>
<p>This makes Ltk explorable with the default symbol completion.</p>
<p><strong>How to start the main loop</strong></p>
<p>As with most bindings, the GUI-related code must be started inside a macro that
handles the main loop, here <code>with-ltk</code>:</p>
<pre><code class="language-lisp">(with-ltk ()
(let ((frame (make-instance 'frame)))
…))
</code></pre>
<p><strong>How to display widgets</strong></p>
<p>After we created some widgets, we must place them on the layout. There
are a few Tk systems for that, but the most recent one and the one we
should start with is the <code>grid</code>. <code>grid</code> is a function that takes as
arguments the widget, its column, its row, and a few optional
parameters.</p>
<p>As with any Lisp code in a regular environment, the functions
signatures are indicated by the editor. It makes Ltk explorable.</p>
<p>Heres how to display a button:</p>
<pre><code class="language-lisp">(with-ltk ()
(let ((button (make-instance 'button :text "hello")))
(grid button 0 0)))
</code></pre>
<p>Thats all there is to it.</p>
<h4 id="reacting-to-events">Reacting to events</h4>
<p>Many widgets have a <code>:command</code> argument that accept a lambda which is
executed when the widgets event is started. In the case of a button,
that will be on a click:</p>
<pre><code class="language-lisp">(make-instance 'button
:text "Hello"
:command (lambda ()
(format t "clicked")))
</code></pre>
<h4 id="interactive-development">Interactive development</h4>
<p>When we start the Tk process in the background with <code>(start-wish)</code>, we
can create widgets and place them on the grid interactively.</p>
<p>See <a href="http://www.peter-herth.de/ltk/ltkdoc/node8.html">the documentation</a>.</p>
<p>Once were done, we can <code>(exit-wish)</code>.</p>
<h4 id="nodgui">Nodgui</h4>
<p>To try the Nodgui demo, do:</p>
<pre><code class="language-lisp">(ql:quickload "nodgui")
(nodgui.demo:demo)
</code></pre>
<p>but hey, to load the demo with the better looking theme, do:</p>
<pre><code class="language-lisp">(nodgui.demo:demo :theme "yaru")
</code></pre>
<p>or</p>
<pre><code class="language-lisp">(setf nodgui:*default-theme* "yaru")
(nodgui.demo:demo)
</code></pre>
<h4 id="nodgui-ui-themes">Nodgui UI themes</h4>
<p>To use the “yaru” theme that comes with nodgui, we can simply do:</p>
<pre><code class="language-lisp">(with-nodgui ()
(use-theme "yaru")
…)
</code></pre>
<p>or</p>
<pre><code class="language-lisp">(with-nodgui (:theme "yaru")
…)
</code></pre>
<p>or</p>
<pre><code class="language-lisp">(setf nodgui:*default-theme* "yaru")
(with-nodgui ()
…)
</code></pre>
<p>It is also possible to install and load another tcl theme. For example, clone the <a href="https://github.com/rdbende/Forest-ttk-theme">Forest ttk theme</a> or the <a href="https://github.com/TkinterEP/ttkthemes/">ttkthemes</a>. Your project directory would look like this:</p>
<pre><code>yourgui.asd
yourgui.lisp
ttkthemes/
</code></pre>
<p>Inside <code>ttkthemes/</code>, you will find themes under the <code>png/</code> directory (the other ones are currently not supported):</p>
<pre><code>/ttkthemes/ttkthemes/png/arc/arc.tcl
</code></pre>
<p>You need to load the .tcl file with nodgui, and tell it to use this theme:</p>
<pre><code class="language-lisp">(with-nodgui ()
(eval-tcl-file "/ttkthemes/ttkthemes/png/arc/arc.tcl")
(use-theme "arc")
… code here …)
</code></pre>
<p>and thats it. Your application now uses a new and decently looking GUI theme.</p>
<h3 id="qt4">Qt4</h3>
<pre><code class="language-lisp">(ql:quickload '(:qtools :qtcore :qtgui))
</code></pre>
<pre><code class="language-lisp">(defpackage #:qtools-test
(:use #:cl+qt)
(:export #:main))
(in-package :qtools-test)
(in-readtable :qtools)
</code></pre>
<p>We create our main widget that will contain the rest:</p>
<pre><code class="language-lisp">(define-widget main-window (QWidget)
())
</code></pre>
<p>We create an input field and a button inside this main widget:</p>
<pre><code class="language-lisp">(define-subwidget (main-window name) (q+:make-qlineedit main-window)
(setf (q+:placeholder-text name) "Your name please."))
</code></pre>
<pre><code class="language-lisp">(define-subwidget (main-window go-button) (q+:make-qpushbutton "Go!" main-window))
</code></pre>
<p>We stack them horizontally:</p>
<pre><code class="language-lisp">(define-subwidget (main-window layout) (q+:make-qhboxlayout main-window)
(q+:add-widget layout name)
(q+:add-widget layout go-button))
</code></pre>
<p>and we show them:</p>
<pre><code class="language-lisp">(with-main-window
(window 'main-window))
</code></pre>
<p><img src="assets/gui/qtools-intro.png" alt="" /></p>
<p>Thats cool, but we dont react to the click event yet.</p>
<h4 id="reacting-to-events-1">Reacting to events</h4>
<p>Reacting to events in Qt happens through signals and slots. <strong>Slots</strong> are
functions that receive or “connect to” signals, and <strong>signals</strong> are event carriers.</p>
<p>Widgets already send their own signals: for example, a button sends a
“pressed” event. So, most of the time, we only need to connect to them.</p>
<p>However, had we extra needs, we can create our own set of signals.</p>
<h5 id="built-in-events">Built-in events</h5>
<p>We want to connect our <code>go-button</code> to the <code>pressed</code> and
<code>return-pressed</code> events and display a message box.</p>
<ul>
<li>we need to do this inside a <code>define-slot</code> function,</li>
<li>where we establish the connection to those events,</li>
<li>and where we create the message box. We grab the text of the <code>name</code>
input field with <code>(q+:text name)</code>.</li>
</ul>
<pre><code class="language-lisp">(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))))
</code></pre>
<p>And voilà. Run it with</p>
<pre><code class="language-lisp">(with-main-window (window 'main-window))
</code></pre>
<h5 id="custom-events">Custom events</h5>
<p>Well implement the same functionality as above, but for demonstration
purposes well create our own signal named <code>name-set</code> to throw when
the button is clicked.</p>
<p>We start by defining the signal, which happens inside the
<code>main-window</code>, and which is of type <code>string</code>:</p>
<pre><code class="language-lisp">(define-signal (main-window name-set) (string))
</code></pre>
<p>We create a <strong>first slot</strong> to make our button react to the <code>pressed</code>
and <code>return-pressed</code> events. But instead of creating the message box
here, as above, we send the <code>name-set</code> signal, with the value of our
input field..</p>
<pre><code class="language-lisp">(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)))
</code></pre>
<p>So far, nobody reacts to <code>name-set</code>. We create a <strong>second slot</strong> that
connects to it, and displays our message. Here again, we precise the
parameter type.</p>
<pre><code class="language-lisp">(define-slot (main-window name-set) ((new-name string))
(declare (connected main-window (name-set string)))
2024-01-12 09:23:31 +01:00
(q+:qmessagebox-information main-window "Greetings"
(format NIL "Good day to you, ~a!" new-name)))
2023-10-25 11:23:21 +02:00
</code></pre>
<p>and run it:</p>
<pre><code class="language-lisp">(with-main-window (window 'main-window))
</code></pre>
<h4 id="building-and-deployment">Building and deployment</h4>
<p>It is possible to build a binary and bundle it together with all the
necessary shared libraries.</p>
<p>Please read <a href="https://github.com/Shinmera/qtools#deployment">https://github.com/Shinmera/qtools#deployment</a>.</p>
<p>You might also like <a href="https://github.com/phoe-trash/furcadia-post-splitter/blob/master/.travis.yml">this Travis CI script</a> to build a self-contained binary for the three OSes.</p>
<h3 id="gtk3">Gtk3</h3>
<p>The
<a href="http://www.crategus.com/books/cl-gtk/gtk-tutorial.html">documentation</a>
is exceptionally good, including for beginners.</p>
<p>The library to quickload is <code>cl-cffi-gtk</code>. It is made of numerous
ones, that we have to <code>:use</code> for our package.</p>
<pre><code class="language-lisp">(ql:quickload "cl-cffi-gtk")
(defpackage :gtk-tutorial
(:use :gtk :gdk :gdk-pixbuf :gobject
:glib :gio :pango :cairo :common-lisp))
(in-package :gtk-tutorial)
</code></pre>
<p><strong>How to run the main loop</strong></p>
<p>As with the other libraries, everything happens inside the main loop
wrapper, here <code>with-main-loop</code>.</p>
<p><strong>How to create a window</strong></p>
<p><code>(make-instance 'gtk-window :type :toplevel :title "hello" ...)</code>.</p>
<p><strong>How to create a widget</strong></p>
<p>All widgets have a corresponding class. We can create them with
<code>make-instance 'widget-class</code>, but we preferably use the constructors.</p>
<p>The constructors end with (or contain) “new”:</p>
<pre><code class="language-lisp">(gtk-label-new)
(gtk-button-new-with-label "Label")
</code></pre>
<p><strong>How to create a layout</strong></p>
2024-01-12 09:23:31 +01:00
<pre><code class="language-lisp">(let ((box (make-instance 'gtk-box :orientation :horizontal
:spacing 6))) ...)
2023-10-25 11:23:21 +02:00
</code></pre>
<p>then pack a widget onto the box:</p>
<pre><code class="language-lisp">(gtk-box-pack-start box mybutton-1)
</code></pre>
<p>and add the box to the window:</p>
<pre><code class="language-lisp">(gtk-container-add window box)
</code></pre>
<p>and display them all:</p>
<pre><code class="language-lisp">(gtk-widget-show-all window)
</code></pre>
<h4 id="reacting-to-events-2">Reacting to events</h4>
<p>Use <code>g-signal-connect</code> + the concerned widget + the event name (as a
string) + a lambda, that takes the widget as argument:</p>
<pre><code class="language-lisp">(g-signal-connect window "destroy"
(lambda (widget)
(declare (ignore widget))
(leave-gtk-main)))
</code></pre>
<p>Or again:</p>
<pre><code class="language-lisp">(g-signal-connect button "clicked"
(lambda (widget)
(declare (ignore widget))
(format t "Button was pressed.~%")))
</code></pre>
<h4 id="full-example">Full example</h4>
<pre><code class="language-lisp">(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"
2024-01-12 09:23:31 +01:00
(lambda (widget)
(declare (ignore widget))
(format t "Button 2 was pressed.~%")))
2023-10-25 11:23:21 +02:00
(gtk-box-pack-start box button))
(gtk-container-add window box)
(gtk-widget-show-all window))))
</code></pre>
<p><img src="assets/gui/gtk3-hello-buttons.png" alt="" /></p>
<h3 id="iup">IUP</h3>
<p>Please check the installation instructions upstream. You may need one
system dependency on GNU/Linux, and to modify an environment variable
on Windows.</p>
<p>Finally, do:</p>
<pre><code class="language-lisp">(ql:quickload "iup")
</code></pre>
<p>We are not going to <code>:use</code> IUP (it is a bad practice generally after all).</p>
<pre><code class="language-lisp">(defpackage :test-iup
(:use :cl))
(in-package :test-iup)
</code></pre>
<p>The following snippet creates a dialog frame to display a text label.</p>
<pre><code class="language-lisp">(defun hello ()
(iup:with-iup ()
2024-01-12 09:23:31 +01:00
(let* ((label (iup:label
:title
(format nil "Hello, World!~%IUP ~A~%~A ~A"
(iup:version)
(lisp-implementation-type)
(lisp-implementation-version))))
2023-10-25 11:23:21 +02:00
(dialog (iup:dialog label :title "Hello, World!")))
(iup:show dialog)
(iup:main-loop))))
(hello)
</code></pre>
<p>Important note for SBCL: we currently must trap division-by-zero
errors (see advancement on <a href="https://github.com/lispnik/iup/issues/30">this
issue</a>). So, run snippets
like so:</p>
<pre><code class="language-lisp">(defun run-gui-function ()
#-sbcl (gui-function)
#+sbcl
(sb-int:with-float-traps-masked
(:divide-by-zero :invalid)
(gui-function)))
</code></pre>
<p><strong>How to run the main loop</strong></p>
<p>As with all the bindings seen so far, widgets are shown inside a
<code>with-iup</code> macro, and with a call to <code>iup:main-loop</code>.</p>
<p><strong>How to create widgets</strong></p>
<p>The constructor function is the name of the widget: <code>iup:label</code>,
<code>iup:dialog</code>.</p>
<p><strong>How to display a widget</strong></p>
<p>Be sure to “show” it: <code>(iup:show dialog)</code>.</p>
<p>You can group widgets on <code>frame</code>s, and stack them vertically or
horizontally (with <code>vbox</code> or <code>hbox</code>, see the example below).</p>
<p>To allow a widget to be expanded on window resize, use <code>:expand
:yes</code> (or <code>:horizontal</code> and <code>:vertical</code>).</p>
<p>Use also the <code>:alignement</code> properties.</p>
<p><strong>How to get and set a widgets attributes</strong></p>
<p>Use <code>(iup:attribute widget attribute)</code> to get the attributes value,
and use <code>setf</code> on it to set it.</p>
<h4 id="reacting-to-events-3">Reacting to events</h4>
<p>Most widgets take an <code>:action</code> parameter that takes a lambda function
with one parameter (the handle).</p>
<pre><code class="language-lisp">(iup:button :title "Test &amp;1"
:expand :yes
:tip "Callback inline at control creation"
:action (lambda (handle)
(iup:message "title" "button1's action callback")
iup:+default+))
</code></pre>
<p>Below we create a label and put a button below it. We display a
message dialog when we click on the button.</p>
<pre><code class="language-lisp">(defun click-button ()
(iup:with-iup ()
2024-01-12 09:23:31 +01:00
(let* ((label (iup:label :title
(format nil "Hello, World!~%IUP ~A~%~A ~A"
(iup:version)
(lisp-implementation-type)
(lisp-implementation-version))))
2023-10-25 11:23:21 +02:00
(button (iup:button :title "Click me"
:expand :yes
:tip "yes, click me"
2024-01-12 09:23:31 +01:00
:action
(lambda (handle)
(declare (ignorable handle))
(iup:message "title"
"button clicked")
iup:+default+)))
2023-10-25 11:23:21 +02:00
(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))
</code></pre>
<p>Heres a similar example to make a counter of clicks. We use a label
and its title to hold the count. The title is an integer.</p>
<pre><code class="language-lisp">(defun counter ()
(iup:with-iup ()
(let* ((counter (iup:label :title 0))
2024-01-12 09:23:31 +01:00
(label (iup:label :title
(format nil "The button was clicked ~a time(s)."
(iup:attribute counter :title))))
2023-10-25 11:23:21 +02:00
(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)))
</code></pre>
<h4 id="list-widget-example">List widget example</h4>
<p>Below we create three list widgets with simple and multiple selection, we
set their default value (the pre-selected row) and we place them
horizontally side by side.</p>
<pre><code class="language-lisp">(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)))
</code></pre>
<h3 id="nuklear">Nuklear</h3>
<p><strong>Disclaimer</strong>: as per the authors words at the time of writing,
bodge-ui is in early stages of development and not ready for general
use yet. There are some quirks that need to be fixed, which might
require some changes in the API.</p>
<p><code>bodge-ui</code> is not in Quicklisp but in its own Quicklisp distribution. Lets install it:</p>
<pre><code class="language-lisp">(ql-dist:install-dist "http://bodge.borodust.org/dist/org.borodust.bodge.txt" :replace t :prompt nil)
</code></pre>
<p>Uncomment and evaluate this line only if you want to enable the OpenGL 2
renderer:</p>
<pre><code class="language-lisp">;; (cl:pushnew :bodge-gl2 cl:*features*)
</code></pre>
<p>Quickload <code>bodge-ui-window</code>:</p>
<pre><code class="language-lisp">(ql:quickload "bodge-ui-window")
</code></pre>
<p>We can run the built-in example:</p>
<pre><code class="language-lisp">(ql:quickload "bodge-ui-window/examples")
(bodge-ui-window.example.basic:run)
</code></pre>
<p>Now lets define a package to write a simple application.</p>
<pre><code class="language-lisp">(cl:defpackage :bodge-ui-window-test
(:use :cl :bodge-ui :bodge-host))
(in-package :bodge-ui-window-test)
</code></pre>
<pre><code class="language-lisp">(defpanel (main-panel
(:title "Hello Bodge UI")
(:origin 200 50)
(:width 400) (:height 400)
(:options :movable :resizable
:minimizable :scrollable
:closable))
(label :text "Nested widgets:")
(horizontal-layout
(radio-group
(radio :label "Option 1")
(radio :label "Option 2" :activated t))
(vertical-layout
(check-box :label "Check 1" :width 100)
(check-box :label "Check 2"))
(vertical-layout
(label :text "Awesomely" :align :left)
(label :text "Stacked" :align :centered)
(label :text "Labels" :align :right)))
(label :text "Expand by width:")
(horizontal-layout
(button :label "Dynamic")
(button :label "Min-Width" :width 80)
(button :label "Fixed-Width" :expandable nil :width 100))
(label :text "Expand by width:")
(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 "Rest:")
(button :label "Top-level Button"))
(defparameter *window-width* 800)
(defparameter *window-height* 600)
(defclass main-window (bodge-ui-window:ui-window) ()
(:default-initargs
:title "Bodge UI Window Example"
: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)))
</code></pre>
<p>and run it:</p>
<pre><code class="language-lisp">(run)
</code></pre>
<p><img src="assets/gui/nuklear-test.png" alt="" /></p>
<p>To react to events, use the following signals:</p>
<pre><code>:on-click
:on-hover
:on-leave
:on-change
:on-mouse-press
:on-mouse-release
</code></pre>
<p>They take as argument a function with one argument, the panel. But
beware: they will be called on each rendering cycle when the widget is
on the given state, so potentially a lot of times.</p>
<h4 id="interactive-development-1">Interactive development</h4>
<p>If you ran the example in the REPL, you couldnt see whats cool. Put
the code in a lisp file and run it, so than you get the window. Now
you can change the panel widgets and the layout, and your changes will
be immediately applied while the application is running!</p>
<h2 id="conclusion">Conclusion</h2>
<p>Have fun, and dont hesitate to share your experience and your apps.</p>
<p class="page-source">
Page source: <a href="https://github.com/LispCookbook/cl-cookbook/blob/master/gui.md">gui.md</a>
</p>
</div>
<script type="text/javascript">
// Don't write the TOC on the index.
if (window.location.pathname != "/cl-cookbook/") {
$("#toc").toc({
content: "#content", // will ignore the first h1 with the site+page title.
headings: "h1,h2,h3,h4"});
}
$("#two-cols + ul").css({
"column-count": "2",
});
$("#contributors + ul").css({
"column-count": "4",
});
</script>
<div>
<footer class="footer">
<hr/>
&copy; 2002&ndash;2023 the Common Lisp Cookbook Project
<div>
2024-05-15 18:18:38 +02:00
📹 Discover <a style="color: darkgrey; text-decoration: underline", href="https://www.udemy.com/course/common-lisp-programming/?referralCode=2F3D698BBC4326F94358">our contributor's Common Lisp video course on Udemy</a>
2023-10-25 11:23:21 +02:00
</div>
</footer>
</div>
<div id="toc-btn">T<br>O<br>C</div>
</div>
<script text="javascript">
HighlightLisp.highlight_auto({className: null});
</script>
<script type="text/javascript">
function duckSearch() {
var searchField = document.getElementById("searchField");
if (searchField && searchField.value) {
var query = escape("site:lispcookbook.github.io/cl-cookbook/ " + searchField.value);
window.location.href = "https://duckduckgo.com/?kj=b2&kf=-1&ko=1&q=" + query;
// https://duckduckgo.com/params
// kj=b2: blue header in results page
// kf=-1: no favicons
}
}
</script>
<script async defer data-domain="lispcookbook.github.io/cl-cookbook" src="https://plausible.io/js/plausible.js"></script>
</body>
</html>