第十九节_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基本窗口控件深度补充_剪贴板与拖曳功能

相关推荐
老白干4 小时前
jjwt 0.9.1 在 JDK 11+ 上的两个“坑”与完整解决方案
java·python·log4j
笨鸟先飞,勤能补拙11 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
长和信泰光伏储能12 小时前
京津冀光伏发电:绿色能源的未来之路
python·能源
浦信仿真大讲堂12 小时前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
Gu Gu Study13 小时前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
卷无止境13 小时前
写代码这件事,到底该讲究点什么?
后端·python
卷无止境13 小时前
循环复杂度到底在算什么,Python 代码怎么才能写得让人一看就懂
后端·python
lpfasd12313 小时前
MediaCrawler 项目深度分析
chrome·python·chrome devtools
Dxy123931021614 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
bamb0014 小时前
一个项目带你入门AI应用开发01
python