第十九节_PySide6基本窗口控件深度补充_剪贴板与拖曳功能(Drag 与 Drop) 下篇

文章目录


前言

本节接上篇内容进行 Drag 与 Drop 功能的介绍。


一、拖曳(Drag & Drop)基础概念

PySide6中拖曳(Drag & Drop)功能允许用户在应用程序内部或不同应用程序之间移动数据。它基于MIME(Multipurpose Internet Mail Extensions)类型系统来标识数据格式,并支持多种操作,如复制、移动和链接。

1.拖曳操作简介

拖曳操作通常涉及两个部分:拖曳源(Drag Source)和放置目标(Drop Target)。拖曳源是用户开始拖曳的部件,放置目标是用户释放鼠标以放置数据的部件。

在PySide6中,任何QWidget都可以作为拖曳源、放置目标或两者兼有。实现拖曳功能通常需要以下步骤:

1)设置部件为可接受放置(对于放置目标);

2)重写事件处理函数以处理拖曳和放置事件。

主要的事件处理函数包括:

① mousePressEvent:检测拖曳开始;

② mouseMoveEvent:启动拖曳操作;

③ dragEnterEvent:当拖曳进入部件时调用,用于判断是否接受拖曳;

④ dragMoveEvent:拖曳在部件内移动时调用,用于更新拖曳的视觉反馈;

⑤ dropEvent:当在部件上释放拖曳时调用,用于处理放置的数据。

对于拖曳源,我们需要启动拖曳操作并设置MIME数据。对于放置目标,我们需要接受拖曳事件并处理MIME数据。

2.拖曳操作的核心组件

组件 作用 相关事件
拖曳源 (Drag Source) 开始拖曳操作的部件 mousePressEvent, mouseMoveEvent
拖放目标 (Drop Target) 接收拖放数据的部件 dragEnterEvent, dragMoveEvent, dropEvent
MIME 数据 传输的数据格式容器 QMimeData
拖曳对象 管理拖曳操作 QDrag

3.拖曳操作的基本流程

二、基础应用

在上篇中的 button_QMime.py 文件中继续添加 QLabel 子类和 QTextEdit 子类:

python 复制代码
class DraggableLabel(QLabel):
    """可拖曳的标签"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet("""
            QLabel {
                border: 2px solid #4CAF50;
                border-radius: 8px;
                padding: 15px;
                margin: 10px;
                background-color: #E8F5E9;
                font-size: 14px;
                font-weight: bold;
                min-width: 100px;
                min-height: 50px;
            }
            QLabel:hover {
                background-color: #C8E6C9;
                border-color: #2E7D32;
                color: #1B5E20;
            }
        """)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)

    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if event.button() == Qt.MouseButton.LeftButton:
            self.drag_start_position = event.position().toPoint()

    def mouseMoveEvent(self, event):
        """鼠标移动事件,开始拖曳"""
        if not (event.buttons() & Qt.MouseButton.LeftButton):
            return

        # 检查是否达到拖曳阈值(避免误操作)
        if (event.position().toPoint() - self.drag_start_position).manhattanLength() < 10:
            return

        # 创建拖曳对象
        drag = QDrag(self)
        mime_data = QMimeData()

        # 设置要传输的数据
        mime_data.setText(self.text())
        mime_data.setData("application/custom-data",
                          QByteArray(f"Custom: {self.text()}".encode()))

        # 设置拖曳的预览图像
        pixmap = self.grab()
        drag.setPixmap(pixmap)

        # 设置光标热点位置
        drag.setHotSpot(event.position().toPoint() - self.rect().topLeft())

        # 执行拖曳操作
        drag.setMimeData(mime_data)
        action = drag.exec(Qt.DropAction.MoveAction | Qt.DropAction.CopyAction)

        # 根据拖曳结果执行相应操作
        if action == Qt.DropAction.MoveAction:
            self.setText("已移动")
        elif action == Qt.DropAction.CopyAction:
            self.setText(f"{self.text()} (复制)")


class DropTextEdit(QTextEdit):
    """可接收拖放文本的文本编辑框"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAcceptDrops(True)
        self.setStyleSheet("""
            QTextEdit {
                border: 2px dashed #9E9E9E;
                border-radius: 8px;
                padding: 10px;
                font-size: 12px;
                min-height: 100px;
            }
            QTextEdit[dropActive="true"] {
                border: 2px solid #2196F3;
                background-color: #E3F2FD;
            }
        """)

    def dragEnterEvent(self, event):
        """拖曳进入事件"""
        if event.mimeData().hasText():
            event.acceptProposedAction()
            self.setProperty("dropActive", True)
            self.style().polish(self)

    def dragLeaveEvent(self, event):
        """拖曳离开事件"""
        self.setProperty("dropActive", False)
        self.style().polish(self)

    def dropEvent(self, event):
        """释放拖曳事件"""
        if event.mimeData().hasText():
            text = event.mimeData().text()
            self.append(f"【拖放文本】: {text}")

            # 处理自定义数据
            if event.mimeData().hasFormat("application/custom-data"):
                custom_data = bytes(event.mimeData().data("application/custom-data")).decode()
                self.append(f"【自定义数据】: {custom_data}")

            event.acceptProposedAction()

        self.setProperty("dropActive", False)
        self.style().polish(self)

