External — GUI для Golang

    Приветствую вас, коллеги!

    Около месяца назад я опубликовал здесь статью GUI-фреймворки — на поток, где предлагалась технология создания GUI-фреймворков для разных языков программирования, основанная на подключении (tcp/ip или каком другом) к внешнему процессу, играющему роль своеобразного GUI-сервера. Здесь я хочу представить конкретную реализацию этой идеи — новый GUI-фреймворк для GolangExternal.

    Зачем вообще потребовалось писать новый GUI для Golang, если в наличии уже имеется немало таких инструментов? В первую очередь, потому, что ни один из них не устраивал меня в полной мере. Нужно было что-то для создания десктопных приложений, кросс-платформенное, чтобы выглядело естественно для каждой платформы. По-возможности, не очень громоздкое, имеющее минимум зависимостей — я привержен минималистическому подходу.

    Я ориентировался вот на этот список. Две позиции — app и walk были сразу вычеркнуты, как не соответствующие требованию кросс-платформенности. После некоторого раздумья отверг и те, что основаны на html/css/javascript. Во-первых, мне кажется несколько непривычным строить десктопное приложение как веб-страницу и, во-вторых, они тянут за собой довольно тяжелые движки. Так, например, go-astilectron и gowd основаны на Electron и nw.js, соответственно, а эти, в свою очередь, на node.js. Представляете, сколько всего должно быть установлено у конечного пользователя, чтобы запустить даже небольшую утилиту? Разве что go-sctiter с этой точки зрения выглядит предпочтительнее: стоящий за ним Sciter не столь монстрообразен.

    Go-gtk и gotk3 основаны на GTK. Это, судя по всему, основательно сделанные пакеты, но я отказался и от них, потому что, на мой взгляд, использовать GTK под Windows — не лучшее решение. GTK-окна не выглядят под Windows «нативными». Qt binding — мощная штука, конечно, но достаточно сложная, и размеры… Когда я прочитал: «You also need 2.5 GB free RAM (which only needed during initial setup) and at least 5 GB free disk space», последние сомнения отпали. Сам Go занимает в десять раз меньше места. А тут еще и лицензионные ограничения: «this binding with its LGPL license is not suitable to be used in closed source application that are intended to be distributed to the general public».

    Что у нас осталось из списка? Ui мог бы быть неплохим вариантом, но он еще на mid-alpha стадии. Fyne тоже выглядит неплохо, но, похоже, не вполне готов. Несколько смутило, что, с одной стороны, «Fyne is built entirely using vector graphics», а, с другой, «EFL windows packages are currently much older so you will not see the vector graphics portions of Fyne applications» — вот так. Ну и не нравится, что для того, чтобы под Windows установить EFL (графическая библиотека, на которой основан Fyne), нужен MSYS.

    Короче, при всем уважении к авторам перечисленных пакетов и к продуктам их труда, для себя я ничего не выбрал и с чистой совестью приступил к тому, что и хотел делать — написать новый GUI фреймворк — External.

    Как я уже писал в предыдущей статье, External не реализует GUI-элементы самостоятельно, он использует для этого отдельное приложение, отдельный процесс, выступающий в роли GUI-сервера, это приложение так и называется — GuiServer. External запускает его, присоединяется к нему по tcp/ip, посылает команды/запросы на создание окон и виджетов, манипулирование ими и пр. и принимает от него сообщения.

    Вот простейшая программа, создающая окно с традиционной надписью Hello, world:

    package main
    
    import egui "github.com/alkresin/external"
    
    func main() {
    
       if egui.Init("") != 0 {
            return
        }
        pWindow := &egui.Widget{X: 100, Y: 100, W: 400, H: 140, Title: "My GUI app"}
        egui.InitMainWindow(pWindow)
    
        pWindow.AddWidget(&egui.Widget{Type: "label",
            X: 20, Y: 60, W: 160, H: 24, Title: "Hello, world!" })
    
        pWindow.Activate()
        egui.Exit()
    }

    Функция Init() запускает GuiServer и присоединяется к нему. Ей может быть передан строковый параметр, определяющий, если надо, название GuiServer'а и путь к нему, ip адрес и порт, уровень журналирования.

    InitMainWindow() создает главное окно приложения с заданными параметрами. Метод AddWidget() — добавляет виджет типа label.

    Activate() — выводит окно на экран и переводит программу в режим ожидания.
    И окна, и виджеты определяются в структуре Widget — я не стал делать отдельные структуры для каждого объекта, т.к. не нашел удобного способа это реализовать, учитывая, что в Go нет наследования. В эту структуру входят поля, общие для большинства виджетов, и map[string]string, где собраны свойства, характерные для конкретного объекта:

    type Widget struct {
          Parent   *Widget
          Type     string
          Name     string
          X        int
          [...]
          Font     *Font
          AProps   map[string]string
          aWidgets []*Widget
    }

    В число методов этой структуры входят уже знакомый нам AddWidget(), а также SetText(), SetImage(), SetParam(), SetColor(), SetFont(), GetText(), Move(), Enable() и др. Особо хотел бы отметить SetCallBackProc() и SetCallBackFunc() — для установки обработчиков событий.
    Перечислять здесь все функции, структуры и методы было бы неуместным, для этого есть, точнее. должна быть, документация. Скажу только о некоторых, чтобы дать какое-то общее представление:

    Menu(), MenuContext(), EndMenu(), AddMenuItem(), AddMenuSeparator() — набор функций для создания меню, главного или контекстного.
    EvalProc(sCode string), EvalFunc(sCode string) передают фрагмент Harbour кода (можно многострочный) для исполнения на GuiServer — своего рода реализация встроенного скриптового языка.
    OpenForm(sPath string) — создает окно на основе описания из xml-файла, созданного HwGui Designer'ом.
    OpenReport(sPath string) — печатает отчет на основе описания из xml-файла, созданного HwGui Designer'ом.
    MsgInfo(), ..., SelectFile(), SelectColor(), SelectFont() — вызов стандартных messagebox'ов и диалогов.
    InitPrinter() и набор методов структуры Printer: Say(), Box(), Line() и др. обеспечивают печать на принтер с возможностью предпросмотра.

    Вот полный список поддерживаемых в настоящее время виджетов:
    label, edit, button, check, radio, radiogr, group, combo, bitmap, line, panel (предназначен для размещения на нем др.виджетов), paneltop, panelbot, ownbtn (ownerdrawn кнопка), splitter, updown, tree, progress, tab, browse (таблица, как его многие называют), cedit (edit с расширенными возможностями), monthcal.

    Все они перечислены в функции init() extwidg.go вместе с дополнительными свойствами. доступными для каждого из них — именно эти свойства устанавливаются через Widget.AProps. У многих из перечисленных виджетов есть и другие свойства, особенно богат на них browse; их можно задать отдельно, используя метод SetParam().

    External получился небольшим по объему, он написан на чистом Go, не тянет за собой других пакетов, кроме нескольких стандартных. Кросс-платформенность обеспечивается GuiServer'ом, который можно скомпилировать под Windows, Linux/Unix, Mac OS. Определенное неудобство связано именно с необходимостью использования этого внешнего модуля, который вам надо или собрать из исходников, или скачать готовый с моего сайта и поместить в каталог, указанный в PATH. Он, кстати, невелик — всего около полутора мегабайт для Windows и около трех — для Linux.

    Как это выглядит, покажу на примере маленького приложения ETutor — Golang tutorial. Эта программа представляет коллекцию фрагментов кода на Go в виде дерева. Код можно редактировать, запускать на выполнение. Ничего особенного, но довольно удобно. Коллекцию можно пополнять, добавлять новые коллекции. Сейчас там собраны (еще не полностью) A Tour of Go, Go by Example и несколько примеров по самому External. ETutor можно использовать и для того, чтобы, например, упорядочить набор каких-либо утилит на Go. Итак, скриншоты.

    Windows 10:



    Debian, Gnome:



    И, наконец, ссылки:

    External на Github
    GuiServer на Github
    ETutor на Github
    Страница о GuiServer у меня на сайте, откуда можно скачать готовые бинарники
    https://groups.google.com/d/forum/guiserver — Группа для обсуждения всех вопросов, связанных с GuiServer и External
    Статья о GuiServer на Хабре
    Поделиться публикацией

    Похожие публикации

    Комментарии 50
      +2
      Написал бы кто под Go среду RAD типа Delphi/Lazarus. Взлетело бы даже в разумно платном виде. Закладываться сейчас на паскаль выглядит как-то не очень перспективно. Продукты Эмбаркадеро достаточно дорогостоящие.
        –1

        Сами же привели в пример Lazarus. Хотя у Embarcadero есть бесплатная редакция для любителей.
        Но вопрос о кросплаторменном GUI фреймворке с приятным внешним видом и для приятного языка будет открытым ещё очень долго.

          –1
          pyqt?
          0
          вы имеете в виду мышкой кнопочки расставлять?
          0
          С Go привык, что скачиваю один бинарник под свою OS, а тут нужен отдельный gui сервер, который имеет дополнительные библиотеки.
            0
            Какие дополнительные библиотеки?
              0
              Gtk. Что собственно рождает проблему, т.к. на разных дистрибутивах разная версия Gtk, glib и libc.
                0
                Это для Линукс. В Windows GTK я не использую. Проблема, конечно, с зоопарком дистрибутивов есть, но многие в этом плане все же совместимы. Поэтому я и выкладываю готовые бинарники для двух дистрибутивов — Debian 8 32-bit и Ubuntu 18.04 64-bit. Могу и для Федор.
            +1

            Т.е. Вы заменили compile-time зависимость от C-шной GUI-библиотеки на run-time зависимость от внешнего бинарника? Зачем? Если для фана, то это здорово, а если ради того, чтобы сохранить возможность устанавливать Go-шное GUI-приложение через go get — это перебор.

              +1
              Возможность устанавливать приложение через go get — всего лишь бонус. А внешний бинарник я использую, поскольку это оказалось гораздо быстрее и проще, чем писать биндинги для С-шной библиотеки, которую, кстати, тоже еще надо написать.
                0

                По факту и биндинги и библиотеку Вы в любом случае написали, отличается только форма — библиотека в виде бинарника, биндинги в виде сетевого протокола. Не уверен, что работы при таком подходе было объективно меньше.

                  0
                  Библиотека написана не на чистом С и не для С, а для Harbour и, частично, на Harbour. Привязать это дело к Go, как библиотеку? Можно, наверное, но будут заморочки. Даже не думал на эту тему.
                    0
                    Шаг влево, вправо и нужно идти учить Harbour, что бы добавить свой контрол?
                      0
                      Для того, чтобы добавить свой контрол и чтоб он был кросс-платформенным, надо еще учить C, Winapi, GTK, Cairo. Harbour здесь — совсем не самая большая проблема.
              0
              О, спасибо.

              Не так давно искал на чем сделать простой GUI индикатор в таск-панель на Linux… ничего универсального не нашел пришлось выпиливать GTK и QT варианты.

              Но как я понял через External индикатор не сделать?.. или я не прав?
                0
                Честно говоря, я не знаю, как делать что-то в таск-панель для Линукс. Это ведь зависит от конкретной графической оболочки?
                  0
                  Ну в GTK и GT есть апликейшн индикаторы — вот через них и рисуется иконка + меню в панель.
                0
                Очень классная инициатива, с GUI в го и правда беда.
                Меня немножко смутил скриншот с Win10. Почему везде шрифт с засечками, даже на панели? О_о
                  0
                  Ну нравится мне Georgia). Можно другой поставить, в ETutor это делается в ini-файле.
                    0
                    Определенное неудобство связано именно с необходимостью использования этого внешнего модуля, который вам надо или собрать из исходников

                    Определенное неудобство? Это не просто неудобство, а велосипедостроение, причем с квадратными колесами.


                    Статическую линковку (всего, да, вместе с сервером [ edited ]) в один бинарник сделать будет проблематично. Все привыкли, когда приложение на Go — один бинарник, в который уже положены все зависимости.


                    Зачем нужен Go, когда фактически GUI рисуется совсем не им?

                      0
                      > Зачем нужен Go, когда фактически GUI рисуется совсем не им?

                      Для тех задач, для которых Go предназначен в первую очередь, для тех, которые он решает лучше других инструментов.
                        +1
                        для тех, которые он решает лучше других инструментов.

                        Микросервисы и CLI?

                      0

                      Почему-то мне кажется, что биндинги к ненативному Qt будут отрабатывать быстрее, чем вызовы к GUI-серверу, который отрисовывает всё "нативно" через WinAPI / GTK+.


                      ( GTK+ 2 в 2018, серьезно?)

                        0
                        > биндинги к ненативному Qt будут отрабатывать быстрее

                        Безусловно. Но в большинстве случаев вы этого не заметите.
                          0

                          Тогда для чего же та самая "нативность" в отображении, если не ради скорости?

                            0
                            Единообразие внешнего вида. Что бы приложение внешне не отличалось от других приложений в системе, а изменения текущей темы так же применялось и на твоё приложение.
                              0
                              В первую очередь — для того, чтобы приложение выглядело естественно для той среды, в которой оно выполняется.
                          0
                          Ну когда же появится нативный GUI пакет для Go, где не надо будет ничего отдельно компилировать или скачивать… просто «go get» и полетели.
                            0
                            Есть вот такой LXN WALK
                            Но он для винды. И как-то туговато шло когда надо что-то за пределами имеющихся примеров использования.
                            0
                            Зачем усложнять? Делать отельный GUI сервер.
                            Чем не устроил например github.com/andlabs/ui?
                              0
                              На деле это привело, наоборот, к упрощению самого пакета. Кроме того, как я писал в предыдущей статье, тот же самый GuiServer может быть основой GUI фреймворков и для других языков. Не забываем и про возможность удаленного выполнения — когда GuiServer выполняется на другом компьютере в сети.
                              Что касается ui — я ведь писал об этом, он находится по оценке авторов еще в статусе mid-alpha.
                                0
                                Что касается ui — я ведь писал об этом, он находится по оценке авторов еще в статусе mid-alpha.

                                А ваш пакет уже готов к использованию в продакшен?

                                  0
                                  HwGui, на которой основан GuiServer, уже давно в production. У External возраст чуть больше месяца, о production можно будет говорить, когда на нем напишется что-нибудь серьезное.
                                    +1

                                    То есть вас не устаивает ui, потому что он mid-alpha, но вы пишете своё, которое в состоянии pre-alpha и вас это не смущает?

                                      0
                                      Дело в том, что в состоянии mid-alpha находится и libui, на которой основывается ui, там не хватает многих важных вещей, как, например, tree, printing, clipboard support — и это только то, что вспомнили авторы.
                                      В External, хотя ему, напомню опять, чуть больше месяца, это и много чего еще есть — потому что давно есть в HwGui.
                                        0

                                        В Qt это тоже есть, так-то, и, возможно, даже "давнее", чем в HwGui.

                                          0
                                          Не сомневаюсь, но вы ведь не о qt писали, а об ui.
                                          А к qt у меня другие претензии, и они тоже изложены в тексте.
                                            0

                                            И только к HwGUI нет претензий. Не вы ли его написали?

                                              0
                                              Я. Потому и выбрал, что все возможные претензии могу решить сам.
                                                0

                                                Теперь все втсало на свои места, спасибо!

                              0

                              А что с MacOS? Hello word не работаает на macOS Mojave (10.14.1)

                                0
                                Теоретически GuiServer можно собрать под MacOS — Harbour под ним собирается, GTK там есть.
                                Но у меня нет под рукой ни одного компьютера с MacOS, поэтому я не могу собрать его сам.
                                В принципе, если очень надо, могу попросить об этом кого-нибудь из Harbour-сообщества.
                                0

                                Hello word не работаает на macOS Mojave (10.14.1)

                                  0
                                  У вас есть GuiServer под MacOS?
                                    0

                                    Билд проходит успешно, екзешник запускается без ошибок. Вот только окно не открывается...

                                      0
                                      Екзешник — в смысле guiserver? Появился egui.log?
                                      Можно еще задать журналирование для GuiServer'а в вызове Init:
                                      egui.Init(«log=2») — тогда должны появиться guiserver.log и ac.log.
                                        0

                                        да:


                                        go build hello_world.go
                                        ./hello_world
                                        echo $?
                                        0
                                        
                                        cat egui.log
                                        guiserver 127.0.0.1 3101
                                        dial tcp4 127.0.0.1:3101: connect: connection refused
                                          0
                                          Эти строчки говорят о том, что программа осуществила попытку запустить guiserver, но не смогла присоединиться к нему — скорее всего, из-за того, что он не был запущен.
                                          Где у вас находится guiserver? В каталоге, который прописан в PATH (или где-то вроде этого, я не знаю, как это в macOS)?
                                          Или в том же каталоге, где и программа? Если так, то не надо ли в macOS запускать исполняемые файлы из текущего каталога, как и в Линуксе, с указанием "./"? В этом случае надо в вызове egui.Init так и прописать:
                                          egui.Init(«guiserver=./guiserver»)
                                  0
                                  == Я ориентировался вот на этот список.

                                  Этот список устарел. Например, в нём нет вполне себе рабочего биндинга к imgui, которую Вы так же не упомянули в своём обзоре.
                                  Соглашусь с коллегами, использование IPC связки с сервером GUI в рантайме вместо привычных и более быстрых механизмов FFI для задачи создания GUI выглядит несколько странно
                                    0
                                    > Этот список устарел.
                                    Может быть, другого у меня нет. Imgui не упомянул, потому что не слышал о ней.

                                    > использование IPC связки с сервером GUI в рантайме вместо привычных и более быстрых механизмов FFI для задачи создания GUI выглядит несколько странно

                                    Да, это необычно. Но такой подход имеет и свои преимущества, об этом я писал в обеих статьях и комментариях к ним. А скорость, об этом я тоже писал, не самый критичный фактор для интерфейса. Мы же не создаем виджеты в цикле с 10000… итераций.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое