Приветствую вас, коллеги!
Около месяца назад я опубликовал здесь статью GUI-фреймворки — на поток, где предлагалась технология создания GUI-фреймворков для разных языков программирования, основанная на подключении (tcp/ip или каком другом) к внешнему процессу, играющему роль своеобразного GUI-сервера. Здесь я хочу представить конкретную реализацию этой идеи — новый GUI-фреймворк для Golang — External.
Зачем вообще потребовалось писать новый 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 на Хабре
Автор: alkresin