Делаем свой десктопный GUI к Apache Kafka или Conduktor для обездоленных

от автора

— Я духов вызывать могу из бездны!
— И я могу, и всякий это может. Вопрос лишь, явятся ль они на зов.

Шекспир, Генрих IV

Как-то так сложилось, что у нас не так много UI для Apache Kafka. А если хочется именно desktop, то Offset Explorer и упомянутый Conduktor. Первый имеет морально устаревший интерфейс 2000х, а второй не оправдано дорогой, т. к. не использую весь его богатый функционал. Вооружившись Qt и librdkafka, набросал conduktor на минималках.

Используй layout Люк

Сложный выпадающий элемент, имеющий множество состояний. Как съесть слона? По кусочкам маленького размера. Лайауты умеют отслеживать изменение видимости компонентов, и высчитывать размеры на основе implicit size объекта. Более мелки компоненты строятся следующим образом

Item {     id: item      implicitHeight: layout.implicitHeight     implicitWidth: layout.implicitWidth     property int selectedLimit: 0      ColumnLayout {         id: layout                  SpinBox {             visible: item.selectedLimit == 1         }          TextField {             visible: item.selectedLimit == 2         }          SpinBox {             visible: item.selectedLimit == 3         }     } }

Сворачивающая левая панель построена на манипуляции с размерами

states: [     State {         name: "default"          PropertyChanges {             target: collapsBtnLabel             text: qsTr("« Collapse")         }          PropertyChanges {             target: menu             width: 300             implicitWidth: 300         }          PropertyChanges {             target: header             state: "default"         }          PropertyChanges {             target: kafka_icon             source: "qrc:/kafka_icon.svg"         }      },     State {         name: "small"          PropertyChanges {             target: collapsBtnLabel             text: "»"         }          PropertyChanges {             target: menu             width: 60             implicitWidth: 60         }          PropertyChanges {             target: header             state: "small"         }          PropertyChanges {             target: kafka_icon             source: menu.broker.color !== Style.BrokerColor[0] ? "qrc:/kafka_icon_reverse.svg" : "qrc:/kafka_icon.svg"         }      } ]

И для сравнения как сделано в Qt Quick Controls

T.CheckDelegate {     id: control      implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,                             implicitContentWidth + leftPadding + rightPadding)     implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,                              implicitContentHeight + topPadding + bottomPadding,                              implicitIndicatorHeight + topPadding + bottomPadding)      padding: 6     spacing: 6      icon.width: 16     icon.height: 16      contentItem: IconLabel {         leftPadding: control.mirrored ? control.indicator.width + control.spacing : 0         rightPadding: !control.mirrored ? control.indicator.width + control.spacing : 0          spacing: control.spacing         mirrored: control.mirrored         display: control.display         alignment: control.display === IconLabel.IconOnly || control.display === IconLabel.TextUnderIcon ? Qt.AlignCenter : Qt.AlignLeft          icon: control.icon         text: control.text         font: control.font         color: control.highlighted ? Fusion.highlightedText(control.palette) : control.palette.text     }      indicator: CheckIndicator {         x: control.mirrored ? control.leftPadding : control.width - width - control.rightPadding         y: control.topPadding + (control.availableHeight - height) / 2          control: control     }      background: Rectangle {         implicitWidth: 100         implicitHeight: 20         color: control.down ? Fusion.buttonColor(control.palette, false, true, true)                             : control.highlighted ? Fusion.highlight(control.palette) : control.palette.base     } } 

Twist таблицы и списка

Твист заключается в переключении между двумя режимами отображения

  • список

  • таблица

Достигается это наложением таблицы со списком, с последующей синхронизации полосы прокрутки. Делается через StackLayout,фиксации высоты делегата и двух ScrollBar

Rectangle {     property int rowHeight: 40     StackLayout {         anchors.fill: parent                  ListView {             Layout.fillWidth: true             Layout.fillHeight: true              ScrollBar.vertical: ScrollBar {                 id: listVerticalBar                  policy: ScrollBar.AsNeeded                 minimumSize: 0.06                 onPositionChanged: tableVerticalBar.position = position             }                          delegate: Rectangle {                 implicitHeight: rowHeight             }         }                  TableView {             Layout.fillWidth: true             Layout.fillHeight: true                               ScrollBar.vertical: ScrollBar {                 id: tableVerticalBar                                  policy: ScrollBar.AsNeeded                 minimumSize: 0.06                 onPositionChanged: listVerticalBar.position = position             }                              delegate: Item {                 implicitHeight: rowHeight             }         }     } }

Делегат для таблицы выглядит следующим образом