在 UI 界面对控件进行提升操作:


① 按照拖曳操作的基本流程,在 DraggableLabel 子类中重写 mousePressEvent 鼠标按下事件和mouseMoveEvent 鼠标移动事件;

② 创建拖曳对象 drag ,并 QMimeData() 类通过存储数据,拖曳过程中会有一些默认图像随着鼠标移动(常见的有一个拖曳的"+"或透明的快捷方式),这些图像会根据 QMimeData 中的数据类型显示不同的效果,也可以使用 setPixmap() 函数设置其他图片的效果。可以使用 setHotSpot() 函数设置鼠标指针相对于控件左上角的位置;

③ 执行拖曳操作,drag.exec()函数会阻断当前事件运行(不会阻断主程序),执行拖曳的其他事件,等待拖曳操作完成之后才会继续执行当前事件;

④ 在 DropTextEdit 子类_可接收拖放文本的文本编辑框中对 dragEnterEvent(拖曳进入事件)、dragLeaveEvent(拖曳离开事件)、dropEvent(释放拖曳事件)进行重写,大体逻辑在上篇中也有介绍,这里不在赘述,需要注意的是通过 self.setAcceptDrops(True) 来启用窗口或部件接受拖放操作。

三、高级应用

在 基础应用 和 上篇文章 中主要举例 QLabel、文本这些的拖拽操作,再介绍一个 图像 的拖拽操作,这个在实际应用中很有用。

collage_cell.py 文件代码:

