Pull to refresh

GUI на Golang: GTK+ 3

Reading time7 min
Views84K

Решил я написать одно кроссплатформенное десктопное приложение на 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 нативный диалог. Также в моём проекте нужна была интернационализация. Там тоже есть некоторые особенности. Если вам интересно увидеть это всё сейчас в коде, то можно подсмотреть здесь.


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


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

Only registered users can participate in poll. Log in, please.
Продолжать писать?
96.35% Да607
3.65% Нет23
630 users voted. 83 users abstained.
Tags:
Hubs:
+48
Comments58

Articles