QT实现QTreeWidget项目拖拽移动功能

主要功能概述

允许用户在QTreeWidget内部拖拽项目

拖拽时显示确认对话框

程序环境

Python 3.8.9
pyside6==6.1.3

pip install pyside6==6.1.3

实现效果

20251022_175428

demo代码获取

Gitee:treewidget-demo

百度网盘:https://pan.baidu.com/s/1rDrZUyrjrqmDrFVSdUgbQA?pwd=hp9b

代码实现

以下是完整的实现代码:

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QMessageBox, QAbstractItemView
import sys
from untitled2 import *

class MainWindow(QMainWindow, Ui_Form):
    def __init__(self):
        super().__init__()
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        self.setupUi(central_widget)

        # 设置拖放属性
        self.file_list.setDragEnabled(True)
        self.file_list.setAcceptDrops(True)
        self.file_list.setDragDropMode(QAbstractItemView.InternalMove)

        # 重写 dropEvent
        def new_drop_event(event: QDropEvent):
            source = event.source()
            if source is not self.file_list:
                event.ignore()
                return

            # 获取目标位置
            target_index = self.file_list.indexAt(event.position().toPoint())
            if not target_index.isValid():
                event.ignore()
                return

            # 获取所有选中的项目(不只是 currentItem)
            selected_items = self.file_list.selectedItems()
            if not selected_items:
                event.ignore()
                return

            # 获取目标项目
            target_item = self.file_list.itemFromIndex(target_index)

            # 显示确认对话框
            target_name = target_item.text(0) if target_item else "列表末尾"

            reply = QMessageBox.question(
                self,
                "确认移动",
                f"确定要将项目: {len(selected_items)}个项目\n移动到: {target_name} 之前吗?",
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
            )

            if reply != QMessageBox.StandardButton.Yes:
                event.ignore()
                return

            if event.proposedAction() == Qt.MoveAction:
                print(f"移动 {len(selected_items)} 个项目")

                # 如果是移动到目标项目上(覆盖)
                if target_item:
                    print(f"覆盖到 {target_item.text(0)}")

                    # 获取目标位置的行号
                    target_row = target_index.row()

                    # 克隆所有选中的项目并插入到目标位置
                    parent = self.file_list.invisibleRootItem()
                    for i, item in enumerate(selected_items):
                        new_item = item.clone()  # 复制项目
                        parent.insertChild(target_row + i, new_item)
                else:
                    # 直接移动所有项目到末尾
                    parent = self.file_list.invisibleRootItem()
                    for item in selected_items:
                        parent.addChild(item.clone())  # 或者直接移动 item

                event.accept()  # 接受事件,阻止默认行为
            else:
                event.ignore()

        self.file_list.dropEvent = new_drop_event

        # 正确连接信号的方式
        self.file_list.model().rowsAboutToBeRemoved.connect(self.on_rows_about_to_be_removed)

        # 用于跟踪移动的临时变量
        self.moved_items_cache = []

    def on_rows_about_to_be_removed(self, parent: QModelIndex, start: int, end: int):
        """在项目被移除前缓存被移动的项目"""
        if not parent.isValid():  # 只处理顶级项目
            self.moved_items_cache = []
            for i in range(start, end + 1):
                item = self.file_list.topLevelItem(i)
                if item:
                    self.moved_items_cache.append(item.text(0))

        print(self.moved_items_cache)
        print("移动至位置start",start,end)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

代码解析

1. 初始化设置

# 设置拖放属性
self.file_list.setDragEnabled(True)
self.file_list.setAcceptDrops(True)
self.file_list.setDragDropMode(QAbstractItemView.InternalMove)

这三行代码启用了QTreeWidget的拖放功能,并设置为内部移动模式(InternalMove),这意味着只允许在控件内部进行拖拽操作。

2. 重写dropEvent

重写了QTreeWidget的dropEvent方法,以实现自定义的拖放逻辑:

def new_drop_event(event: QDropEvent):
    # 检查来源是否是当前控件
    source = event.source()
    if source is not self.file_list:
        event.ignore()
        return

首先检查拖拽事件的来源,如果不是来自当前QTreeWidget,则忽略该事件。

3. 获取目标位置

通过indexAt方法获取鼠标释放位置对应的项目索引,如果位置无效则忽略事件。

# 获取目标位置
target_index = self.file_list.indexAt(event.position().toPoint())
if not target_index.isValid():
    event.ignore()
    return

4. 获取选中项目

获取所有被选中的项目,而不是仅获取当前项目(currentItem),这样可以支持多选拖拽。

# 获取所有选中的项目(不只是 currentItem)
selected_items = self.file_list.selectedItems()
if not selected_items:
    event.ignore()
    return

5. 显示确认对话框

在移动前显示确认对话框,显示将要移动的项目数量和目标位置。

# 显示确认对话框
target_name = target_item.text(0) if target_item else "列表末尾"

reply = QMessageBox.question(
    self,
    "确认移动",
    f"确定要将项目: {len(selected_items)}个项目\n移动到: {target_name} 之前吗?",
    QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)

if reply != QMessageBox.StandardButton.Yes:
    event.ignore()
    return

6. 处理移动逻辑

根据拖拽动作类型处理移动逻辑:

  • 如果是移动动作(MoveAction),则克隆选中的项目并插入到目标位置
  • 如果目标位置无效,则将项目添加到末尾
if event.proposedAction() == Qt.MoveAction:
    print(f"移动 {len(selected_items)} 个项目")

    # 如果是移动到目标项目上(覆盖)
    if target_item:
        print(f"覆盖到 {target_item.text(0)}")

        # 获取目标位置的行号
        target_row = target_index.row()

        # 克隆所有选中的项目并插入到目标位置
        parent = self.file_list.invisibleRootItem()
        for i, item in enumerate(selected_items):
            new_item = item.clone()  # 复制项目
            parent.insertChild(target_row + i, new_item)
    else:
        # 直接移动所有项目到末尾
        parent = self.file_list.invisibleRootItem()
        for item in selected_items:
            parent.addChild(item.clone())  # 或者直接移动 item

    event.accept()  # 接受事件,阻止默认行为
else:
    event.ignore()

7. 跟踪项目移动

连接rowsAboutToBeRemoved信号到自定义槽函数,用于跟踪被移动的项目。

# 连接信号
self.file_list.model().rowsAboutToBeRemoved.connect(self.on_rows_about_to_be_removed)

# 跟踪变量
self.moved_items_cache = []

在项目被移除前,缓存这些项目的文本内容,可用于后续的操作。

def on_rows_about_to_be_removed(self, parent: QModelIndex, start: int, end: int):
    """在项目被移除前缓存被移动的项目"""
    if not parent.isValid():  # 只处理顶级项目
        self.moved_items_cache = []
        for i in range(start, end + 1):
            item = self.file_list.topLevelItem(i)
            if item:
                self.moved_items_cache.append(item.text(0))

    print(self.moved_items_cache)
    print("移动至位置start",start,end)

END 😃

posted @ 2025-10-22 18:12  Rrea  阅读(2)  评论(0)    收藏  举报