// =============== 文件表格面板 =============== import QtQuick 2.15 import QtQuick.Controls 2.15 import Qt.labs.qmlmodels 1.0 // 表格 import QtGraphicalEffects 1.15 // 子元素圆角 import QtQuick.Dialogs 1.3 // 文件对话框 Item { id: fTableRoot // ========================= 【定义】 ========================= // 表头。定义每一列。 property var headers: [ // 第一列也作为总key(tk),不允许重复。 {key: "path", title: "文件", }, {key: "time", title: "耗时", }, {key: "state", title: "状态", }, // 可选项: // btn: true 启用按钮 // onClicked: 单击函数 // left: true 左对齐 // display: 显示函数,输入value,返回显示文本 ] property string openBtnText: "选择文件" property string clearBtnText: "清空" property string defaultTips: "拖入或选择文件" property string fileDialogTitle: "请选择文件" property var fileDialogNameFilters: ["文件 (*.jpg *.jpe *.jpeg *.jfif *.png *.webp *.bmp *.tif *.tiff)"] property int spacing: size_.smallLine // 表项水平间隔 property int minWidth0: size_.smallLine * 5 // 第0列最小宽度 property bool isLock: false // 是否锁定UI操作 // ========================= 【调用接口】 ========================= // 增:添加一项。 row:字典,key在headers中,如 { "path" "time" "state" } // ik:可以是表格行index(int),也可以是总key(string) function add(row, ik=-1) { const key = row[headerKey] if(key in dataDict) { console.warn(`add: ${key} 已在dataDict中!`) return false } if(ik === -1 || ik === rowCount) { dataDict[key] = rowCount dataModel.append(row) } else { const i = ik2i(ik) if(i < 0) { console.warn(`add: ik ${ik} ${i} < 0 !`) return false } dataDict[key] = i dataModel.insert(i, row) } updateWidth() return true } // 删:删除一项 function del(ik) { const i = ik2i(ik) if(i < 0) { console.warn(`del: ik ${ik} ${i} < 0 !`) return false } const key = dataModel.get(i)[headerKey] delete dataDict[key] dataModel.remove(i) return true } // 删:清空 function clear() { dataModel.clear() dataDict = {} } // 改:属性字典 function set(ik, columnDict) { const i = ik2i(ik) if(i < 0) { console.warn(`set: ik ${ik} ${i} < 0 !`) return false } dataModel.set(i, columnDict) updateWidth() return true } // 改:单个属性 function setProperty(ik, columnKey, value) { const i = ik2i(ik) if(i < 0) { console.warn(`setProperty: ik ${ik} ${i} < 0 !`) return false } dataModel.setProperty(i, columnKey, value) updateWidth() return true } // 查:ik转index。返回-1表示失败。 function ik2i(ik) { if (typeof ik === "number") { if(ik >= 0 && ik < rowCount) return ik } else if (typeof ik === "string") { if(ik in dataDict) return dataDict[ik] } return -1 } // 查:获取单个行的字典 function get(ik) { const i = ik2i(ik) if(i < 0) { console.warn(`get: ik ${ik} ${i} < 0 !`) return {} } return dataModel.get(i) } // 查:获取key列的所有数据,返回每项为value function getColumnsValue(key) { let list = [] for(let y = 0; y < rowCount; y++) { list.push( dataModel.get(y)[key] ) } return list } // 查:获取多个列的数据,返回每项为字典 function getColumnsValues(keys=[]) { let list = [] if(keys.length > 0) { for(let y = 0; y < rowCount; y++) { const data = dataModel.get(y) const d = {} for(let i in keys) d[keys[i]] = data[keys[i]] list.push(d) } } else { for(let y = 0; y < rowCount; y++) list.push(dataModel.get(y)) } return list } // 定义信号 signal addPaths(var paths) // 添加文件的信号 signal click(var info) // 点击条目的信号 Component.onCompleted: { dataDict = {} columnCount = headers.length for(let i=0; i ws[i]) ws[i] = maxWidth } // 表体 for(let y in tableView.items) { const repeater = tableView.items[y].repeater for(let x = 1; x < columnCount; x++) { let maxWidth = repeater.itemAt(x).maxWidth + fTableRoot.spacing*2 if(maxWidth > ws[x]) ws[x] = maxWidth } } // 赋值 / 计算第0列宽度 let w0 = tableArea.width for(let i = 1; i < columnCount; i++) { headerModel.setProperty(i, "width", ws[i]) w0 -= ws[i] } // 更新第0列宽度 updateWidth0(w0) } // 更新第0列宽度 function updateWidth0(w0 = -1) { if(headerModel.count <= 0) return if(w0 < 0) { w0 = tableArea.width for(let i = 1; i < columnCount; i++) w0 -= headerModel.get(i).width } w0 += columnCount-10 // 避让右侧滚动条空间 if(w0 < minWidth0) w0 = minWidth0 headerModel.setProperty(0, "width", w0) } // ========================= 【布局】 ========================= // 表格区域 Rectangle { id: tableArea anchors.fill: parent color: theme.bgColor Item { id: tableContainer anchors.fill: parent // 上方操控版 Item { id: tableTopPanel anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right height: size_.line * 2 // 左打开图片按钮 IconTextButton { id: openBtn visible: parent.width > openBtn.width + clearBtn.width // 容器宽度过小时隐藏 anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: size_.smallSpacing * 0.5 icon_: "folder" text_: openBtnText onClicked: { if(isLock) return fileDialog.open() } } // 右清空按钮 IconTextButton { id: clearBtn anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: size_.smallSpacing * 0.5 icon_: "clear" text_: clearBtnText onClicked: { if(isLock) return fTableRoot.clear() } } } // 提示 DefaultTips { visibleFlag: fTableRoot.rowCount anchors.top: tableTopPanel.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom tips: defaultTips } // 表头 Item { id: tableHeaderContainer visible: fTableRoot.rowCount > 0 anchors.top: tableTopPanel.bottom anchors.left: parent.left anchors.right: parent.right height: size_.line * 1.5 onWidthChanged: updateWidth0() Row { anchors.fill: parent spacing: -1 Repeater { model: headerModel id: headerRepeater Rectangle { width: model.width anchors.top: parent.top anchors.bottom: parent.bottom color: theme.bgColor border.width: 1 border.color: theme.coverColor2 property alias maxWidth: hText.width Text_ { id: hText anchors.top: parent.top anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter verticalAlignment: Text.AlignVCenter // 垂直居中 font.pixelSize: size_.smallText text: model.title } } } } } // 表体 TableView { id: tableView anchors.top: tableHeaderContainer.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom flickableDirection: Flickable.VerticalFlick // 只允许垂直滚动 boundsBehavior: Flickable.StopAtBounds // 禁止flick过冲。不影响滚轮滚动的过冲 model: dataModel clip: true property var items: tableView.children[0].children rowSpacing: -1 delegate: Item { Component.onCompleted: updateWidth() TableView.onReused: updateWidth() implicitHeight: size_.smallLine * 1.5 implicitWidth: 1 property int rowIndex: index property var rowModel: model property alias repeater: repeater Row { anchors.fill: parent spacing: -1 Repeater { id: repeater model: headerModel Rectangle { width: model.width anchors.top: parent.top anchors.bottom: parent.bottom color: theme.bgColor border.width: 1 border.color: theme.coverColor2 property alias maxWidth: hText.width property int columnIndex: index property string columnKey: model.key property var header: headers[columnIndex] clip: true Button_ { visible: header.btn?true:false anchors.fill: parent radius: 0 onClicked: { if(header.onClicked) { header.onClicked(rowIndex) } } } Text_ { id: hText property bool isLeft: headers[columnIndex].left?true:false anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: isLeft? parent.left : undefined anchors.leftMargin: fTableRoot.spacing * 0.5 anchors.horizontalCenter: isLeft? undefined : parent.horizontalCenter verticalAlignment: Text.AlignVCenter // 垂直居中 font.pixelSize: size_.smallText color: (columnKey != "state"|| typeof rowModel.state != "string" || rowModel.state.length == 0) ? theme.subTextColor : (rowModel.state.startsWith("×") ? theme.noColor : (rowModel.state.startsWith("√") ? theme.yesColor : theme.subTextColor)) text: header.display ? header.display(rowModel[columnKey]) : rowModel[columnKey] } } } } } // 滚动条 ScrollBar.vertical: ScrollBar { } } } // 内圆角裁切 layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { width: tableContainer.width height: tableContainer.height radius: size_.btnRadius } } } // 文件选择对话框 // QT-5.15.2 会报错:“Model size of -225 is less than 0”,不影响使用。 // QT-5.15.5 修复了这个Bug,但是PySide2尚未更新到这个版本号。只能先忍忍了 // https://bugreports.qt.io/browse/QTBUG-92444 FileDialog_ { id: fileDialog title: fileDialogTitle nameFilters: fileDialogNameFilters folder: shortcuts.pictures selectMultiple: true // 多选 onAccepted: { addPaths(fileDialog.fileUrls_) } } }