python 复制代码
class CollageCell(QLabel):
    """拼贴单元格"""

    def __init__(self, index, parent=None):
        super().__init__(parent)
        self.index = index
        self.pixmap = None
        self.placeholder_color = self.generate_placeholder_color()

        self.setAcceptDrops(True)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setMinimumSize(150, 150)

        # 设置样式
        self.setStyleSheet(f"""
            QLabel {{
                border: 2px dashed {self.placeholder_color.darker(150).name()};
                border-radius: 8px;
                background-color: {self.placeholder_color.name()};
                color: {self.placeholder_color.darker().name()};
                font-weight: bold;
            }}
            QLabel:hover {{
                border: 2px solid #2196F3;
                background-color: {self.placeholder_color.lighter(120).name()};
            }}
        """)
        self.setText(f"单元格 {index}\n拖放图像到这里")

    def generate_placeholder_color(self):
        """生成随机的占位符颜色"""
        colors = [
            QColor(255, 235, 238),  # 浅红
            QColor(255, 243, 224),  # 浅橙
            QColor(255, 249, 196),  # 浅黄
            QColor(240, 255, 234),  # 浅绿
            QColor(232, 245, 253),  # 浅蓝
            QColor(243, 229, 245),  # 浅紫
            QColor(252, 228, 236),  # 浅粉
        ]
        return random.choice(colors)

    def dragEnterEvent(self, event):
        """拖拽进入事件"""
        if event.mimeData().hasUrls() or event.mimeData().hasImage():
            event.acceptProposedAction()
            self.setStyleSheet(f"""
                QLabel {{
                    border: 3px solid #4CAF50;
                    border-radius: 8px;
                    background-color: {self.placeholder_color.lighter(130).name()};
                    color: {self.placeholder_color.darker().name()};
                }}
            """)

    def dragLeaveEvent(self, event):
        """拖拽离开事件"""
        self.update_style()

    def dropEvent(self, event):
        """放置事件"""
        mime_data = event.mimeData()
        image_loaded = False

        # 从URL加载
        if mime_data.hasUrls():
            for url in mime_data.urls():
                if url.isLocalFile():
                    file_path = url.toLocalFile()
                    if self.is_image_file(file_path):
                        pixmap = QPixmap(file_path)
                        if not pixmap.isNull():
                            self.set_image(pixmap)
                            image_loaded = True
                            break

        # 从图像数据加载
        if not image_loaded and mime_data.hasImage():
            image = mime_data.imageData()
            if isinstance(image, QPixmap):
                self.set_image(image)
            elif isinstance(image, QImage):
                self.set_image(QPixmap.fromImage(image))

        self.update_style()

    def is_image_file(self, file_path):
        """检查是否为图像文件"""
        extensions = ['.png', '.jpg', '.jpeg', '.bmp', '.gif', '.webp']
        return os.path.splitext(file_path)[1].lower() in extensions

    def set_image(self, pixmap):
        """设置图像"""
        self.pixmap = pixmap

        # 缩放图像以适应单元格
        scaled_pixmap = pixmap.scaled(
            self.size() - QSize(10, 10),
            Qt.AspectRatioMode.KeepAspectRatio,
            Qt.TransformationMode.SmoothTransformation
        )

        self.setPixmap(scaled_pixmap)
        self.setText("")

    def clear_image(self):
        """清除图像"""
        self.pixmap = None
        self.setPixmap(QPixmap())
        self.setText(f"单元格 {self.index}\n拖放图像到这里")
        self.update_style()

    def update_style(self):
        """更新样式"""
        if self.pixmap:
            self.setStyleSheet("""
                QLabel {
                    border: 2px solid #757575;
                    border-radius: 8px;
                    background-color: white;
                }
            """)
        else:
            self.setStyleSheet(f"""
                QLabel {{
                    border: 2px dashed {self.placeholder_color.darker(150).name()};
                    border-radius: 8px;
                    background-color: {self.placeholder_color.name()};
                    color: {self.placeholder_color.darker().name()};
                    font-weight: bold;
                }}
            """)

    def get_image_data(self):
        """获取图像数据"""
        if self.pixmap and not self.pixmap.isNull():
            return self.pixmap
        return None

