Решил я написать одно кроссплатформенное десктопное приложение на Go. Сделал CLI-версию, всё работает отлично. Да ещё и кросскомпиляция в Go поддерживается. Всё в общем отлично. Но понадобилась также и GUI-версия. И тут началось...


Golang gotk3


Выбор библиотеки (биндинга) для GUI


Приложение должно было быть кроссплатформенным.
Поэтому должно компилироваться под Windows, GNU/Linux и macOS.
Выбор пал на такие библиотеки:



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 для отображения: WindowsMSHTML, GNU/Linuxgtk-webkit2, macOSCocoa/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

После запуска получим окно такого вида:


Простой пример на Golang gotk3


Поздравляю! У вас получилось простое приложение из README gotk3!


Больше примеров можно найти на Github gotk3. Их разбирать я не буду. Давайте лучше займёмся тем, чего нет в примерах!


Glade


Есть такая вещь для Gtk+ 3Glade. Это конструктор графических интерфейсов для GTK+. Выглядит примерно так:


Glade


Чтобы вручную не создавать каждый элемент и не помещать его потом где-то в окне с помощью программного кода, можно весь дизайн накидать в 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


В общем надизайнил я примерно такое окно:


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

И получаем:


Golang Glade gotk3


Ура! Теперь мы представление формы держим 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 мы увидем этот текст в метке:


Golang Glade gotk3 signal


Теперь у нас интерактивное приложение!


Заключение


На данном этапе всё кажется простым и не вызывает трудностей. Но трудности у меня появились при кросскомпиляции (ведь gotk3 компилируется с CGO), интеграции с операционными системами и с диалогом выбора файла. Я даже добавил в проект gotk нативный диалог. Также в моём проекте нужна была интернационализация. Там тоже есть некоторые особенности. Если вам интересно увидеть это всё сейчас в коде, то можно подсмотреть здесь.


Исходые коды примеров из статьи находятся здесь.


А если хотите почитать продолжение, то можете проголосовать. И в случае, если окажется это кому-нибудь интересным, я продолжу писать.

