Работа с TableView в QML и изменение ширины столбцов

Добавлено 29 мая 2022 в 08:40

Построение пользовательских интерфейсов с помощью QML дает много свободы. Но также это сталкивает разработчика с проблемами, о существовании которых он, возможно, даже не задумывался. В данной статье мы рассмотрим создание пользовательского интерфейса, использующего таблицу (на базе компонента QML TableView), реализующего довольно очевидную функцию ручного изменения ширины столбцов.

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

Изменение ширины столбца
Исходное состояние интерфейса
Изменение ширины столбца
Изменение ширины столбца

Полный код примера доступен на GitHub:

Для предоставления данных, которые будут отображаться в TableView, используется тестовый класс MulticastTableModel, наследующийся от QAbstractTableModel. Здесь всё стандартно и ничем не отличается от моделей при работе с Qt Widgets. Единственное дополнительное действие, которое необходимо выполнить, – регистрация типа QML для данного класса модели в функции main():

qmlRegisterType<MulticastTableModel>("MulticastTableModel", 0, 1, "MulticastTableModel");

Код работы с табличным представлением в данном примере вынесен в отдельный файл ResizableColumnsTableView.qml, чтобы показать, как его можно использовать в качестве отдельного компонента.

Итак, в QML для представления таблицы в пользовательском интерфейсе используется два отдельных компонента: HorizontalHeaderView (для отображения заголовка таблицы) и TableView (для отображения контента таблицы). Поэтому начинаем построение нашего компонента ResizableColumnsTableView с компоновки расположения этих двух элементов. Дополнительно с помощью свойства syncView указываем для заголовка, с каким представлением ему синхронизировать ширину столбцов, расстояние между столбцами, прокрутку по горизонтали, а также текст в заголовках столбцов. А также выносим свойство model в корневой элемент, чтобы его можно было задать извне.

Rectangle {
    id: table
    required property var model
    // ...

    ColumnLayout {
        anchors.fill: parent
        spacing: 0

        HorizontalHeaderView {
            id: horizontalHeader
            reuseItems: false
            syncView: tableView
            height: 30
            Layout.fillWidth: true
            boundsBehavior:Flickable.StopAtBounds

            // ...
        } 

        TableView {
            id: tableView
            Layout.fillWidth: true
            Layout.fillHeight: true
            clip: true
            boundsBehavior:Flickable.StopAtBounds

            model: table.model

            // ...
        }

    } // ColumnLayout

} // Rectangle table

Настройка таблицы TableView

Далее задаем делегата для отображения контента ячеек в TableView.

delegate: Rectangle {
    implicitHeight: 26
    border.color: "#bbb"
    border.width: 1
    Text {
        id: cellText
        text: display
        anchors.verticalCenter: parent.verticalCenter
        x: 4
    }
}

Здесь мы задаем высоту строк таблицы, цвет и толщину границ ячеек, а также задаем для текста небольшой отступ от границы ячейки по горизонтали и выравнивание текста по центру по вертикали.

Далее настраиваем полосы прокрутки в TableView. Ниже показан фрагмент для вертикальной полосы прокрутки (код для горизонтальной полосы аналогичен).

ScrollBar.vertical: ScrollBar {
    policy: ScrollBar.AsNeeded
    active: true
    onActiveChanged: {
        if (!active)
            active = true;
    }
}

Здесь мы хотим, чтобы полосы прокрутки отображались всегда, если контент таблицы не влезает в размеры TableView. Однако при использовании стиля Material полосы отображаются только при прокрутке и через некоторое время скрываются. Поэтому здесь используется немного костыльное решение: при попытке скрыть полосу прокрутки (по таймеру) активируем ее снова.

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

Rectangle {
    id: table

    required property var columnWidths

    function columnWidthProvider(column) {
        return columnWidths[column]
    }
    
    ColumnLayout {
    
        // ...
    
        TableView {
            // ...
            columnWidthProvider: table.columnWidthProvider
            // ...
        } // TableView

    } // ColumnLayout

} // Rectangle table

Функция columnWidthProvider берет значения ширин из свойства columnWidths, которое представляет собой массив значений ширин, и которое указывается с начальными значениями при создании компонента (ниже приведен фрагмент из файла main.qml, в котором создается элемент ResizableColumnsTableView).

ResizableColumnsTableView {
    Layout.fillHeight: true
    Layout.fillWidth: true
    model: MulticastTableModel {}
    columnWidths: [250, 200, 100]
}

Настройка заголовка таблицы HorizontalHeaderView

Делегат заголовка будет состоять из трех дочерних элементов:

  1. текстовый элемент Text (headerText) – как несложно догадаться, отображает текст заголовка столбца;
  2. прямоугольник Rectangle (splitter) – интерактивный элемент, за который мы будем хвататься и перетаскивать для изменения ширины столбца;
  3. область обработки событий мыши MouseArea (dragArea) – невизуальный элемент, позволяющий обрабатывать перетаскивание прямоугольника splitter.
