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

Шекспир, Генрих 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ГИС