class DraggableThumbnail(QLabel):
    """可拖拽的缩略图"""
    def __init__(self, pixmap, image_path="", parent=None):
        super().__init__(parent)
        self.original_pixmap = pixmap
        self.image_path = image_path

        self.setFixedSize(100, 100)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)

        # 设置样式
        self.setStyleSheet("""
            QLabel {
                border: 2px solid #E0E0E0;
                border-radius: 8px;
                padding: 5px;
                background-color: white;
            }
            QLabel:hover {
                border-color: #2196F3;
                background-color: #F0F7FF;
            }
        """)

        # 显示缩略图
        if not pixmap.isNull():
            scaled_pixmap = pixmap.scaled(
                90, 90, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation
            )
            self.setPixmap(scaled_pixmap)
        else:
            self.setText("无图像")

    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if event.button() == Qt.MouseButton.LeftButton:
            self.drag_start_position = event.position().toPoint()

    def mouseMoveEvent(self, event):
        """鼠标移动事件"""
        if not (event.buttons() & Qt.MouseButton.LeftButton):
            return

        if (event.position().toPoint() - self.drag_start_position).manhattanLength() < 10:
            return

        # 创建拖拽对象
        drag = QDrag(self)
        mime_data = QMimeData()

        # 设置数据
        if self.image_path:
            url = QUrl.fromLocalFile(self.image_path)
            mime_data.setUrls([url])

        # 设置图像数据
        if not self.original_pixmap.isNull():
            mime_data.setImageData(self.original_pixmap)

        # 创建拖拽预览
        preview_pixmap = self.original_pixmap.scaled(
            80, 80, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation
        )

        drag.setPixmap(preview_pixmap)
        drag.setHotSpot(QPoint(preview_pixmap.width() // 2, preview_pixmap.height() // 2))
        drag.setMimeData(mime_data)

        drag.exec(Qt.DropAction.CopyAction)

主程序代码:

python 复制代码
		# 右侧:拼贴区域初始化
        # 拼贴配置
        self.grid_rows = 3
        self.grid_cols = 3
        self.cell_size = 150
        self.spacing = 10
        self.background_color = QColor(240, 240, 240)
        self.collage_image = None

        # 对齐方式
        #self.ui.lab_Show.setAlignment(Qt.AlignmentFlag.AlignCenter)  # 图片在Label中居中显示

        # 拼贴网格容器
        self.ui.m_widget.setStyleSheet("background-color: #F5F5F5; border-radius: 10px;")

        # 创建新的网格布局
        self.grid_layout = QGridLayout(self.ui.m_widget)
        self.grid_layout.setSpacing(self.spacing)

        self.cells = []
        cell_index = 1

        for row in range(self.grid_rows):
            for col in range(self.grid_cols):
                cell = CollageCell(cell_index)
                self.grid_layout.addWidget(cell, row, col)
                self.cells.append(cell)
                cell_index += 1

        # 左侧:图像库初始化
        # QScrollArea - 区域滚动
        self.ui.library_scroll.setWidgetResizable(True)  # 关键:允许内容自动调整
        self.library_layout = QVBoxLayout(self.ui.content_widget)

        # 添加示例图像
        self.add_sample_images()

        # 将内容部件设置为滚动区域的部件
        self.ui.library_scroll.setWidget(self.ui.content_widget)

        #按钮槽函数
        self.ui.add_images_btn.clicked.connect(self.add_images_to_library)
        self.ui.clear_library_btn.clicked.connect(self.clear_library)

        self.ui.generate_btn.clicked.connect(self.generate_collage)
        self.ui.clear_all_btn.clicked.connect(self.clear_collage)
        self.ui.save_btn.clicked.connect(self.save_image)

    def add_sample_images(self):
        """添加示例图像"""
        # 创建一些示例图像
        colors = [
            QColor(255, 200, 200),  # 红色
            QColor(200, 255, 200),  # 绿色
            QColor(200, 200, 255),  # 蓝色
            QColor(255, 255, 200),  # 黄色
            QColor(255, 200, 255),  # 紫色
            QColor(200, 255, 255),  # 青色
        ]

        for i, color in enumerate(colors):
            # 创建图像
            pixmap = QPixmap(100, 100)
            pixmap.fill(Qt.GlobalColor.transparent)  #填充透明

            painter = QPainter(pixmap)
            painter.setRenderHint(QPainter.RenderHint.Antialiasing)

            # 绘制颜色块
            painter.setBrush(QBrush(color))
            painter.setPen(QPen(color.darker(), 2))
            painter.drawEllipse(10, 10, 80, 80)

            # 绘制编号
            painter.setPen(Qt.GlobalColor.black)
            painter.setFont(QFont("Arial", 16, QFont.Weight.Bold))
            painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, str(i + 1))

            painter.end()

            # 创建缩略图
            thumbnail = DraggableThumbnail(pixmap, f"示例图像_{i + 1}")
            self.library_layout.addWidget(thumbnail)

        self.library_layout.addStretch()

    def add_images_to_library(self):
        """添加图像到库"""
        file_paths, _ = QFileDialog.getOpenFileNames(
            self, caption="选择图像文件",
            dir=os.path.abspath('.') + '/Resources/image',
            filter="图像文件 (*.png *.jpg *.jpeg *.bmp *.gif *.webp);;所有文件 (*.*)"
        )

        for file_path in file_paths:
            pixmap = QPixmap(file_path)
            if not pixmap.isNull():
                thumbnail = DraggableThumbnail(pixmap, file_path)
                self.library_layout.insertWidget(self.library_layout.count() - 1, thumbnail)

        # 获取垂直滚动条
        scrollbar = self.ui.library_scroll.verticalScrollBar()

        # 确保内容已经完全加载和布局
        self.ui.content_widget.updateGeometry()
        QApplication.processEvents()

        # 设置滚动条到最大值(底部)显示最新添加的图像
        scrollbar.setValue(scrollbar.maximum())

    def clear_library(self):
        """清空图像库"""
        while self.library_layout.count() > 1:  # 保留最后的stretch
            item = self.library_layout.takeAt(0)
            if item.widget():
                item.widget().deleteLater()

        # 重新添加示例图像
        self.add_sample_images()

    def generate_collage(self):
        """生成拼贴图像"""
        # 收集所有非空单元格的图像
        cell_images = []
        for cell in self.cells:
            if cell.pixmap:
                cell_images.append(cell.pixmap)

        if not cell_images:
            QMessageBox.warning(self, "警告", "没有图像可以生成拼贴")
            return

        # 创建拼贴图像
        collage_width = self.grid_cols * self.cell_size + (self.grid_cols - 1) * self.spacing
        collage_height = self.grid_rows * self.cell_size + (self.grid_rows - 1) * self.spacing

        self.collage_image = QPixmap(collage_width, collage_height)
        self.collage_image.fill(self.background_color)

        painter = QPainter(self.collage_image)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # 绘制网格背景
        painter.setBrush(QBrush(self.background_color))
        painter.setPen(Qt.PenStyle.NoPen)
        painter.drawRect(self.collage_image.rect())

        # 绘制每个单元格
        cell_index = 0
        for row in range(self.grid_rows):
            for col in range(self.grid_cols):
                if cell_index < len(self.cells):
                    cell = self.cells[cell_index]

                    # 计算位置
                    x = col * (self.cell_size + self.spacing)
                    y = row * (self.cell_size + self.spacing)

                    if cell.pixmap:
                        # 绘制图像
                        scaled_pixmap = cell.pixmap.scaled(
                            self.cell_size, self.cell_size,
                            Qt.AspectRatioMode.KeepAspectRatio,
                            Qt.TransformationMode.SmoothTransformation
                        )

                        # 计算居中位置
                        offset_x = (self.cell_size - scaled_pixmap.width()) // 2
                        offset_y = (self.cell_size - scaled_pixmap.height()) // 2

                        painter.drawPixmap(x + offset_x, y + offset_y, scaled_pixmap)
                    else:
                        # 绘制占位符
                        painter.setBrush(QBrush(cell.placeholder_color))
                        painter.setPen(QPen(cell.placeholder_color.darker(150), 2))
                        painter.drawRoundedRect(x, y, self.cell_size, self.cell_size, 8, 8)

                        # 绘制单元格编号
                        painter.setPen(Qt.GlobalColor.gray)
                        painter.setFont(QFont("Arial", 20, QFont.Weight.Bold))
                        painter.drawText(
                            x, y, self.cell_size, self.cell_size,
                            Qt.AlignmentFlag.AlignCenter, str(cell.index)
                        )

                    cell_index += 1

        painter.end()

        # 显示生成的拼贴
        # 保持宽高比缩放
        scaled_pixmap = self.collage_image.scaled(self.ui.lab_Show.geometry().width(),
                                      self.ui.lab_Show.geometry().height(),
                                      Qt.AspectRatioMode.KeepAspectRatio,  # 保持宽高比
                                      Qt.TransformationMode.SmoothTransformation)  # 平滑变换
        self.ui.lab_Show.setPixmap(scaled_pixmap)

    def clear_collage(self):
        """清空拼贴"""
        for cell in self.cells:
            cell.clear_image()
        self.statusBar().showMessage("拼贴已清空", 2000)

    def save_image(self, pixmap):
        """保存图像"""
        file_path, _ = QFileDialog.getSaveFileName(
            self, "保存拼贴图像",
            "collage.png",
            "PNG文件 (*.png);;JPEG文件 (*.jpg *.jpeg);;BMP文件 (*.bmp)"
        )

        if file_path:
            if self.collage_image.save(file_path):
                self.statusBar().showMessage(f"图像已保存: {file_path}", 3000)
            else:
                QMessageBox.warning(self, "错误", "保存图像失败")

① collage_cell.py 文件代码中的 CollageCell 类主要是对单元格的生成(默认 3 x 3 网格),还有对拖拽进入事件的接收;其中的

DraggableThumbnail 类是对图像库 QScrollArea 控件中进行图像生成和添加的图像进行处理,并且允许对该控件里的图进行拖动操作;

② 在主程序中 先进行 右侧:拼贴区域初始化左侧:图像库初始化 ,最后对按钮进行槽函数绑定;

③ cell = CollageCell(cell_index)、self.grid_layout.addWidget(cell, row, col) 函数就是对 CollageCell 类的应用,通过 for 循环在左侧界面添加单元格;thumbnail = DraggableThumbnail(pixmap, f"示例图像_{i + 1}") 函数用来在右侧进行缩略图的创建;

④ 当进行图像添加的时候,若想一直显示最新的添加的图像时,通过 scrollbar = self.ui.library_scroll.verticalScrollBar() 函数来获取垂直滚动条,然后通过 self.ui.content_widget.updateGeometry()、QApplication.processEvents()函数来确保内容已经完全加载和布局,最后通过 scrollbar.setValue(scrollbar.maximum()) 函数来设置滚动条到最大值(底部)显示最新添加的图像;

⑤ 从代码中可以发现使用了 QScrollArea 类、QPainter类、QFileDialog类、QMessageBox类等,都是前面所学习过的内容,现在只是在其基础上进行融合,然后构造一个程序。

总结

Drag 和 Drop 是 Qt/PySide6 中强大的交互功能:

启用简单:setAcceptDrops(True) 加上几个事件处理
功能强大:支持文本、文件、图像、自定义数据等
高度可定制:可以控制拖拽图像、热点、动作类型等
跨平台:在 Windows、macOS、Linux 上都能正常工作
通过合理使用拖放功能,可以极大提升应用程序的易用性和用户体验。

本节源码路径为:PySide6基本窗口控件深度补充_剪贴板与拖曳功能

相关推荐
猿饵块2 小时前
python--锁
java·jvm·python
星辰落满衣2 小时前
股票实时交易数据之Python、Java等多种主流语言实例代码演示通过股票数据接口
java·开发语言·python
F_D_Z3 小时前
哈希表解Two Sum问题
python·算法·leetcode·哈希表
智算菩萨3 小时前
【实战】使用讯飞星火API和Python构建一套文本摘要UI程序
开发语言·python·ui
Groundwork Explorer3 小时前
异步框架+POLL混合方案应对ESP32 MPY多任务+TCP多连接
python·单片机
梦帮科技3 小时前
Scikit-learn特征工程实战:从数据清洗到提升模型20%准确率
人工智能·python·机器学习·数据挖掘·开源·极限编程
xqqxqxxq3 小时前
Java 集合框架之线性表(List)实现技术笔记
java·笔记·python
verbannung3 小时前
Python进阶: 元类与属性查找理解
python