Работа с 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
