Недавно нашлось время поработать над cl-gtk2, выложил пару изменений. Ниже — краткая их сводка.

Проверка на минимальную версию библиотек

При загрузке cl-gtk2 теперь проверяет, какая версия Gtk+ установлена и генерирует ошибку комиляции, если версия слишком старая. Видимо, вопросов о том, почему не cl-gtk2 компилируется, должно быть меньше.

Поддержка нескольких версий Gtk+

Добавлена рудиментарная поддержка нескольких версий Gtk+. При загрузке cl-gtk2 добавляет в *features* символы, соответствующие версиям glib и Gtk+, и остальной код может использовать их для условной компиляции классов/функций/методов. Поэтому, если загрузить cl-gtk2 при установленной Gtk+-2.18, то будут доступны классы из Gtk+-2.18. Но при обновлении Gtk+ надо будет перекомпилировать cl-gtk2.

Улучшена демонстрационная программа

Немного переделал интерфейс демо-программы gtk-demo:demo, теперь выглядит как текстовая страница со ссылками на демонстрации:

Работа с главным циклом приложения

Теперь функции ensure-gtk-main, leave-gtk-main, join-gtk-main и макрос within-main-loop работают схожим образом как в многопоточных лиспах, так и в однопоточных.

Использовать их следует следующим образом. В главную функцию 'main' помещается код вида:

(defun run ()
  (within-main-loop
    (your-application-code) ;; в какой-то момент приложение вызове leave-gtk-main
  ))
(defun main ()
  (run)
  (join-gtk-main)
  (quit))

Тогда функция main вернет управление, когда приложение вызовет leave-gtk-main, и приложение завершится. Функцию main можно использовать в качестве toplevel-функции при сохранении образа или при запуске скрипта, а функцию run можно использовать для запуска приложения во время разработки — приложение будет запущено в фоне и можно будет спокойно его дописывать.

Потокобезопасная финализация экземпляров GBoxed-типов

Как-то совсем забыл изначально сделать финализацию GBoxed'ов потокобезопасной, а теперь вот вспомнил — встретил ряд багов, связанных с этим.

Сегодня удалось запустить cl-gtk2 под windows.

Простая проблема была в том, что неправильно были описаны gtk'шные библиотеки (неправильные имена файлов).

Чуть более сложная проблема была в том, что CFFI оптимизировал вызов foreign-funcall с константным аргументом. А виндовый порт SBCL пытался прилинковать вызываемую функцию при компиляции даже в том случае, если вызов никогда не происходил. До оптимизации (foreign-funcall-pointer (foreign-symbol-pointer ...) ...) авторы CFFI не дошли, что хорошо:)

Текущая версия cl-gtk2 работает только с достаточно свежей версией Gtk+-2.16. Взять ее можно здесь. После распаковки библиотек следует добавить путь к gtk/bin в PATH, чтобы CFFI нашел библиотеки. Либо же можно добавить путь к gtk/bin в CFFI:*FOREIGN-LIBRARY-DIRECTORIES*. Для работы GtkGlExt аналогично следует установить GtkGlExt, GLUT (раз и два)

На удивление, все заработало почти сразу же - включая даже GtkGlExt - glut'овский чайник крутился на демке.

Но по не выясненным пока причинам, если запускать cl-gtk2 из Slime, то вызов gtk-main приводит к зависанию SBCL. Похоже, что swank делает что-то с файлами/select'ом/сигналами, что плохо взаимодействует с gtk'шным циклом обработки сообщений.

Добавил лисповскую реализацию GtkTreeModel для деревьев в cl-gtk2. А это, в свою очередь, позволяет пользоваться виджетом GtkTreeView для отображения древовидно-табличных данных. Поддержку Gtk'шного GtkTreeModel я не сделал и не вижу в этом необходимости, раз есть своя реализация GtkTreeModel.

На данном скрине запечатлено дерево разбора для лисповского выражения, задающего функцию вычисления квадратного корня (набранная по памяти из SICP). Демка вызывается выражением (gtk-demo:demo-treeview-tree).


Как можно заметить, kde я больше не использую, перешел на gnome.


