import QtQuick 2.15 import QtQuick.Templates 2.15 as T import Qt.labs.qmlmodels 1.0 import DelegateUI 1.0 DelRectangle { id: control clip: true color: DelTheme.Primary.colorBgBase topLeftRadius : 6 topRightRadius: 6 columnHeaderTitleFont { family: DelTheme.DelTableView.fontFamily pixelSize: DelTheme.DelTableView.fontSize } rowHeaderTitleFont { family: DelTheme.DelTableView.fontFamily pixelSize: DelTheme.DelTableView.fontSize } property bool animationEnabled: DelTheme.animationEnabled property bool alternatingRow: false property int defaultColumnHeaderWidth: 100 property int defaultColumnHeaderHeight: 40 property int defaultRowHeaderWidth: 40 property int defaultRowHeaderHeight: 40 property bool columnGridVisible: false property bool rowGridVisible: false property real minimumRowHeight: 40 property real maximumRowHeight: Number.NaN property var initModel: [] property var columns: [] property var checkedKeys: [] property color colorGridLine: DelTheme.DelTableView.colorGridLine property bool columnHeaderVisible: true property font columnHeaderTitleFont property color colorColumnHeaderTitle: DelTheme.DelTableView.colorColumnTitle property color colorColumnHeaderBg: DelTheme.DelTableView.colorColumnHeaderBg property bool rowHeaderVisible: true property font rowHeaderTitleFont property color colorRowHeaderTitle: DelTheme.DelTableView.colorRowTitle property color colorRowHeaderBg: DelTheme.DelTableView.colorRowHeaderBg property color colorResizeBlockBg: DelTheme.DelTableView.colorResizeBlockBg property Component columnHeaderDelegate: Item { id: __columnHeaderDelegate property string align: headerData.align ?? 'center' property string selectionType: headerData.selectionType ?? '' property var sorter: headerData.sorter property var sortDirections: headerData.sortDirections ?? [] property var onFilter: headerData.onFilter Text { anchors { left: __checkBoxLoader.active ? __checkBoxLoader.right : parent.left leftMargin: __checkBoxLoader.active ? 0 : 10 right: parent.right rightMargin: 10 top: parent.top topMargin: 4 bottom: parent.bottom bottomMargin: 4 } font: control.columnHeaderTitleFont text: headerData.title color: control.colorColumnHeaderTitle verticalAlignment: Text.AlignVCenter horizontalAlignment: { if (__columnHeaderDelegate.align == 'left') return Text.AlignLeft; else if (__columnHeaderDelegate.align == 'right') return Text.AlignRight; else return Text.AlignHCenter; } } MouseArea { enabled: __sorterLoader.active hoverEnabled: true height: parent.height anchors.left: __checkBoxLoader.right anchors.right: __sorterLoader.right onEntered: cursorShape = Qt.PointingHandCursor; onExited: cursorShape = Qt.ArrowCursor; onClicked: { control.sort(column); __sorterLoader.updateIcon(); } } Loader { id: __checkBoxLoader anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter active: __columnHeaderDelegate.selectionType == 'checkbox' sourceComponent: DelCheckBox { id: __parentBox Component.onCompleted: { __parentBox.checkState = __private.parentCheckState; } onToggled: { if (checkState == Qt.Unchecked) { __private.model.forEach( object => { __private.checkedKeysMap.delete(object.key); }); __private.checkedKeysMapChanged(); } else { __private.model.forEach( object => { __private.checkedKeysMap.set(object.key, true); }); __private.checkedKeysMapChanged(); } } Connections { target: __private function onParentCheckStateChanged() { __parentBox.checkState = __private.parentCheckState; } } } } Loader { id: __sorterLoader anchors.right: __filterLoader.active ? __filterLoader.left : parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter active: sorter !== undefined sourceComponent: columnHeaderSorterIconDelegate onLoaded: { if (sortDirections.length === 0) return; let ref = control.columns[column]; if (!ref.hasOwnProperty('activeSorter')) { ref.activeSorter = false; } if (!ref.hasOwnProperty('sortIndex')) { ref.sortIndex = -1; } if (!ref.hasOwnProperty('sortMode')) { ref.sortMode = 'false'; } updateIcon(); } property int column: model.column property alias sorter: __columnHeaderDelegate.sorter property alias sortDirections: __columnHeaderDelegate.sortDirections property string sortMode: 'false' function updateIcon() { if (sortDirections.length === 0) return; let ref = control.columns[column]; if (ref.activeSorter) { sortMode = ref.sortMode; } else { sortMode = 'false'; } } } Loader { id: __filterLoader anchors.right: parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter active: onFilter !== undefined sourceComponent: columnHeaderFilterIconDelegate property int column: model.column property alias onFilter: __columnHeaderDelegate.onFilter } } property Component rowHeaderDelegate: Item { Text { anchors { left: parent.left leftMargin: 8 right: parent.right rightMargin: 8 top: parent.top topMargin: 4 bottom: parent.bottom bottomMargin: 4 } font: control.rowHeaderTitleFont text: (row + 1) color: control.colorRowHeaderTitle verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } } property Component columnHeaderSorterIconDelegate: Item { id: __sorterIconDelegate width: __sorterIconColumn.width height: __sorterIconColumn.height + 12 Column { id: __sorterIconColumn anchors.verticalCenter: parent.verticalCenter spacing: -2 DelIconText { visible: sortDirections.indexOf('ascend') !== -1 colorIcon: sortMode === 'ascend' ? DelTheme.DelTableView.colorIconHover : DelTheme.DelTableView.colorIcon iconSource: DelIcon.CaretUpOutlined iconSize: DelTheme.DelTableView.fontSize - 2 } DelIconText { visible: sortDirections.indexOf('descend') !== -1 colorIcon: sortMode === 'descend' ? DelTheme.DelTableView.colorIconHover : DelTheme.DelTableView.colorIcon iconSource: DelIcon.CaretDownOutlined iconSize: DelTheme.DelTableView.fontSize - 2 } } } property Component columnHeaderFilterIconDelegate: Item { width: __headerFilterIcon.width height: __headerFilterIcon.height + 12 HoverIcon { id: __headerFilterIcon anchors.centerIn: parent iconSource: DelIcon.SearchOutlined colorIcon: hovered ? DelTheme.DelTableView.colorIconHover : DelTheme.DelTableView.colorIcon onClicked: { __filterPopup.open(); } } DelPopup { id: __filterPopup x: -width * 0.5 y: parent.height padding: 5 animationEnabled: false contentItem: Column { spacing: 5 DelInput { id: __searchInput width: parent.width anchors.horizontalCenter: parent.horizontalCenter placeholderText: qsTr('Search ') + control.columns[column].dataIndex onEditingFinished: __searchButton.clicked(); Component.onCompleted: { let ref = control.columns[column]; if (ref.hasOwnProperty('filterInput')) { text = ref.filterInput; } else { ref.filterInput = ''; } } } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: 5 DelIconButton { id: __searchButton text: qsTr('Search') iconSource: DelIcon.SearchOutlined type: DelButton.Type_Primary onClicked: { __filterPopup.close(); control.columns[column].filterInput = __searchInput.text; control.filter(); } } DelButton { text: qsTr('Reset') onClicked: { __filterPopup.close(); control.columns[column].filterInput = ''; control.filter(); } } DelButton { text: qsTr('Close') type: DelButton.Type_Link onClicked: { __filterPopup.close(); } } } } } } onColumnsChanged: { let headerColumns = []; let headerRow = {}; for (const object of columns) { let column = Qt.createQmlObject('import Qt.labs.qmlmodels 1.0; TableModelColumn {}', __columnHeaderModel); column.display = object.dataIndex; headerColumns.push(column); headerRow[object.dataIndex] = object; } __columnHeaderModel.clear(); if (columnHeaderVisible) { __columnHeaderModel.columns = headerColumns; __columnHeaderModel.rows = [headerRow]; } let cellColumns = []; for (let i = 0; i < columns.length; i++) { let column = Qt.createQmlObject('import Qt.labs.qmlmodels 1.0; TableModelColumn {}', __cellModel); column.display = `__data${i}`; cellColumns.push(column); } __cellModel.columns = cellColumns; } onInitModelChanged: { clearSort(); filter(); } function checkForRows(rows) { rows.forEach( row => { if (row >= 0 && row < __private.model.length) { const key = __private.model[row].key; __private.checkedKeysMap.set(key, true); } }); __private.checkedKeysMapChanged(); } function checkForKeys(keys) { keys.forEach(key => __private.checkedKeysMap.set(object.key, true)); __private.checkedKeysMapChanged(); } function getCheckedKeys() { return [...__private.checkedKeysMap.keys()]; } function clearAllCheckedKeys() { __private.checkedKeysMap.clear(); __private.checkedKeysMapChanged(); __private.parentCheckState = Qt.Unchecked; __private.parentCheckStateChanged(); } function sort(column) { /*! 仅需设置排序相关属性, 真正的排序在 filter() 中完成 */ if (columns[column].hasOwnProperty('sorter')) { columns.forEach( (object, index) => { if (object.hasOwnProperty('sorter')) { if (column === index) { object.activeSorter = true; object.sortIndex = (object.sortIndex + 1) % object.sortDirections.length; object.sortMode = object.sortDirections[object.sortIndex]; } else { object.activeSorter = false; object.sortIndex = -1; object.sortMode = 'false'; } } }); } filter(); } function clearSort() { columns.forEach( object => { if (object.sortDirections && object.sortDirections.length !== 0) { object.activeSorter = false; object.sortIndex = -1; object.sortMode = 'false'; } }); __private.model = [...initModel]; } function filter() { let changed = false; let model = [...initModel]; columns.forEach( object => { if (object.hasOwnProperty('onFilter') && object.hasOwnProperty('filterInput')) { model = model.filter((record, index) => object.onFilter(object.filterInput, record)); changed = true; } }); if (changed) __private.model = model; /*! 根据 activeSorter 列排序 */ columns.forEach( object => { if (object.activeSorter === true) { if (object.sortMode === 'ascend') { /*! sorter 作为上升处理 */ __private.model.sort(object.sorter); __private.modelChanged(); } else if (object.sortMode === 'descend') { /*! 返回 ascend 相反结果即可 */ __private.model.sort((a, b) => object.sorter(b, a)); __private.modelChanged(); } else { /*! 还原 */ __private.model = model; } } }); } function clearFilter() { columns.forEach( object => { if (object.hasOwnProperty('onFilter') || object.hasOwnProperty('filterInput')) object.filterInput = ''; }); __private.model = [...initModel]; } function clear() { __private.model = initModel = []; __cellModel.clear(); columns.forEach( object => { if (object.sortDirections && object.sortDirections.length !== 0) { object.activeSorter = false; object.sortIndex = -1; object.sortMode = 'false'; } if (object.hasOwnProperty('onFilter') || object.hasOwnProperty('filterInput')) { object.filterInput = ''; } }); } function appendRow(object) { __private.model.push(object); __cellModel.appendRow(__private.toCellObject(object)); } function getRow(rowIndex) { if (rowIndex >= 0 && rowIndex < __private.model.length) { return __private.model[rowIndex]; } return undefined; } function insertRow(rowIndex, object) { __private.model.splice(rowIndex, 0, object); __cellModel.insertRow(rowIndex, __private.toCellObject(object)); } function moveRow(fromRowIndex, toRowIndex, count = 1) { if (fromRowIndex >= 0 && fromRowIndex < __private.model.length && toRowIndex >= 0 && toRowIndex < __private.model.length) { const objects = __private.model.splice(from, count); __private.model.splice(to, 0, ...objects); __cellModel.moveRow(fromRowIndex, toRowIndex, count); } } function removeRow(rowIndex, count = 1) { if (rowIndex >= 0 && rowIndex < __private.model.length) { __private.model.splice(rowIndex, count); __cellModel.removeRow(rowIndex, count); } } function setRow(rowIndex, object) { if (rowIndex >= 0 && rowIndex < __private.model.length) { __private.model[rowIndex] = object; __cellModel.setRow(rowIndex, __private.toCellObject(object)); } } component HoverIcon: DelIconText { signal clicked() property alias hovered: __hoverHandler.hovered HoverHandler { id: __hoverHandler cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: parent.clicked(); } } component ResizeArea: MouseArea { property bool isHorizontal: true property var target: __columnHeaderItem property point startPos: Qt.point(0, 0) property real minimumWidth: 0 property real maximumWidth: Number.NaN property real minimumHeight: 0 property real maximumHeight: Number.NaN property var resizeCallback: (result) => { } preventStealing: true hoverEnabled: true onEntered: cursorShape = isHorizontal ? Qt.SplitHCursor : Qt.SplitVCursor; onPressed: (mouse) => { if (target) { startPos = Qt.point(mouseX, mouseY); } } onPositionChanged: (mouse) => { if (pressed && target) { if (isHorizontal) { var resultWidth = 0; var offsetX = mouse.x - startPos.x; if (maximumWidth != Number.NaN && (target.width + offsetX) > maximumWidth) { resultWidth = maximumWidth; } else if ((target.width + offsetX) < minimumWidth) { resultWidth = minimumWidth; } else { resultWidth = target.width + offsetX; } resizeCallback(resultWidth); } else { var resultHeight = 0; var offsetY = mouse.y - startPos.y; if (maximumHeight != Number.NaN && (target.height + offsetY) > maximumHeight) { resultHeight = maximumHeight; } else if ((target.height + offsetY) < minimumHeight) { resultHeight = minimumHeight; } else { resultHeight = target.height + offsetY; } resizeCallback(resultHeight); } mouse.accepted = true; } } } Behavior on color { enabled: control.animationEnabled; ColorAnimation { duration: DelTheme.Primary.durationMid } } QtObject { id: __private property var model: [] property int parentCheckState: Qt.Unchecked property var checkedKeysMap: new Map function updateParentCheckBox() { let checkCount = 0; model.forEach( object => { if (checkedKeysMap.has(object.key)) { checkCount++; } }); parentCheckState = checkCount == 0 ? Qt.Unchecked : checkCount == model.length ? Qt.Checked : Qt.PartiallyChecked; parentCheckStateChanged(); } function updateCheckedKeys() { control.checkedKeys = [...checkedKeysMap.keys()]; } function toCellObject(object) { let dataObject = new Object; for (let i = 0; i < control.columns.length; i++) { const dataIndex = control.columns[i].dataIndex ?? ''; if (object.hasOwnProperty(dataIndex)) { dataObject[`__data${i}`] = object[dataIndex]; } else { dataObject[`__data${i}`] = null; } } return dataObject; } onModelChanged: { __cellView.contentY = 0; __cellModel.clear(); let cellRows = []; model.forEach( (object, index) => { let data = { }; for (let i = 0; i < columns.length; i++) { const dataIndex = columns[i].dataIndex ?? ''; if (object.hasOwnProperty(dataIndex)) { data[`__data${i}`] = object[dataIndex]; } else { data[`__data${i}`] = null; } } cellRows.push(data); }); __cellModel.rows = cellRows; __cellView.rowHeights = Array.from({ length: model.length }, () => control.defaultRowHeaderHeight); __rowHeaderModel.rows = model; updateParentCheckBox(); } onParentCheckStateChanged: updateCheckedKeys(); onCheckedKeysMapChanged: updateCheckedKeys(); } DelRectangle { id: __columnHeaderViewBg height: control.defaultColumnHeaderHeight anchors.left: control.rowHeaderVisible ? __rowHeaderViewBg.right : parent.left anchors.right: parent.right topLeftRadius: control.rowHeaderVisible ? 0 : 6 topRightRadius: 6 color: control.colorColumnHeaderBg visible: control.columnHeaderVisible TableView { id: __columnHeaderView anchors.fill: parent syncDirection: Qt.Horizontal syncView: __cellModel.rowCount == 0 ? null : __cellView columnWidthProvider: __cellView.columnWidthProvider boundsBehavior: Flickable.StopAtBounds clip: true model: TableModel { id: __columnHeaderModel } delegate: Item { id: __columnHeaderItem implicitHeight: control.defaultColumnHeaderHeight clip: true required property var model required property var display property int row: model.row property int column: model.column property string selectionType: display.selectionType ?? '' property bool editable: display.editable ?? false property real minimumWidth: display.minimumWidth ?? 40 property real maximumWidth: display.maximumWidth ?? Number.NaN TableView.onReused: { if (selectionType == 'checkbox') __private.updateParentCheckBox(); } Loader { anchors.fill: parent sourceComponent: control.columnHeaderDelegate property alias model: __columnHeaderItem.model property var headerData: control.columns[column] property alias column: __columnHeaderItem.column } Rectangle { z: 2 width: 1 color: control.colorGridLine height: parent.height * 0.5 anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter } ResizeArea { width: 8 height: parent.height minimumWidth: __columnHeaderItem.minimumWidth maximumWidth: __columnHeaderItem.maximumWidth anchors.right: parent.right anchors.rightMargin: -width * 0.5 target: __columnHeaderItem isHorizontal: true resizeCallback: result => __cellView.setColumnWidth(__columnHeaderItem.column, result); } } } Rectangle { width: parent.width height: 1 anchors.bottom: parent.bottom color: control.colorGridLine } } Rectangle { id: __rowHeaderViewBg width: control.defaultRowHeaderWidth anchors.top: control.columnHeaderVisible ? __columnHeaderViewBg.bottom : __cellMouseArea.top anchors.bottom: __cellMouseArea.bottom color: control.colorRowHeaderBg visible: control.rowHeaderVisible TableView { id: __rowHeaderView anchors.fill: parent syncDirection: Qt.Vertical syncView: __cellView boundsBehavior: Flickable.StopAtBounds clip: true model: TableModel { id: __rowHeaderModel TableModelColumn { } } delegate: Item { id: __rowHeaderItem implicitWidth: control.defaultRowHeaderWidth clip: true required property var model property int row: model.row Loader { anchors.fill: parent sourceComponent: control.rowHeaderDelegate property alias model: __rowHeaderItem.model property alias row: __rowHeaderItem.row } Rectangle { z: 2 width: parent.width * 0.5 color: control.colorGridLine height: 1 anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter } ResizeArea { width: parent.width height: 8 minimumHeight: control.minimumRowHeight maximumHeight: control.maximumRowHeight anchors.bottom: parent.bottom anchors.bottomMargin: -height * 0.5 target: __rowHeaderItem isHorizontal: false resizeCallback: result => __cellView.setRowHeight(__rowHeaderItem.row, result); } } } Rectangle { width: 1 height: parent.height anchors.right: parent.right color: control.colorGridLine } } MouseArea { id: __cellMouseArea anchors.top: control.columnHeaderVisible ? __columnHeaderViewBg.bottom : parent.top anchors.bottom: parent.bottom anchors.left: __columnHeaderViewBg.left anchors.right: __columnHeaderViewBg.right hoverEnabled: true onExited: __cellView.currentHoverRow = -1; onWheel: wheel => wheel.accepted = true; TableView { id: __cellView property int currentHoverRow: -1 property var rowHeights: [] function setRowHeight(row, rowHeight) { rowHeights[row] = rowHeight; forceLayout(); } function setColumnWidth(column, columnWidth) { control.columns[column].width = columnWidth; __columnHeaderView.forceLayout() forceLayout(); } rowHeightProvider: row => rowHeights[row]; columnWidthProvider: column => { let object = control.columns[column]; if (object.hasOwnProperty('width')) return object.width; else return control.defaultColumnHeaderWidth; } anchors.fill: parent boundsBehavior: Flickable.StopAtBounds T.ScrollBar.horizontal: __hScrollBar T.ScrollBar.vertical: __vScrollBar clip: true reuseItems: false /*! 重用有未知BUG */ model: TableModel { id: __cellModel } delegate: Rectangle { id: __rootItem implicitHeight: control.defaultRowHeaderWidth clip: true color: { if (__private.checkedKeysMap.has(key)) { if (row == __cellView.currentHoverRow) return DelTheme.isDark ? DelTheme.DelTableView.colorCellBgDarkHoverChecked : DelTheme.DelTableView.colorCellBgHoverChecked; else return DelTheme.isDark ? DelTheme.DelTableView.colorCellBgDarkChecked : DelTheme.DelTableView.colorCellBgChecked; } else { return row == __cellView.currentHoverRow ? DelTheme.DelTableView.colorCellBgHover : control.alternatingRow && __rootItem.row % 2 !== 0 ? DelTheme.DelTableView.colorCellBgHover : DelTheme.DelTableView.colorCellBg; } } TableView.onReused: { checked = __private.checkedKeysMap.has(key); if (__childCheckBoxLoader.item) { __childCheckBoxLoader.item.checked = checked; } } required property var model required property var index required property var display property int row: model.row property int column: model.column property string key: __private.model[row] ? (__private.model[row].key ?? '') : '' property string selectionType: control.columns[column].selectionType ?? '' property string dataIndex: control.columns[column].dataIndex ?? '' property string filterInput: control.columns[column].filterInput ?? '' property alias cellData: __rootItem.display property bool checked: false Loader { active: control.rowGridVisible width: parent.width height: 1 anchors.bottom: parent.bottom sourceComponent: Rectangle { color: control.colorGridLine } } Loader { active: control.columnGridVisible width: 1 height: parent.height anchors.right: parent.right sourceComponent: Rectangle { color: control.colorGridLine } } MouseArea { anchors.fill: parent hoverEnabled: true onEntered: __cellView.currentHoverRow = __rootItem.row; Loader { id: __childCheckBoxLoader active: selectionType == 'checkbox' anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter sourceComponent: DelCheckBox { id: __childBox Component.onCompleted: { __childBox.checked = __rootItem.checked = __private.checkedKeysMap.has(key); } onToggled: { if (checkState == Qt.Checked) { __private.checkedKeysMap.set(__rootItem.key, true); __rootItem.checked = true; } else { __private.checkedKeysMap.delete(__rootItem.key); __rootItem.checked = false; } __private.updateParentCheckBox(); __cellView.currentHoverRowChanged(); } Connections { target: __private function onCheckedKeysMapChanged() { __childBox.checked = __rootItem.checked = __private.checkedKeysMap.has(__rootItem.key); } } } property alias key: __rootItem.key } Loader { anchors.left: __childCheckBoxLoader.active ? __childCheckBoxLoader.right : parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom sourceComponent: { if (control.columns[__rootItem.column].delegate) { return control.columns[__rootItem.column].delegate; } else { return null; } } property alias row: __rootItem.row property alias column: __rootItem.column property alias cellData: __rootItem.cellData property alias cellIndex: __rootItem.index property alias dataIndex: __rootItem.dataIndex property alias filterInput: __rootItem.filterInput } } } Behavior on contentY { NumberAnimation {}} } } Loader { id: __resizeRectLoader z: 10 width: __rowHeaderViewBg.width height: __columnHeaderViewBg.height active: control.rowHeaderVisible && control.columnHeaderVisible sourceComponent: DelRectangle { color: control.colorResizeBlockBg topLeftRadius: 6 ResizeArea { width: parent.width height: 8 minimumHeight: control.defaultColumnHeaderHeight anchors.bottom: parent.bottom anchors.bottomMargin: -height * 0.5 target: __columnHeaderViewBg isHorizontal: false resizeCallback: result => __columnHeaderViewBg.height = result; } ResizeArea { width: 8 height: parent.height minimumWidth: control.defaultRowHeaderWidth anchors.right: parent.right anchors.rightMargin: -width * 0.5 target: __rowHeaderViewBg isHorizontal: true resizeCallback: result => __rowHeaderViewBg.width = result; } } } DelScrollBar { id: __hScrollBar z: 11 anchors.left: control.rowHeaderVisible ? __rowHeaderViewBg.right : __cellMouseArea.left anchors.right: __cellMouseArea.right anchors.bottom: __cellMouseArea.bottom } DelScrollBar { id: __vScrollBar z: 12 anchors.right: __cellMouseArea.right anchors.top: control.columnHeaderVisible ? __columnHeaderViewBg.bottom : __cellMouseArea.top anchors.bottom: __cellMouseArea.bottom } }