• Qt5 QML TreeView currentIndex当前选中项的一些问题


    0.前言

    Qt5 QML Controls1.4 中的 TreeView 存在诸多问题,比如节点连接的虚线不好实现,currentIndex 的设置和 changed 信号的触发等。我想主要的原因在于 TreeView 是派生自 BasicTableView,而 TableView 内部又是由 ListView 实现的。

    正好项目用到了 TreeView,就踩一踩坑,发现 currentIndex 的很多行为是反直觉的,和 QWidgets 的 QTreeView 逻辑都不一样。比如没法直接设置 currentIndex,又比如收起或删除子节点后,currentIndex 不是指向根节点,而是内部 ListView 的下一行节点。如果时间充裕,建议还是自己实现一个 TreeView。

    本文主要总结下遇到的 currentIndex 相关的一些问题,因为目前主要用单选,所以处理的场景也是单选的。都是些零碎的小问题,后面可能会补充一些,以及修复 Demo 的 Bug。

    开发环境:Win10 + MSVC2019 + Qt5.15.2 64bit

    Demo 链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qml/TestQml_20221120_TreeSelection

    1.探索问题

    当展开收起,或者增删节点的时候,如果选中项在操作节点所在行的下面,TreeView 的 currentIndexChanged 会触发两次,第一次触发时会是一个错误的值。查看源码可以看到 currentIndex 的实现如下:

    1. readonly property var currentIndex: modelAdaptor.updateCount, modelAdaptor.mapRowToModelIndex(__currentRow)
    2. property alias __currentRow: listView.currentIndex
    3. __model: TreeModelAdaptor {
    4. id: modelAdaptor
    5. model: root.model
    6. // Hack to force re-evaluation of the currentIndex binding
    7. property int updateCount: 0
    8. onModelReset: updateCount++
    9. onRowsInserted: updateCount++
    10. onRowsRemoved: updateCount++
    11. onExpanded: root.expanded(index)
    12. onCollapsed: root.collapsed(index)
    13. }

    Adaptor 处理 Tree 和内部 List 的映射,内部 ListView 只持有展开显示的节点内容,展开收起对其来说和增删是一样的。currentIndex 是绑定到一个逗号表达式,当展开收起或者增删节点时,updateCount++ 触发 currentIndex 变更,但是此时 ListView 的 currentIndex 还没更新,所以会得到一个错误的 currentIndex,等 ListView 刷新了才能计算出一个正确的 currentIndex,这就解释了为什么会触发两次 currentIndexChanged。

    还有个较大的问题是,收起或删除选中节点,默认是取内部 ListView 的下一行,如果是删除展开子树的末尾项,不会自动选中前面的兄弟节点,而是往下跑到另一个子树去了。解决思路就是收起和删除时先把 currentIndex 设置为操作之后的 index。

    既然 TreeView 的 currentIndex 更新有问题,我们引入 ItemSelectionModel 来处理选择。引入 ItemSelectionModel 后,发现 selection 的 currentIndex 和 TreeView 的 currentIndex 有时候会不一致,高亮优先显示 selection 的,但是按键优先用 view 的,两个 index 不一致的时候行为就很怪异。解决方式也是提前设置操作之后的 index,不使用默认的行为。

    ItemSelectionModel 有个问题,TreeModel reset 数据的时候,不会触发 currentIndexChanged,从源码来看是因为某些原因他把信号给阻塞了:

    1. void QItemSelectionModel::reset()
    2. {
    3. const QSignalBlocker blocker(this);
    4. clear();
    5. }

    通过前面的一系列问题,我们很多地方都需要自己预置 currentIndex,TreeView 没有没有设置的接口,但是可以通过设置内部 ListView 的 currentIndex 来实现,而 ItemSelectionModel 提供了 setCurrentIndex 的接口。要实现代码选中某个节点,ListView 当前行和 TreeView 的 index 需要转换。Adaptor 有一个公开的函数 mapRowToModelIndex 可以将行转为 index,但是 index 转行的接口没有注册为 QML 可访问,迫不得已得把 Adaptor 的源码复制粘贴一份,导出 itemIndex 接口。

    1. //选中
    2. function selectIndex(index) {
    3. if (index.valid) {
    4. __listView.forceActiveFocus()
    5. __listView.currentIndex = modelAdaptor.itemIndex(index)
    6. mouseArea.mouseSelect(index, Qt.NoModifier, false)
    7. }
    8. }
    9. //清除选中
    10. function clearSelect() {
    11. __listView.forceActiveFocus()
    12. __listView.currentIndex = -1
    13. if (selection) {
    14. selection.clear()
    15. }
    16. }

    自定义 Adaptor 后 TreeView 和 TreeViewStyle 等都得 copy 源码自定义一下,因为 QML 很多东西不是多态性的,不是直接派生个新组件重写某个属性就完了,而且 Controls1.x 的组件耦合性又很强,需要把相关的都修改一下,可以参照前言中我的 Demo。

    2.修改片段

    自定义 Apdator 后,除了公开 itemIndex 接口,还有个重要的任务就是控制收起和删除节点后,当前节点不要只往后面跑,优先考虑兄弟节点和根节点。

    1. __model: BasicTreeModelAdaptor {
    2. id: modelAdaptor
    3. model: root.model
    4. // Hack to force re-evaluation of the currentIndex binding
    5. property int updateCount: 0
    6. onModelReset: updateCount++
    7. onRowsInserted: updateCount++
    8. onRowsRemoved: updateCount++
    9. onExpanded: root.expanded(index)
    10. onCollapsed: root.collapsed(index)
    11. onRowsAboutToBeRemoved: function(parent, first, last) {
    12. //Adaptor的row是仅可见的row
    13. //删除or收起某一行则触发row remove
    14. //如果隐藏的选中行,默认是到了下一行节点,无论这个节点是不是在同一个子树
    15. //console.log(first, last, root.__listView.currentIndex, root.currentIndex)
    16. if (first < 0 || root.__listView.currentIndex < first ||
    17. root.__listView.currentIndex > last)
    18. return
    19. var temp = root.currentIndex
    20. if (!temp.valid)
    21. return
    22. //如果是收起则前往收起节点,如果是删除,有兄弟从后往前找兄弟,没兄弟就找上一层节点
    23. //因为adaptor是个listview的model,所以参数是row,要转换成tree的index
    24. //先找下一个节点是不是兄弟,或者前面没节点了
    25. var next_index = modelAdaptor.mapRowToModelIndex(last + 1)
    26. if (first === 0 || next_index.valid && next_index.parent === temp.parent) {
    27. __listView.forceActiveFocus()
    28. __listView.currentIndex = last + 1
    29. mouseArea.mouseSelect(next_index, Qt.NoModifier, false)
    30. return
    31. }
    32. //不然就去上一个节点是否是祖先或者兄弟
    33. var prev_index = modelAdaptor.mapRowToModelIndex(first - 1)
    34. //console.log('--',temp, prev_index, temp.parent, prev_index.parent)
    35. if (!prev_index.valid)
    36. return
    37. //current和prev都往上一层找,所以需要两层循环
    38. while (temp.valid) {
    39. //console.log('--',temp, prev_index, temp.parent, prev_index.parent)
    40. var prev_temp = prev_index
    41. while (prev_temp.valid) {
    42. //console.log('++',temp, prev_temp, temp.parent, prev_temp.parent)
    43. if (temp.parent === prev_temp || temp.parent === prev_temp.parent) {
    44. __listView.forceActiveFocus()
    45. __listView.currentIndex = modelAdaptor.itemIndex(prev_temp)
    46. mouseArea.mouseSelect(prev_temp, Qt.NoModifier, false)
    47. return;
    48. }
    49. prev_temp = prev_temp.parent
    50. }
    51. temp = temp.parent
    52. }
    53. //可能还有其他没考虑到的情况
    54. console.log('other row remove', first, last, root.__listView.currentIndex)
    55. }
    56. }

    双击节点可以增加展开/收起逻辑。

    1. onDoubleClicked: {
    2. var clickIndex = __listView.indexAt(0, mouseY + __listView.contentY)
    3. if (clickIndex > -1) {
    4. var modelIndex = modelAdaptor.mapRowToModelIndex(clickIndex)
    5. //增加双击展开收起
    6. if (!branchDecorationContains(mouse.x, mouse.y)) {
    7. if (modelAdaptor.isExpanded(modelIndex))
    8. modelAdaptor.collapse(modelIndex)
    9. else
    10. modelAdaptor.expand(modelIndex)
    11. }
    12. if (!root.__activateItemOnSingleClick)
    13. root.activated(modelIndex)
    14. root.doubleClicked(modelIndex)
    15. }
    16. }

  • 相关阅读:
    C++项目实战——基于多设计模式下的同步&异步日志系统(总集篇)
    div的并列和包含关系
    数据结构与算法之 leetcode 47. 全排列 II (回溯)
    SpringSecurity(十五)---OAuth2的运行机制(上)-OAuth2概念和授权码模式讲解
    Java框架 SpringMVC--完全注解配置
    LeetCode 热题 100 | 二叉树(终)
    【Embedded System】裸机接口开发
    mybatis_plus
    解决使用`npm install`或`npm i`命令之后报`Unexpected token in JSON at position`错误的问题
    CodeForces..学习读书吧.[简单].[条件判断].[找最小值]
  • 原文地址:https://blog.csdn.net/gongjianbo1992/article/details/127956454