Работа с TableView в QML и изменение ширины столбцов
Построение пользовательских интерфейсов с помощью 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
Делегат заголовка будет состоять из трех дочерних элементов:
- текстовый элемент
Text
(headerText
) – как несложно догадаться, отображает текст заголовка столбца; - прямоугольник
Rectangle
(splitter
) – интерактивный элемент, за который мы будем хвататься и перетаскивать для изменения ширины столбца; - область обработки событий мыши
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