В качестве средства для создания документации пока остановился на texinfo. Ни одно из средств (среди тех, которые я смотрел и которые удалось запустить) для генерации документации из исходников и documentation strings меня не устроило. Шаблон для документации взял из исходников CFFI. На основе этого шаблона уже задокументировал биндинг к GObject.

Починил в CL-GTK2 возможность сохранения образов в SBCL (всего лишь надо при сохранении образа завершать главный цикл обработки сообщений, а при восстановлении из образа инициализировать Gtk+, зарегистрировать типы, реализованные в лиспе (пока это лишь LispArrayListStore)).

С помощью cl-launch или используя sb-ext:save-lisp-and-die можно создать образ с приложением, который запускается гораздо быстрее (по сравнению с загрузкой CL-GTK2 из fasl'ов или с загрузкой из исходников).

Выложил ebuild для cl-gtk2: dev-lisp/cl-gtk2-9999.ebuild.

Сделал страничку для cl-gtk2 на common-lisp.net: http://common-lisp.net/project/cl-gtk2/.

Продолжаю пилить CL-GTK2. Набросал небольшой туториал. Написал небольшой class browser. Оказалось довольно просто, но выяснилось, что я опять забыл, как пользоваться GtkTreeView.

Вот что получилось:

С помощью вот такого сравнительно небольшого кода:

(defun demo-class-browser ()
  (let ((output *standard-output*))
    (with-main-loop
        (let* ((window (make-instance 'gtk-window
                                      :window-position :center
                                      :title "Class Browser"
                                      :default-width 400
                                      :default-height 600))
               (search-entry (make-instance 'entry))
               (search-button (make-instance 'button :label "Search"))
               (scroll (make-instance 'scrolled-window
                                      :hscrollbar-policy :automatic
                                      :vscrollbar-policy :automatic))
               (slots-model (make-instance 'array-list-store))
               (slots-list (make-instance 'tree-view :model slots-model)))
          (let ((v-box (make-instance 'v-box))
                (search-box (make-instance 'h-box)))
            (container-add window v-box)
            (box-pack-start v-box search-box :expand nil)
            (box-pack-start search-box search-entry)
            (box-pack-start search-box search-button :expand nil)
            (box-pack-start v-box scroll)
            (container-add scroll slots-list))
          (store-add-column slots-model "gchararray"
                            (lambda (slot)
                              (format nil "~S" (closer-mop:slot-definition-name slot))))
          (let ((col (make-instance 'tree-view-column :title "Slot name"))
                (cr (make-instance 'cell-renderer-text)))
            (tree-view-column-pack-start col cr)
            (tree-view-column-add-attribute col cr "text" 0)
            (tree-view-append-column slots-list col))
          (labels ((display-class-slots (class)
                     (format output "Displaying ~A~%" class)
                     (loop
                        repeat (store-items-count slots-model)
                        do (store-remove-item slots-model (store-item slots-model 0)))
                     (closer-mop:finalize-inheritance class)
                     (loop
                        for slot in (closer-mop:class-slots class)
                        do (store-add-item slots-model slot)))
                   (on-search-clicked (button)
                     (declare (ignore button))
                     (with-gtk-message-error-handler
                         (let* ((class-name (read-from-string (entry-text search-entry)))
                                (class (find-class class-name)))
                           (display-class-slots class)))))
            (g-signal-connect search-button "clicked" #'on-search-clicked))
          (widget-show window)))))

Потихоньку изучаю CLOS MOP. Отличная штука.

Хочу использовать для упрощения создания биндинга cl-gtk2.

Можно, например, с помощью MOP'а один раз написать универсальный конструктор (специализация make-instance), доступ к слотам (специализация slot-value-using-class, slot-boundp-using-class, slot-makunbound-using-class), и можно писать такие конструкции:

(defclass gtk-label ()
  ((angle :g-property-name "angle"
          :allocation :gobject
          :accessor gtk-label-angle
          :g-property-type "gdouble")
   (label :allocation :gobject
          :g-property-name "label"
          :g-property-type "gchararray"
          :accessor gtk-label-label))
  (:metaclass gobject-class)
  (:g-type-name . "GtkLabel"))

Записав такое определение, сразу получаем класс, привязанный к соотвествующему gobject-классу:

можно создавать объекты этого класса, и при создании gtk-label будет создаваться GtkLabel с нужными параметрами:

(defparameter *label* (make-instance 'gtk-label :angle 15.0 :label "Привет, мир!"))

сразу имеем доступ к слотам:

(gtk-label-label *label*)
=>
"Привет, мир!"

Конечно, можно обойтись и без MOP (я использую генератор конструктора и функций доступа к слотам на основе макросов), но с MOP получается уж больно красиво и элегантно.

Еще надо разобраться с тем, как работают метаобъекты для обобщенных функций, и как-то суметь реализовать наследование классов от gobject-классов через MOP.

Надо еще убедиться, что MOP поддерживается хотя бы несколькими свободными реализациями лиспа в достаточном объеме.

Выложил свой биндинг к Gtk+ на repo.or.cz. Если есть интерес к биндингу, то welcome.

Чтобы получить код, надо воспользоваться git'ом.

git clone git://repo.or.cz/cl-gtk2.git

или

git clone http://repo.or.cz/r/cl-gtk2.git

UPD: Забыл предупредить: качество биндинга пока не дотягивает даже до альфа-версии. Для использования нужно сделать симлинки на gtk/gtk.asd, glib/glib.asd, gdk/gdk.asd. Разрабатывалось и тестировалось на sbcl amd64, на других системах могут быть разные глюки. Демо-программы лежат в пакете gtk-demo (исходники их находятся в gtk/gtk.demo.lisp).

Захотелось мне добавить к моей долго работающей программе (для дипломной работы) какую-нибудь приемлемую индикацию прогресса работы. Т.е., программа работает себе (вычисляет какую-то свою полезную вещь), и в это время на экране должен отображаться прогресс-бар, который показывает, какая работа сейчас выполняется, какая часть работы сделана и показывает, сколько времени еще осталось до завершения.

На самом деле, так как программа сложная, то нужен не один прогресс-бар, а иерархия прогресс-баров: каждый прогресс-бар нижнего уровня показывает, какая часть работы верхнего уровня сделана.

В итоге, внешний вид получился вот таким:

Сделано очень просто: есть два макроса — with-progress-bar и with-progress-bar-action. Первый макрос навешивается вокруг кода, который выполняет некую работу; в момент начала выполнения кода показывается прогресс-бар, а в момент завершения кода прогресс-бар убирается. Второй макрос навешивается вокруг кода, совершающего некоторую логическую часть работы; в момент завершения выполнения кода содержимое прогресс-бара обновляется и вычисляется оценка времени, которое займет выполнение работы.

Чтобы этот UI не мешал основной программе и не зависал, он запускается в другом потоке.

Примерный use-case:

(defun test-progress ()
  (with-progress-bar ("Snowball" 10)
    (loop
       repeat 10
       do (with-progress-bar-action
            (with-progress-bar ("Texts" 10)
              (loop
                 repeat 10
                 do (with-progress-bar-action (sleep 1))))))))

Помню, когда надо было реализовать аналогичное (и гораздо более простое) отображение прогресса на C#, это заняло у меня где-то полдня. На лиспе же я написал это за полчаса. Хорошее соотношение затраченного времени, я считаю.

А это — мой прототип Lisp IDE :)

Мой биндинг к gtk+ начал приобретать форму. Есть управление памятью (как для GObject, так и для GBoxed), описание объектов, генератор описания типов по метаданным, привязка обработчиков сигналов. Не хватает возможности создать класс, являющийся наследником класса из GObject и реализовать интерфейс (но есть прототип) и описания всего API Gtk+; а также нужны биндинги к Cairo, Pango.

Смог понять, как создавать свои типы GObject'ов и реализовывать в них интерфейсы (с помощью одного макроса по описанию интерфейса генерируется код инициализации интерфейса, callback'и, перенаправляющие вызов метода интерфейса в вызов generic function, и собственно сами generic function).

Попробовал реализовать интерфейс GtkTreeModel — работает.

Пожалуй, это один из нетривиальных моментов.

(defun test-treeview ()
  (let* ((window (make-instance 'gtk:gtk-window :type :toplevel :title "Treeview" :border-width 30))
         (model (make-instance 'tree-model))
         (tv (make-instance 'gtk:tree-view :model model :headers-visible t)))
    (g-signal-connect window "destroy" (lambda (w) (declare (ignore w)) (gtk:gtk-main-quit)))
    (let ((column (make-instance 'gtk:tree-view-column :title "Number"))
          (renderer (make-instance 'gtk:cell-renderer-text :text "A text")))
      (%gtk-tree-view-column-pack-start column renderer t)
      (%gtk-tree-view-column-add-attribute column renderer "text" 0)
      (%gtk-tree-view-append-column tv column))
    (gtk:container-add window tv)
    (gtk:gtk-widget-show-all window)
    (gtk:gtk-main)))
Tree View

В GTK+/Glib/GObject, оказывается, продумали создание биндингов. Например, везде, где используются сигналы (способ оповещения о событиях в UI, например, сигнал "clicked" у кнопки, на который можно навешать обработчики), можно передавать не только указатель на сишную функцию, но и объект GClosure. Тут-то и заключено удобство. Во-первых, в GClosure можно передавать свои данные. Во-вторых, каждому GClosure можно сопоставить свою функцию маршалинга данных и вызова внешней функции (которая соберет аргументы, которые заботливо сложены в массив GValue, в которых указаны тип и значение), вызовет нужную функцию, и запишет ее результат в другое GValue). В-третьих, когда GClosure становится ненужным (используется подсчет ссылок), у него вызывается функция финализации (которую можно написать свою). В итоге, например, для возможности передавать лисповские замыкания в качестве обработчиков сигналов в GTK+, достаточно всего двух callback'ов: для функции маршалинга и для функции финализации.

При этом, мы имеем:

  1. Для создания callback'ов достаточно CFFI (а он не позволяет переносимо создавать сишные колбэки из замыканий, а только из свободных функций).
  2. Можем передавать замыкания в качестве обработчиков сигналов.
  3. Замыкания уничтожаются, когда соответствующий виджет уничтожается.
Таким образом, и пользоваться удобно, и нет утечек замыканий. Почти нет — если все же виджет по каким-то причинам не уничтожен, то и его обработчики сигналов тоже висят в памяти. Но обнаружение таких виджетов — уже другая, независимая, задача.

Вот что можно делать:

(g-signal-connect-closure
  button
  "clicked"
  (bare-gtk::create-closure
    (let ((count 0))
      (lambda (widget) (format t "Нажал ~A раз~%" (incf count)))))
  +false+)

Смотря на исходники lgtk и clg (достаточно качественные биндинги), задаюсь вопросом: а зачем я это делаю? Наверное, просто так, чтобы уметь. Ну и то, что они используют sbcl/cmucl/clisp-специфичные функции, а я стараюсь оставаться в рамках CFFI (для переносимости). Особенно интересно, как в lgtk реализованы callback'и, восстановление после ошибок и сборка мусора. Это действительно стоит внимания.

Убедился, что на лиспе вполне можно писать переносимые приложения с GUI. Без всякой подготовки запустил свои примеры для биндинга к gtk+ на clisp, хотя пишу его в SBCL.

На картинке запущено два одинаковых примера: один из sbcl, другой из clisp.

картинка )
Один экзамен закончился, и решил немного отдохнуть.
Попробовал запустить lambda-gtk в sbcl, но он не пошел. Запуск примеров обламывался на стадии создания callback'ов.
Достаточно оказалось изменить два макроса: gtk:define-signal-handler и g::callback, чтобы все заработало.
Теперь могу всякие guiшные вещи писать.
Так выглядит один из примеров:

Profile

dmitry_vk

April 2023

S M T W T F S
      1
234567 8
9101112131415
16171819202122
23242526272829
30      

Syndicate

RSS Atom

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Apr. 5th, 2026 04:48 am
Powered by Dreamwidth Studios