Решил я написать одно кроссплатформенное десктопное приложение на Go. Сделал CLI-версию, всё работает отлично. Да ещё и кросскомпиляция в Go поддерживается. Всё в общем отлично. Но понадобилась также и GUI-версия. И тут началось...
Выбор библиотеки (биндинга) для GUI
Приложение должно было быть кроссплатформенным.
Поэтому должно компилироваться под Windows, GNU/Linux и macOS.
Выбор пал на такие библиотеки:
- gotk3 (GTK+ 3)
- therecipe/qt (Qt)
- zserge/webview (нативный вебвью)
Electron и прочие фреймворки, которые тянут с собой Chromium и node.js, я откинул так как они весят достаточно много, ещё и съедают много ресурсов операционной системы.
Теперь немного о каждой библиотеке.
gotk3
Биндинг библиотеки GTK+ 3. Покрытие далеко не всех возможностей, но всё основное присутсвует.
Компилируется приложение с помощью стандартного go build
. Кроссплатформенная компиляция возможна, за исключением macOS. Только с macOS можно скомпилировать под эту ОС, ну и с macOS можно будет скомпилировать и под Windows + GNU/Linux.
Интерфейс будет выглядить нативно для GNU/Linux, Windows (нужно будет указать специальную тему). Для macOS будет выглядеть не нативно. Выкрутиться можно только разве что страшненькой темой, которая будет эмулирувать нативные элементы macOS.
therecipe/qt
Биндинг библиотеки Qt 5. Поддержка QML, стандартных виджетов. Вообще этот биндинг многие советуют.
Компилируется с помощью специальной команды qtdeploy
. Кроме десктопных платформ есть также и мобильные. Кросскомпиляция происходит с помощью Docker. Под операционные системы Apple можно скомпилировать только с macOS.
При желании на Qt можно добиться чтобы интерфейс выглядел нативно на десктопных ОС.
zserge/webview
Библиотека, которая написана изначально на C, автор прикрутил её ко многим языкам, в том числе и к Go. Использывается нативный webview для отображения: Windows — MSHTML, GNU/Linux — gtk-webkit2, macOS — Cocoa/WebKit. Кроме кода на Go нужно будет и на JS пописать, ну и HTML пригодится.
Компилируется при помощи go build
, кросскомпиляция возможна с помощью xgo.
Выглядеть нативно может настолько насколько позволит стандартный браузер.
Выбор
Почему же я выбрал именно gotk3?
В therecipe/qt мне не понравилась слишком сложная система сборки приложения, даже специальную команду сделали.
zserge/webview вроде бы не плох, весить будет не много, но всё-таки это webview и могут быть стандартные проблемы, которые бывают в таких приложениях: может что-то где-то поехать. И это не Electron, где всегда в комплекте продвинутый Chromium, а в какой-нибудь старой Windows может всё поехать. Да и к тому же придётся ещё и на JS писать.
gotk3 я выбрал как что-то среднее. Можно собирать стандартным go build
, выглядит приемлемо, да и вообще я GTK+ 3 люблю!
В общем я думал, что всё будет просто. И что зря про Go говорят, что в нём проблема с GUI. Но как же я ошибался...
Начинаем
Устанавливаем всё из gotk3 (gtk, gdk, glib, cairo) себе:
go get github.com/gotk3/gotk3/...
Также у вас в системе должна быть установлена сама библиотека GTK+ 3 для разработки.
GNU/Linux
В Ubuntu:
sudo apt-get install libgtk-3-dev
В Arch Linux:
sudo pacman -S gtk3
macOS
Через Homebrew:
brew install gtk-mac-integration gtk+3
Windows
Здесь всё не так просто. В официальной инструкции предлагают использовать MSYS2 и уже в ней всё делать. Лично я писал код на других операционных системах, а кросскомпиляцию для Windows делал в Arch Linux, о чём надеюсь скоро напишу.
Простой пример
Теперь пишем небольшой файл с кодом main.go
:
package main
import (
"log"
"github.com/gotk3/gotk3/gtk"
)
func main() {
// Инициализируем GTK.
gtk.Init(nil)
// Создаём окно верхнего уровня, устанавливаем заголовок
// И соединяем с сигналом "destroy" чтобы можно было закрыть
// приложение при закрытии окна
win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
if err != nil {
log.Fatal("Не удалось создать окно:", err)
}
win.SetTitle("Простой пример")
win.Connect("destroy", func() {
gtk.MainQuit()
})
// Создаём новую метку чтобы показать её в окне
l, err := gtk.LabelNew("Привет, gotk3!")
if err != nil {
log.Fatal("Не удалось создать метку:", err)
}
// Добавляем метку в окно
win.Add(l)
// Устанавливаем размер окна по умолчанию
win.SetDefaultSize(800, 600)
// Отображаем все виджеты в окне
win.ShowAll()
// Выполняем главный цикл GTK (для отрисовки). Он остановится когда
// выполнится gtk.MainQuit()
gtk.Main()
}
Спомпилировать можно с помощью команды go build
, а потом запустить бинарник. Но мы просто запустим его:
go run main.go
После запуска получим окно такого вида:
Поздравляю! У вас получилось простое приложение из README gotk3!
Больше примеров можно найти на Github gotk3. Их разбирать я не буду. Давайте лучше займёмся тем, чего нет в примерах!
Glade
Есть такая вещь для Gtk+ 3 — Glade. Это конструктор графических интерфейсов для GTK+. Выглядит примерно так:
Чтобы вручную не создавать каждый элемент и не помещать его потом где-то в окне с помощью программного кода, можно весь дизайн накидать в Glade. Потом сохранить всё в XML-подобный файл *.glade и загрузить его уже через наше приложение.
Установка Glade
GNU/Linux
В дистрибутивах GNU/Linux установить glade не составит труда. В какой-нибудь Ubuntu это будет:
sudo apt-get install glade
В Arch Linux:
sudo pacman -S glade
macOS
В загрузках с официального сайта очень старая сборка. Поэтому устанавливать лучше через Homebrew:
brew install glade
А запускать потом:
glade
Windows
Скачать не самую последнюю версию можно здесь. Я лично на Windows вообще не устанавливал, поэтому не знаю насчёт стабильность работы там Glade.
Простое приложение с использованием Glade
В общем надизайнил я примерно такое окно:
Сохранил и получил файл main.glade
:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkWindow" id="window_main">
<property name="title" translatable="yes">Пример Glade</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkEntry" id="entry_1">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_1">
<property name="label" translatable="yes">Go</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">This is label</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
То есть у нас получилось окно window_main
(GtkWindow
), в котором внутри контейнер (GtkBox
), который содержит поле ввода entry_1
(GtkEntry
), кнопку button_1
(GtkButton
) и метку label_1
(GtkLabel
). Кроме этого ещё имеются аттрибуты отсупов (я настроил немного), видимость и другие аттрибуты, которые Glade добавила автоматически.
Давайте теперь попробуем загрузить это представление в нашем main.go
:
package main
import (
"log"
"github.com/gotk3/gotk3/gtk"
)
func main() {
// Инициализируем GTK.
gtk.Init(nil)
// Создаём билдер
b, err := gtk.BuilderNew()
if err != nil {
log.Fatal("Ошибка:", err)
}
// Загружаем в билдер окно из файла Glade
err = b.AddFromFile("main.glade")
if err != nil {
log.Fatal("Ошибка:", err)
}
// Получаем объект главного окна по ID
obj, err := b.GetObject("window_main")
if err != nil {
log.Fatal("Ошибка:", err)
}
// Преобразуем из объекта именно окно типа gtk.Window
// и соединяем с сигналом "destroy" чтобы можно было закрыть
// приложение при закрытии окна
win := obj.(*gtk.Window)
win.Connect("destroy", func() {
gtk.MainQuit()
})
// Отображаем все виджеты в окне
win.ShowAll()
// Выполняем главный цикл GTK (для отрисовки). Он остановится когда
// выполнится gtk.MainQuit()
gtk.Main()
}
Снова запускаем:
go run main.go
И получаем:
Ура! Теперь мы представление формы держим XML-подобном main.glade
файле, а код в main.go
!
Сигналы
Окно запускается, но давайте добавим интерактивности. Пусть текст из поля ввода при нажатии на кнопку попадёт в метку.
Для этого для начала получим элементы поля ввода, кнопки и метке в коде:
// Получаем поле ввода
obj, _ = b.GetObject("entry_1")
entry1 := obj.(*gtk.Entry)
// Получаем кнопку
obj, _ = b.GetObject("button_1")
button1 := obj.(*gtk.Button)
// Получаем метку
obj, _ = b.GetObject("label_1")
label1 := obj.(*gtk.Label)
Я не обрабатываю ошибки, которые возвращает функция GetObject()
, для того, чтобы код был более простым. Но в реальном рабочем приложении их обязательно необходимо обработать.
Хорошо. С помощью кода выше мы получаем наши элементы формы. А теперь давайте обработаем сигнал clicked
кнопку (когда кнопка нажата). Сигнал GTK+ — это по сути реакция на событие. Добавим код:
// Сигнал по нажатию на кнопку
button1.Connect("clicked", func() {
text, err := entry1.GetText()
if err == nil {
// Устанавливаем текст из поля ввода метке
label1.SetText(text)
}
})
Теперь запускаем код:
go run main.go
После ввода какого-нибудь текста в поле и нажатию по кнопке Go мы увидем этот текст в метке:
Теперь у нас интерактивное приложение!
Заключение
На данном этапе всё кажется простым и не вызывает трудностей. Но трудности у меня появились при кросскомпиляции (ведь gotk3 компилируется с CGO), интеграции с операционными системами и с диалогом выбора файла. Я даже добавил в проект gotk нативный диалог. Также в моём проекте нужна была интернационализация. Там тоже есть некоторые особенности. Если вам интересно увидеть это всё сейчас в коде, то можно подсмотреть здесь.
Исходые коды примеров из статьи находятся здесь.
А если хотите почитать продолжение, то можете проголосовать. И в случае, если окажется это кому-нибудь интересным, я продолжу писать.