Item {     implicitWidth: 100     implicitHeight: rowHeight      StackLayout {         anchors.fill: parent         currentIndex: column                  Text {             // topic         }                  Text {             // part         }         //...     } }

Нехитрая схема, которая позволяет настраивать вид каждой колонки. Если не боитесь дополнительных зависимостей, то можете взять DelegateChooser и DelegateChoice.

Ещё раз о таблицах

Изменение количества колонок в таблице и их размер, это привычный функционал и что-то само собой разумеется. Что может пойти не так? TableView позволяет задавать функцию columnWidthProvider, которая позволяет устанавливать ширину столбца. Начиная с Qt 5.13 если вернуть 0, колонка будет скрыта.

Что бы применить изменения, дергаем forceLayout, как это сделано в документации

TableView {     id: tableView     property var columnWidths: [100, 50, 80, 150]     columnWidthProvider: function (column) { return columnWidths[column] }          Timer {         running: true         interval: 2000         onTriggered: {             tableView.columnWidths[2] = 150             tableView.forceLayout();         }     } }

В моём случае это выглядит так

Rectangle {     id: main          property var columnVisible: [true, true, true, true, true, true, true]     property var columnWidths: [100, 50, 100, 150, 100, 350, 200]          function columnWidthProvider(column) {         let visible = columnVisible[column];         let width = visible ? columnWidths[column] : 0;         return width;     }      function hideColumn(column, hide) {         columnVisible[column] = hide;         view.forceLayout();     } //...     TableView {         id: view         columnWidthProvider: main.columnWidthProvider     } }

Осталось за малым, вывести заголовок таблицы и сделать изменение размера столбца. Естественно нашелся компонент под это дело, а именно HorizontalHeaderView

HorizontalHeaderView {     id: horizontalHeader      reuseItems: false     syncView: view     height: 30     Layout.fillWidth: true      delegate: Rectangle {         id: root          implicitWidth: 50         implicitHeight: 30          Text {             anchors.centerIn: parent             text: display             color: Style.LabelColor             font.bold: true         }          Rectangle {             id: splitter              color: Style.BorderColor             height: parent.height             width: 1             visible: mouseArea.containsMouse             x: columnWidths[index] - 1             onXChanged: {                 if (drag.active) {                     main.columnWidths[index] = splitter.x + 1;                     view.forceLayout();                 }             }              DragHandler {                 id: drag                  yAxis.enabled: false                 xAxis.enabled: true                 cursorShape: Qt.SizeHorCursor             }          }      } }

Да, это работает. Беремся за splitter и двигаем влево, вправо. Какие тут подводные камни? Понимающие люди обратили внимание на reuseItems: false. HorizontalHeaderView это view, а в view используется пул элементов(reusing items), что бы сэкономить на создание и удалении. Документация не рекомендует иметь делегаты с состоянием.

На что влияет reuseItems: true в данном примере. Представьте, вы взялись за splitter и растягиваете колонку, которая имеет ширину 300. В какой-то момент view решает пере использовать элемент, возвращает в пул, достаёт от туда, и вставляет в другое место и инициализирует шириной 40. Получаем не очень понятное поведение. В моём случае такое поведение проявляется при вставке/удалении строк таблицы.

Про окна

Интерфейс не перегружен окнами как MDI и не является SDI. Окна создаются динамически, что бы меньше имели общего состояния. Каждый такой компонент Window и создается следующим образом

function createConsumerScreen(topic, topicModel, broker) {     let component = Qt.createComponent("qrc:/qml/Consumer/ConsumerScreen.qml");     let posX = appWindow.x + appWindow.width/2 - Constants.ConsumerScreenWidth/2;     let posY = appWindow.y + appWindow.height/2 - Constants.ConsumerScreenHeight/2;     let state = {         x: posX,         y: posY,         topic:topic,         topicModel: topicModel,         broker: broker     };      let consumer = component.createObject(appWindow, state); }  function createMessageScreen(x,y,width,height,  message) {     let component = Qt.createComponent("qrc:/qml/Consumer/MessageView.qml");     let posX = x + width/2 - Constants.MessageViewWidth/2;     let posY = y + height/2 - Constants.MessageViewHeight/2;     let state = {         x: posX,         y: posY,         message:message,     };      let consumer = component.createObject(appWindow, state); }

Заключение

Получился прототип, который можно развивать до полноценного MVP. Из ближайших планов настроить какой-нибудь CI для сборок под Windows и Mac OS X. Весь код доступен на Git Hub.

P.S.

Передаю пламенный привет Антону Водостоеву из 2ГИС

ссылка на оригинал статьи https://habr.com/ru/post/568296/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *