文章目录
前言
本节接上篇内容进行 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基本窗口控件深度补充_剪贴板与拖曳功能