Files
work-secretfile-selfcheck/UmiOCR-data/qt_res/qml/Widgets/FilesTableView.qml

429 lines
15 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============== 文件表格面板 ===============
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: [
// 第一列也作为总keytk不允许重复。
{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可以是表格行indexint也可以是总keystring
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<columnCount; i++){
headerModel.append({
"key": headers[i].key,
"title": headers[i].title,
"width": 1,
})
}
headerKey = headers[0].key
updateWidth(true)
}
// ========================= 【逻辑】 =========================
property int columnCount: 0 // 列数量, onCompleted 中初始化
property int rowCount: dataModel.count // 行数量
property string headerKey: "" // 自动
// 表头, key title width
ListModel { id: headerModel }
// 数据, 项为headers的key
ListModel { id: dataModel }
property var dataDict: {} // 指向 dataModel 的 index
onRowCountChanged: {
headerModel.setProperty(0, "title", headers[0].title + ` (${rowCount})`)
}
// 宽度更新
Timer {
id: updateWidthTimer
interval: 100
repeat: false
onTriggered: {
updateWidth(true)
}
}
// 更新全部宽度
function updateWidth(timer=false) {
if(!timer) { // 启动计时器,减少调用频率
updateWidthTimer.restart()
return
}
let ws = Array(columnCount).fill(1)
// 表头
for(let i = 1; i < columnCount; i++) {
let maxWidth = headerRepeater.itemAt(i).maxWidth + fTableRoot.spacing*2
if(maxWidth > 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_)
}
}
}