delegate: Rectangle {
    id: columnHeader
    color: "#eee"
    border.color: "#bbb"
    border.width: 1
    implicitWidth: headerText.contentWidth + 5*splitter.width
    implicitHeight: 30

    Text {
        id: headerText
        anchors.centerIn: parent
        text: display
    }

    Rectangle {
        id: splitter
        
        // ...
    } 

    MouseArea {
        id: dragArea
        
        // ...
    }

} // delegate Rectangle columnHeader

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

Далее настраиваем элемент области обработки событий мыши MouseArea (dragArea):

  • привязываем его размеры к прямоугольнику splitter;
  • задаем курсор, отображающийся при наведении на данный элемент (cursorShape: Qt.SizeHorCursor);
  • чтобы заданный курсор отображался сразу при наведении на данный элемент, задаем свойство hoverEnabled: true (по умолчанию курсор изменится только при клике на элемент);
  • задаем цель перетаскивания (drag.target: splitter);
  • включаем перетаскивание только по горизонтали (drag.axis: Drag.XAxis);
  • устанавливаем порог перетаскивания на 0 (drag.threshold: 0), иначе перетаскивание запустится только при довольно большом отдалении курсора от сплиттера;
  • задаем минимальное значение x (drag.minimumX: parent.implicitWidth), чтобы не обрезать текст заголовка при чрезмерном сужении столбца.
MouseArea {
    id: dragArea
    anchors.fill: splitter
    cursorShape: Qt.SizeHorCursor
    drag.target: splitter
    drag.axis: Drag.XAxis
    drag.threshold: 0
    drag.minimumX: parent.implicitWidth
    hoverEnabled: true
}

Далее настраиваем прямоугольник сплиттера:

  • задаем цвет, размер и положение (он будет находиться на правой границе ячейки заголовка столбца);
  • он будет видим только при наведении мыши и перетаскивании (visible: dragArea.containsMouse || dragArea.drag.active);
  • в обработчике изменения положения по горизонтали (onXChanged) обновляем соответствующее значение в массиве ширин столбцов (columnWidths) и запускаем принудительное изменение компоновки (tableView.forceLayout()).
Rectangle {
    id: splitter
    x: table.columnWidths[index] - width
    height: horizontalHeader.height
    width: 4
    color: "#999"
    visible: dragArea.containsMouse || dragArea.drag.active

    onXChanged: {
        if (dragArea.drag.active) {
            table.columnWidths[index] = splitter.x + width;
            tableView.forceLayout();
        }
    }
} 

Вот и всё. Ниже приведен код файла ResizableColumnsTableView.qml, а полный код проекта примера доступен на GitHub.

Код файла ResizableColumnsTableView.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Rectangle {
    id: table

    required property var model
    required property var columnWidths

    function columnWidthProvider(column) {
        return columnWidths[column]
    }

    ColumnLayout {
        anchors.fill: parent
        spacing: 0

        HorizontalHeaderView {
            id: horizontalHeader

            reuseItems: false
            syncView: tableView
            height: 30
            Layout.fillWidth: true
            boundsBehavior:Flickable.StopAtBounds

            delegate: Rectangle {
                id: columnHeader
                color: "#eee"
                border.color: "#bbb"
                border.width: 1
                implicitWidth: headerText.contentWidth + 5*splitter.width
                implicitHeight: 30

                Text {
                    id: headerText
                    anchors.centerIn: parent
                    text: display
                }

                Rectangle {
                    id: splitter
                    x: table.columnWidths[index] - width
                    height: horizontalHeader.height
                    width: 4
                    color: "#999"
                    visible: dragArea.containsMouse || dragArea.drag.active

                    onXChanged: {
                        if (dragArea.drag.active) {
                            table.columnWidths[index] = splitter.x + width;
                            tableView.forceLayout();
                        }
                    }
                } // Rectangle splitter

                MouseArea {
                    id: dragArea
                    anchors.fill: splitter
                    cursorShape: Qt.SizeHorCursor
                    drag.target: splitter
                    drag.axis: Drag.XAxis
                    drag.threshold: 0
                    drag.minimumX: parent.implicitWidth
                    hoverEnabled: true
                }

            } // delegate Rectangle columnHeader

        } // HorizontalHeaderView

        TableView {
            id: tableView
            Layout.fillWidth: true
            Layout.fillHeight: true
            clip: true
            boundsBehavior:Flickable.StopAtBounds
            columnWidthProvider: table.columnWidthProvider
            model: table.model

            ScrollBar.vertical: ScrollBar {
                policy: ScrollBar.AsNeeded
                active: true
                onActiveChanged: {
                    if (!active)
                        active = true;
                }
            }

            ScrollBar.horizontal: ScrollBar {
                policy: ScrollBar.AsNeeded
                active: true
                onActiveChanged: {
                    if (!active)
                        active = true;
                }
            }

            delegate: Rectangle {
                implicitHeight: 26
                border.color: "#bbb"
                border.width: 1
                Text {
                    id: cellText
                    text: display
                    anchors.verticalCenter: parent.verticalCenter
                    x: 4
                }
            }
        } // TableView

    } // ColumnLayout

} // Rectangle table

Теги

GUI / Графический интерфейс пользователяHorizontalHeaderViewMVD / model-view-delegate / модель-представление-делегатQMLQtQt WidgetsQtQuickQtQuick ControlsTableViewПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.