— Я духов вызывать могу из бездны!
— И я могу, и всякий это может. Вопрос лишь, явятся ль они на зов.
Шекспир, Генрих IV
Как-то так сложилось, что у нас не так много UI для Apache Kafka. А если хочется именно desktop, то Offset Explorer и упомянутый Conduktor. Первый имеет морально устаревший интерфейс 2000х, а второй не оправдано дорогой, т. к. не использую весь его богатый функционал. Вооружившись Qt и librdkafka, набросал conduktor на минималках.
Используй layout Люк
![](https://habrastorage.org/getpro/habr/upload_files/24a/4b6/fed/24a4b6fede5aab582a1bde49f717d7ae.png)
Сложный выпадающий элемент, имеющий множество состояний. Как съесть слона? По кусочкам маленького размера. Лайауты умеют отслеживать изменение видимости компонентов, и высчитывать размеры на основе 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
}
}
}
![](https://habrastorage.org/getpro/habr/upload_files/216/c79/0fd/216c790fd95d569580ab40cfa34f7330.png)
Сворачивающая левая панель построена на манипуляции с размерами
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 таблицы и списка
![](https://habrastorage.org/getpro/habr/upload_files/d0b/ce9/333/d0bce9333bea80f14cfa3e474f43f565.png)
Твист заключается в переключении между двумя режимами отображения
список
таблица
Достигается это наложением таблицы со списком, с последующей синхронизации полосы прокрутки. Делается через 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.
Ещё раз о таблицах
![](https://habrastorage.org/getpro/habr/upload_files/25e/ba8/3e6/25eba83e65bf135941d77919e56ff611.png)
Изменение количества колонок в таблице и их размер, это привычный функционал и что-то само собой разумеется. Что может пойти не так? 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ГИС