Универсальный GUI ~= конец страданиям

Для меня идеальный GUI это app, который не требует затрат на программирование, дизайн, обслуживание и способный одинаково работать с любыми языками, и на любой платформе без всяких подстроек. Возможно ли это при нашей жизни мы попробуем разобраться.

Несложно освоить по отдельности что-то из Vue/React, JavaFX, Python PyQt, …, но получать данные и взаимодействовать с зоопарком софта простым и элегантным способом, не думая о user-OS/броузерах/платформах — нерешаемая задача для подобных инструментов. Я не хочу лезть в новый фреймворк (даже в старый, забытый), меняя язык программирования, выгребая грабли и забивая голову мусором. Хочу программить именно мою задачу, не отвлекаясь на борьбу со всякими GUI фреймворками. И для себя нашел решение.

В качестве протокола обмена будем использовать Json как топовый формат по условиям популярность/понятность/читаемость/поддержка у всех программных языков в той или иной мере.

Сервер отдает Json данные, по которым наш app GUI должен задизайнить картинку, удовлетворяющую спекам красивого GUI. Google со своим Material Design на сегодня является стандартом, поэтому берем его.

Требования к современному GUI включают наличие стандартных элементов, таких как кнопки, поля ввода, таблицы и т. Д. Прикинем, как используя минимальный набор соглашений сказать GUI, что нам нужны некие элементы на экране. Приведем основные:

  • Кнопка без состояния {«name»: «Push me»}. Если элемент содержит только имя то он кнопка.
  • Поле ввода {«name»: «Edit me», «value»:»} потому что тип value — строка.
  • Кнопка свитчер {«name»: «My state», «value»: false} потому что тип false — boolean.
  • Выбор из списка {«name»: «Switch something», value: «choice1», options=[«choice1», «choice2», «choice3»]}
  • Картинка {«name»: «Image, «url'»:»…», «width»: …, «height»: … }
  • Таблица {«name»: «My table», 'headers'=[«Name», «Synonym»], rows = [
    [«young», «youthful»],
    [«small»,» meager»],
    …]
    }

Для кастомизируемости можно задать тип элемента {type: «ButtonSwitcher»}, если автоматически подобранный по JSON тип не устраивает. Такое возможно, когда на одинаковый JSON может быть отображен более чем одним способом. Например «Выбор из списка» может быть представлен как поле ввода с выпадающим списком, а может и как горизонтальный набор кнопок, одна из которых активна и соответствует текущему value.

При малом количестве опций имеет смысл автоматом использовать кнопочный вариант, при большом (более 3) — поле ввода-выбора. Наш GUI сам выбирает оптимальным для отрисовки способом, но если сильно нужно — type: … в помощь. В норме тип не нужен и автодизайнер должен справляться сам.

Дополним картину по мелочам:

  • если имя не должно отображаться на экране, оно должно начинаться с _;
  • если нужно где-то подрисовать иконку, добавляем «icon»: «любое имя стандартной Material Design иконки»; push кнопка-иконка соответственно будет описана как {«name»:»_Check», «icon»: «check»}
  • сложные элементы управления такие Таблицы, Viewers, могут иметь дополнительные параметры, как то «colors», «params», … которые могут использоваться для специфической отрисовки или выбора доступных опций редактирования/поиска/выделений для таблицы. По умолчанию — максимальный набор опций, стандартные цвета по текущей теме.


Для логической группировки элементов введем понятие блока, который группирует логически связанные элементы в один визуальный блок.
{'name': «Block 1», «elems»: [ {«name»:»_Check», «icon»: «check»}, … ]}
В пределах блока все элементы должны иметь уникальные имена, ибо они же id.

В норме блоки строятся горизонтально, если они не могут все влезть на экран (запустили app GUI на мобиле например), автодизайнер скрывает их, но добавляет в тулбар иконки, позволяющие открыть их тапом. Это дает опцию работы со сложным GUI даже на маленьких экранах.

Верхний уровень описания — Screen. Имеет вид {«name»: «Screan», blocks: […], menu: [{«name»: «Screen», «icon»: …, }, …], «toolbar»: [набор JSON — элементов (кнопки, поля, что угодно)]}

Добавим деталей реализации для условного uniGUI, поддерживающего наш JSON-протокол. Он — отдельный процесс, связывающийся с сервером данных по Websocket и обеспечивающий отображение его данных с последующим информированием обо всех важных для сервера обновлений этих данных.

При коннекте к серверу uniGUI ожидает получить Screen. Получив его он дизайнит и рисует полученную инфу оптимальным для текущего экрана у юзера образом, дальше ждет реакции от юзера и сервера. От построенного изображения данных сервер получает поток сообщений JSON, которые полностью описывают что сделал юзер. Имеют вид [«Block», «Elem», «type of action», «value»], где «Block» и «Elem» — имена блока и элемента, value — значение события.

Сервер может либо принять изменение либо откатить их, послав инфо окно о несоответствии. Может поднять диалоговое окно, которое описывается как блок, и имеет доп. параметр «buttons», который описывает кнопки диалога. Клиент мгновенно отображает актуальные данные сервера и их изменения. Пересылаются только измененные сервером объекты.Получать события и обеспечить их обработку сделаем Websocket прослойку (фреймворк), которая будет автоматом транслировать сообщения в вызовы обработчиков, которые привязаны к нашим данным (объектам).

Вся магия на сервере сведется к тому, что наши отображаемые данные должны быть сцеплены с прослойкой таким образом, чтобы без всякого кодинга обеспечить их автоматическую трансляцию в JSON и вызов обратных уведомлений от активности юзера. Так это зависит от возможностей конкретного языка, то под каждый язык прослойка может иметь разные варианты как архитектурно, так и конкретно.

Например, в одном случае прослойка имеет папку скринов, каждый из модулей в котором имеет описание одного экрана на Python. При старте прослойка считывает экраны, отдает юзеру тот, у которого глобальная priority = 0. Все данные автоматом транслируются с помощью jsonpickle. Сложные элементы обладают собственной «интеллектуальностью», снимая с программиста заботу о деталях. Например таблица получает набор rows, в котором по умолчанию могут отсутствовать id row, когда кол-во данных в row == кол-ву элементов в headers. В этом случае при выборе пользователем строки или редактировании ее содержимого сервер пошлет в качестве id ee индекс в row. Если кол-во данных в row на один больше чем в headers, последний элемент в rows интерпретируется как id и именно он шлется на сервер. Подобная автоматизация сильно упрощает жизнь, где не нужна какая-то специфика, но если вдруг нужна, она доступна наименее трудозатратным образом.

Задача обеспечить автоматическую трансляцию в JSON, следуя формату, занимающему одну-две страницы, вполне решаема на любом языке (надеюсь, что так).

Чтобы не обсуждать применимость этого подхода для сложных app-ов, ниже привожу скрины такого как описано uniGUI, написанного на flutter. Выбор упал на него за многоплатформенность и отсутствие дополнительных слоев типа JS/chrome. В минус flutter можно записать отвратную поддержку десктопа и низкое качество кода верхнего слоя (GUI элементы), неприятную архитектуру для точечных обновлений и управления элементами как данными, что, впрочем, лечится.

4u9h2zlhr0bzbvhbugz5obt8y6a.png

Поток сообщений app GUI → сервер примерно выглядит так:
flutter: [Glossary, Terms, =, 658]
flutter: [_Details, Links, @, @Folliculitis]
flutter: [_Details, Links, @, @Adolescent]
flutter: [toolbar, _Back, =, _Back]
flutter: [toolbar, _Forward, =, _Forward]
flutter: [toolbar, _Back, =, _Back]
flutter: [_Details, _Status, =, Virtual]
flutter: [_Details, _Status, =, Stable]
flutter: [_Details, Links, @, Inflammation]
flutter: [_Details, _Status, =, Virtual]
flutter: [_Details, _Status, =, Stable]
flutter: [toolbar, _Back, =, _Back]
flutter: [_Details, Links, @, @Folliculitis]

А не будет ли такой GUI тормозить при ожидании событий с сервера. Нет, потому что GUI работает в режиме уведомлений сервера, реагируя на все действия юзера как нормальный локальный GUI. По умолчанию он считает молчание сервера на действия юзера как Ok. При смене экранов понятно, что должно прийти его описание. Тут возможна задержка по сравнению c типичным app c embedded GUI.

Итог. Возможно уже сегодня, на текущих инструментах сделать GUI убирающий 90% стандартного муторного кода обслуживания и не заморачиваться где и как он будет работать. По крайней мере, для бизнес/сайенс приложений. Эта статья — Proof of concept того, что создание условного App Browser не только возможно, но и необходимо.

© Habrahabr.ru