Решил я написать одно кроссплатформенное десктопное приложение на 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 нативный диалог. Также в моём проекте нужна была интернационализация. Там тоже есть некоторые особенности. Если вам интересно увидеть это всё сейчас в коде, то можно подсмотреть здесь.
Исходые коды примеров из статьи находятся здесь.
А если хотите почитать продолжение, то можете проголосовать. И в случае, если окажется это кому-нибудь интересным, я продолжу писать.
Комментарии (39)
Bookvarenko
13.08.2018 13:59Автор, что дальше-то было? Не томи!
jhekasoft Автор
13.08.2018 14:01Судя по голосованию придётся писать дальше. :)
c0f04
13.08.2018 18:49+1Если сталкивались, особенно интересует подключение собственных виджетов из сишных библиотек и работа с TreeView (как с ListStore, так и с TreeStore), в частности с итераторами. А в идеале и создание собственных виджетов на Golang.
Хочется перевести проект с Си на Golang, но пока непонятно будет ли оно стоить. А дома времени на эксперименты почти нет.jhekasoft Автор
13.08.2018 18:56+1С TreeView и ListStore работал, с итераторами. Думаю расскажу. Собственные виджеты не делал. Это был мой первый опыт с GTK+. :)
serf
14.08.2018 12:04Я угадаю, дальше произошел перезед на electronjs.org
jhekasoft Автор
14.08.2018 12:11Нет. Я написал приложение на nw.js (https://github.com/jhekasoft/insteadman2), понял, что такие технологии, в которых нужня тянуть Chromium и node.js меня не устраивают вообще. И поэтому переписал приложение на Go + gotk3 (https://github.com/jhekasoft/insteadman3).
serf
14.08.2018 15:27А что не устроило в Chromium и node.js? Пробовали TypeScript вместо JavaScript? На JavaScript я бы тоже стал писать что-то сложнее одноразовых простых скриптов.
serf
14.08.2018 15:54На JavaScript я бы тоже НЕ стал писать что-то сложнее одноразовых простых скриптов.
jhekasoft Автор
14.08.2018 17:42Не устроило то, что они много весят. Ну и ресурсы системы едять порядочно. Компромиссом может быть github.com/zserge/webview. Это нативный вебвью.
Но надо отметить, что Visual Studio Code сделан как раз на Electron с TypeScript. И по сравнению с IntelliJ Idea весит гораздо меньше, да и память не так сильно жрёр. Это говорит о том, что Electron может быть лучше растолстевшего JVM.serf
14.08.2018 23:38У меня есть проект на Electron + TypeScript. По началу в WebStorm все бегало (причем эта штука полегче Idea будет). Но с ростом проекта стало местами дико тормазить, в то время как в Visual Studio Code все шустро бегает на этом же проекте. Так что дело не в технологиях, а в техническом дизайне решения и его реализации.
Стереотип медленности Chromium и node.js (Electron + TypeScript) полагаю вызван существующим мнением что большая часть веб разработчиков все еще являются скрипт кидди.
kuznetsovin
13.08.2018 18:26В свое время (года 2 назад) тоже хотел написать декстопное приложение на Go, но биндинги gtk и qt в то время были мягко говоря не «очень» рабочие. И после продолжительных поисков я остановился на биндинге sciter оказалось очень даже ничего, правда с оформлением и внутренними скриптами время я потратил изрядно.
Для себя я сделал вывод что-то что сложенее 2-3 окон (например переключение между ними, диалоги, прогресс бары) на Go сделать довольно-таки сложно.jhekasoft Автор
13.08.2018 20:04+2Всё, что можно сделать с помощью GTK+, можно сделать и на Go. А на GTK+ есть довольно-таки сложные GUI-приложения. Если чего-то нет в биндинге gotk3 для GTK+, то всегда можно добавить. Я делал такое приложение: jhekasoft.github.io/insteadman. Это ланчер для INSTEAD-игр Там ещё окно настроек есть. Возможно не сильно сложное приложение, но я не вижу приград для усложнения.
romychs
13.08.2018 18:46Сейчас GTK имеет более свежую версию, чем поддерживается gotk3 (3.6-3.16), например у меня — 3.18 и так просто, как в статье скомпилировать сам gotk3 и приложение не получится, используем:
для установки:
go install -tags gtk_3_18 github.com/gotk3/gotk3/gtk
для сборки приложения:
go build -v -tags gtk_3_18 -gcflags "-N -l"
Это есть в issue проектаjhekasoft Автор
13.08.2018 18:54В Homebrew версия 3.22.30: formulae.brew.sh/formula/gtk+3. На ArchLinux тоже версия 3.22.30: www.archlinux.org/packages/extra/x86_64/gtk3. Поэтому у меня проблем не было.
Теги в gotk3 сделаны специально для старых версий.
То есть у вас устаревшая версия установлена. Какая у вас операционная система?romychs
13.08.2018 19:07Linux Mint 18.3, как бы не очень и старая она.
jhekasoft Автор
13.08.2018 19:16Тем не менее 3.22 уже как 3 месяца релизнули. Но gotk3 поддерживает версии выше 3.16. У меня свободно компилится.
Там у них для разных версий есть отдельные файлы, в которых только фичи тех версий. Возможно просто что-то напутали в gotk3 с этими версиями и поправят.
stychos
14.08.2018 03:02В therecipe/qt мне не понравилась слишком сложная система сборки приложения, даже специальную команду сделали.
Это не в therecipe/qt сделали, а просто в Qt. Ну а так, по личному небольшому опыту, с Qt много возни иногда возникает. Ну, например, «нативный» вид в macOS весьма кривоват, на текущей бете Mojave тоже всё плохо по умолчанию. А над системой сборки, чтобы настроить и привыкнуть, можно пару-тройку дней плясать.
hzs
14.08.2018 05:44Долгое время сидел на KDE, если нужно быстро что-то написать для себя, был достаточно удобный инструмент QtCreator.
Сейчас перешёл на Mate и не увидел удобного визуального инструмента под GTK.
Есть Anjuta, но толком ни документации, ни примеров.
В итоге написал себе нужную программу на Python, но хочется, конечно, что-то нормально компилируемое.
С GO ни разу не сталкивался, но синтаксис вроде простой и понятный, надо будет пощупать.
Жаль что под линукс нет простого и понятного инструмента, типа Visual Studio.JekaMas
14.08.2018 11:26IntelliJ Goland, берите EAP и сможете попробовать язык в деле и в более удобной IDE, чем альтернативы.
bosom
14.08.2018 07:48Уважаемый автор, просьба дополни свою статья информацией о том что нужно будет иметь на компьютере пользователя чтобы пользоваться подобным приложением.
К примеру я разработчик, я
загадилустановил на свой мак home brew (на самом деле пользуюсь mac port именно чтобы не отчищать ...), поставил GTK и все что нужно для компиляции.
Сделал приложение и хочу его отдать пользователю(ям), пользователей не заставить ставить всякое не нужное им, чтобы такое приложение работало.
Поэтому опиши что нужно запаковать, как куда или ставить на пользовательский комп чтобы приложение работало на другом компьютере, исходя из того что пользовательский компьютер свеж и чист. Так как go кроссплатформенный, то для всех платформ.
Одно из прекрасных преимуществ приложений на go в том что они компилируются в статический бинарник не требующий ничего. В этом случае так же? Или нужно тащить с собой GTK?
Я помню был биндиг под Golang для QT, в том случае приходилось тащить тележкунавозав виде самого QT.
Если нужно тащить GTK, то зачем такое счастье? Например под mac вполне себе то же самое можно написать на swift, который очень похож на язык Golang и получить полноценное приложение без зависимостей. Или написать UI на атом или подобном, а внутрь встроить Golang приложение как внутренний сервер.
В чем преимущества описанного решения?
fsmorygo
14.08.2018 10:47Про GTK не скажу, но Swift, как бы Вам не казалось, вместе с приложением бандлит весь Swift stdlib.
И так будет до тех пор, пока не разберутся с Swift ABI, а это хотели сначала к 3 версии сделать, потом к 4, сейчас к 5.
jhekasoft Автор
14.08.2018 11:25На GNU/Linux библиотеки будут в системе, на macOS — тащаться с собой нужные библиотеки в бандле (как и во всех других приложениях), под Windows — нужные библиотеки идут в папке с программой. Их вес не сильно велик для современных реалий. Уж точно меньше всяких Электронов. Я об этом рассказу в продолжении.
Вот в документации gotk3 про это: github.com/gotk3/gotk3/wiki/Cross-Compiling#deployment.
jhekasoft Автор
14.08.2018 15:34Ну и если написать на Мак под Свифт, то приложение будет только для Мака. Не кроссплатформенное.
Под атомом вы имеете ввиду Electron? Я писал, что с ним тащится Chromium + node.js. Это не очень. Плюс ресурсов жрёт много.
shamanis
14.08.2018 08:10Спасибо за статью!
Вот только недавно искал что взять для GUI в Go. Склонялся к Qt, т.к. с ним я знаком со времен разработки на PyQt, но отталкивала монструозность установки всего этого и дальнейшей компиляция.
0xf0a00
14.08.2018 10:07Файл main.glade как то упаковывается в бинарник? Или при компиляции разворачивается в код который создаст описанные в файле объекты?
При компиляции создается автономный бинарник или для его работы пользователю придется еще что то доставлять?jhekasoft Автор
14.08.2018 11:29main.glade можно при желании упаковать в бинарник. Но он задуман всё-таки как отдельный файл представления, которое можно поменять не перекомпилируя приложение. При компиляции его парсит GtkBuilder и динамически создаёт объекты.
Нужны будут динамические библиотеки GTK+, их общий вес не сильно велик по нынещним меркам: github.com/gotk3/gotk3/wiki/Cross-Compiling#deployment. Я дальше об этом расскажу.
ALexhha
14.08.2018 10:42В общем я думал, что всё будет просто. И что зря про Go говорят, что в нём проблема с GUI. Но как же я ошибался...
из статьи не понятно, причем от слова совсем, в чем же таки заключались проблемы и как их решали?!jhekasoft Автор
14.08.2018 11:30На данном этапе всё кажется простым и не вызывает трудностей. Но трудности у меня появились при кросскомпиляции
Дальше я написал. В общем об этом в продолжении. :)
recompileme
14.08.2018 12:21libui не рассматривали?
recompileme
14.08.2018 12:30Ваш пример на этой либе:
package main import ( "github.com/andlabs/ui" ) func main() { err := ui.Main(func() { input := ui.NewEntry() button := ui.NewButton("Greet") greeting := ui.NewLabel("") box := ui.NewVerticalBox() box.Append(ui.NewLabel("Enter your name:"), false) box.Append(input, false) box.Append(button, false) box.Append(greeting, false) window := ui.NewWindow("Hello", 200, 100, false) window.SetMargined(true) window.SetChild(box) button.OnClicked(func(*ui.Button) { greeting.SetText("Hello, " + input.Text() + "!") }) window.OnClosing(func(*ui.Window) bool { ui.Quit() return true }) window.Show() }) if err != nil { panic(err) } }
отсюда: getting started
jhekasoft Автор
14.08.2018 12:37Рассматривал. Там даже нет списков, treeview. Не серьёзная вещь пока что. Так, демку можно сделать, но полноценное приложение на нём не получится. По крайней мере то, которое я задумывал.
trigun117
14.08.2018 17:43Когда делал для пет-проекта GUI на Go то выбрал вот эту библиотеку github.com/andlabs/ui все довольно таки удобно, разве что с марта месяца не обновлялась. Вот такое получилось
jhekasoft Автор
14.08.2018 17:48Да, выше писали о ней уже. Я о ней знаю. Но там нет таблиц. Мне нужны были таблицы. Посложнее интерфейс в приложении:
berezuev
На маке в первую очередь надо поставить pkg-config и gtk, иначе go get ошибками плюётся.