Комментарии (39)


  1. berezuev
    13.08.2018 13:58

    На маке в первую очередь надо поставить pkg-config и gtk, иначе go get ошибками плюётся.

    brew install pkg-config gtk-mac-integration gtk+3


  1. Bookvarenko
    13.08.2018 13:59

    Автор, что дальше-то было? Не томи!


    1. jhekasoft Автор
      13.08.2018 14:01

      Судя по голосованию придётся писать дальше. :)


      1. c0f04
        13.08.2018 18:49
        +1

        Если сталкивались, особенно интересует подключение собственных виджетов из сишных библиотек и работа с TreeView (как с ListStore, так и с TreeStore), в частности с итераторами. А в идеале и создание собственных виджетов на Golang.

        Хочется перевести проект с Си на Golang, но пока непонятно будет ли оно стоить. А дома времени на эксперименты почти нет.


        1. jhekasoft Автор
          13.08.2018 18:56
          +1

          С TreeView и ListStore работал, с итераторами. Думаю расскажу. Собственные виджеты не делал. Это был мой первый опыт с GTK+. :)


      1. serf
        14.08.2018 12:04

        Я угадаю, дальше произошел перезед на electronjs.org


        1. jhekasoft Автор
          14.08.2018 12:11

          Нет. Я написал приложение на nw.js (https://github.com/jhekasoft/insteadman2), понял, что такие технологии, в которых нужня тянуть Chromium и node.js меня не устраивают вообще. И поэтому переписал приложение на Go + gotk3 (https://github.com/jhekasoft/insteadman3).


          1. serf
            14.08.2018 15:27

            А что не устроило в Chromium и node.js? Пробовали TypeScript вместо JavaScript? На JavaScript я бы тоже стал писать что-то сложнее одноразовых простых скриптов.


            1. serf
              14.08.2018 15:54

              На JavaScript я бы тоже НЕ стал писать что-то сложнее одноразовых простых скриптов.


            1. jhekasoft Автор
              14.08.2018 17:42

              Не устроило то, что они много весят. Ну и ресурсы системы едять порядочно. Компромиссом может быть github.com/zserge/webview. Это нативный вебвью.

              Но надо отметить, что Visual Studio Code сделан как раз на Electron с TypeScript. И по сравнению с IntelliJ Idea весит гораздо меньше, да и память не так сильно жрёр. Это говорит о том, что Electron может быть лучше растолстевшего JVM.


              1. serf
                14.08.2018 23:38

                У меня есть проект на Electron + TypeScript. По началу в WebStorm все бегало (причем эта штука полегче Idea будет). Но с ростом проекта стало местами дико тормазить, в то время как в Visual Studio Code все шустро бегает на этом же проекте. Так что дело не в технологиях, а в техническом дизайне решения и его реализации.

                Стереотип медленности Chromium и node.js (Electron + TypeScript) полагаю вызван существующим мнением что большая часть веб разработчиков все еще являются скрипт кидди.


  1. kuznetsovin
    13.08.2018 18:26

    В свое время (года 2 назад) тоже хотел написать декстопное приложение на Go, но биндинги gtk и qt в то время были мягко говоря не «очень» рабочие. И после продолжительных поисков я остановился на биндинге sciter оказалось очень даже ничего, правда с оформлением и внутренними скриптами время я потратил изрядно.
    Для себя я сделал вывод что-то что сложенее 2-3 окон (например переключение между ними, диалоги, прогресс бары) на Go сделать довольно-таки сложно.


    1. jhekasoft Автор
      13.08.2018 20:04
      +2

      Всё, что можно сделать с помощью GTK+, можно сделать и на Go. А на GTK+ есть довольно-таки сложные GUI-приложения. Если чего-то нет в биндинге gotk3 для GTK+, то всегда можно добавить. Я делал такое приложение: jhekasoft.github.io/insteadman. Это ланчер для INSTEAD-игр Там ещё окно настроек есть. Возможно не сильно сложное приложение, но я не вижу приград для усложнения.


  1. 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 проекта


    1. 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 сделаны специально для старых версий.

      То есть у вас устаревшая версия установлена. Какая у вас операционная система?


      1. romychs
        13.08.2018 19:07

        Linux Mint 18.3, как бы не очень и старая она.


        1. jhekasoft Автор
          13.08.2018 19:16

          Тем не менее 3.22 уже как 3 месяца релизнули. Но gotk3 поддерживает версии выше 3.16. У меня свободно компилится.

          Там у них для разных версий есть отдельные файлы, в которых только фичи тех версий. Возможно просто что-то напутали в gotk3 с этими версиями и поправят.


  1. stychos
    14.08.2018 03:02

    В therecipe/qt мне не понравилась слишком сложная система сборки приложения, даже специальную команду сделали.

    Это не в therecipe/qt сделали, а просто в Qt. Ну а так, по личному небольшому опыту, с Qt много возни иногда возникает. Ну, например, «нативный» вид в macOS весьма кривоват, на текущей бете Mojave тоже всё плохо по умолчанию. А над системой сборки, чтобы настроить и привыкнуть, можно пару-тройку дней плясать.


  1. hzs
    14.08.2018 05:44

    Долгое время сидел на KDE, если нужно быстро что-то написать для себя, был достаточно удобный инструмент QtCreator.
    Сейчас перешёл на Mate и не увидел удобного визуального инструмента под GTK.
    Есть Anjuta, но толком ни документации, ни примеров.
    В итоге написал себе нужную программу на Python, но хочется, конечно, что-то нормально компилируемое.
    С GO ни разу не сталкивался, но синтаксис вроде простой и понятный, надо будет пощупать.
    Жаль что под линукс нет простого и понятного инструмента, типа Visual Studio.


    1. jhekasoft Автор
      14.08.2018 11:21

      Есть Visual Studio Code, Есть IntelliJ Idea.


      1. hzs
        14.08.2018 12:53

        Visual Studio Code это же, вроде как просто редактор кода.
        Мне-то хочется с визуальным конструктором UI и всё такое.


    1. JekaMas
      14.08.2018 11:26

      IntelliJ Goland, берите EAP и сможете попробовать язык в деле и в более удобной IDE, чем альтернативы.


    1. romychs
      14.08.2018 12:53

      Ну и Lite IDE не плох, с IntelliJ Goland не сравнить, конечно. Но достаточно быстр и удобен.


  1. bosom
    14.08.2018 07:48

    Уважаемый автор, просьба дополни свою статья информацией о том что нужно будет иметь на компьютере пользователя чтобы пользоваться подобным приложением.


    К примеру я разработчик, я загадил установил на свой мак home brew (на самом деле пользуюсь mac port именно чтобы не отчищать ...), поставил GTK и все что нужно для компиляции.
    Сделал приложение и хочу его отдать пользователю(ям), пользователей не заставить ставить всякое не нужное им, чтобы такое приложение работало.
    Поэтому опиши что нужно запаковать, как куда или ставить на пользовательский комп чтобы приложение работало на другом компьютере, исходя из того что пользовательский компьютер свеж и чист. Так как go кроссплатформенный, то для всех платформ.


    Одно из прекрасных преимуществ приложений на go в том что они компилируются в статический бинарник не требующий ничего. В этом случае так же? Или нужно тащить с собой GTK?
    Я помню был биндиг под Golang для QT, в том случае приходилось тащить тележку навоза в виде самого QT.


    Если нужно тащить GTK, то зачем такое счастье? Например под mac вполне себе то же самое можно написать на swift, который очень похож на язык Golang и получить полноценное приложение без зависимостей. Или написать UI на атом или подобном, а внутрь встроить Golang приложение как внутренний сервер.


    В чем преимущества описанного решения?


    1. fsmorygo
      14.08.2018 10:47

      Про GTK не скажу, но Swift, как бы Вам не казалось, вместе с приложением бандлит весь Swift stdlib.


      И так будет до тех пор, пока не разберутся с Swift ABI, а это хотели сначала к 3 версии сделать, потом к 4, сейчас к 5.


    1. jhekasoft Автор
      14.08.2018 11:25

      На GNU/Linux библиотеки будут в системе, на macOS — тащаться с собой нужные библиотеки в бандле (как и во всех других приложениях), под Windows — нужные библиотеки идут в папке с программой. Их вес не сильно велик для современных реалий. Уж точно меньше всяких Электронов. Я об этом рассказу в продолжении.

      Вот в документации gotk3 про это: github.com/gotk3/gotk3/wiki/Cross-Compiling#deployment.


    1. jhekasoft Автор
      14.08.2018 15:34

      Ну и если написать на Мак под Свифт, то приложение будет только для Мака. Не кроссплатформенное.

      Под атомом вы имеете ввиду Electron? Я писал, что с ним тащится Chromium + node.js. Это не очень. Плюс ресурсов жрёт много.


  1. shamanis
    14.08.2018 08:10

    Спасибо за статью!
    Вот только недавно искал что взять для GUI в Go. Склонялся к Qt, т.к. с ним я знаком со времен разработки на PyQt, но отталкивала монструозность установки всего этого и дальнейшей компиляция.


    1. jhekasoft Автор
      14.08.2018 11:26

      И вам спасибо! Я дальше расскажу, что плохого в GTK+.


  1. 0xf0a00
    14.08.2018 10:07

    Файл main.glade как то упаковывается в бинарник? Или при компиляции разворачивается в код который создаст описанные в файле объекты?
    При компиляции создается автономный бинарник или для его работы пользователю придется еще что то доставлять?


    1. jhekasoft Автор
      14.08.2018 11:29

      main.glade можно при желании упаковать в бинарник. Но он задуман всё-таки как отдельный файл представления, которое можно поменять не перекомпилируя приложение. При компиляции его парсит GtkBuilder и динамически создаёт объекты.

      Нужны будут динамические библиотеки GTK+, их общий вес не сильно велик по нынещним меркам: github.com/gotk3/gotk3/wiki/Cross-Compiling#deployment. Я дальше об этом расскажу.


  1. ALexhha
    14.08.2018 10:42

    В общем я думал, что всё будет просто. И что зря про Go говорят, что в нём проблема с GUI. Но как же я ошибался...
    из статьи не понятно, причем от слова совсем, в чем же таки заключались проблемы и как их решали?!


    1. jhekasoft Автор
      14.08.2018 11:30

      На данном этапе всё кажется простым и не вызывает трудностей. Но трудности у меня появились при кросскомпиляции


      Дальше я написал. В общем об этом в продолжении. :)


  1. recompileme
    14.08.2018 12:21

    libui не рассматривали?


    1. recompileme
      14.08.2018 12:30

      Ваш пример на этой либе:

      image

      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


    1. jhekasoft Автор
      14.08.2018 12:37

      Рассматривал. Там даже нет списков, treeview. Не серьёзная вещь пока что. Так, демку можно сделать, но полноценное приложение на нём не получится. По крайней мере то, которое я задумывал.


      1. recompileme
        14.08.2018 12:57

        да, забыл, сори, когда смотрел пару лет назад таблицы были тоже зародыше


  1. trigun117
    14.08.2018 17:43

    Когда делал для пет-проекта GUI на Go то выбрал вот эту библиотеку github.com/andlabs/ui все довольно таки удобно, разве что с марта месяца не обновлялась. Вот такое получилось

    image

    image


    1. jhekasoft Автор
      14.08.2018 17:48

      Да, выше писали о ней уже. Я о ней знаю. Но там нет таблиц. Мне нужны были таблицы. Посложнее интерфейс в приложении:
      image