效果
![]()
直接上代码
from PySide6.QtWidgets import (
QGridLayout,
QGroupBox,
QHBoxLayout,
QVBoxLayout,
QApplication,
QWidget,
QLabel,
QScrollArea,
)
from PySide6.QtCore import Signal
from PySide6.QtCore import Qt, QSize, Signal, QMimeData, QPoint, QRect, QUrl
from PySide6.QtGui import QPixmap, QPainter, QImage, QDrag
import subprocess
import pathlib
import fitz #pip install PyMuPdf
import weakref
import qdarktheme
class PDFInfo:
def __init__(self, path: pathlib.Path):
self.path: pathlib.Path = path
self.pixmap: QPixmap = None # type: ignore
self.page_count: int = 0
def load_pixmap(self):
if not self.pixmap:
doc = fitz.open(self.path)
self.page_count = len(doc)
page = doc.load_page(0)
pm = page.get_pixmap() # type: ignore
imgfmt = (
QImage.Format.Format_RGBA8888
if pm.alpha
else QImage.Format.Format_RGB888
)
self.pixmap = QPixmap.fromImage(
QImage(pm.samples, pm.width, pm.height, pm.stride, imgfmt)
)
return self.pixmap
def word(self, ind: int):
return f"{ind}: 共{self.page_count}页"
class card(QGroupBox):
doubleClicked = Signal(int)
cardDropped = Signal(int, int) # 参数1:源card索引, 参数2:目标card索引
def __init__(self, parent):
super().__init__(parent)
self.pp = weakref.ref(parent)
self.pdf_info: PDFInfo = None # type: ignore
self.setAcceptDrops(True)
def supportedDropActions(self):
return Qt.DropAction.CopyAction | Qt.DropAction.MoveAction
def setui(self):
self.lb = QLabel()
self.lb.setScaledContents(True)
hbox = QHBoxLayout()
self.setLayout(hbox)
hbox.addWidget(self.lb)
self.setFixedSize(QSize(200, 160))
return self
def setdata(self, pdf_info: PDFInfo):
self.pdf_info = pdf_info
if self.pdf_info.page_count > 1:
self.setProperty("muti", True)
else:
self.setProperty("muti", False)
# self.setTitle(f"{pdf_info.path.stem}: 共{pdf_info.page_count}页")
def mouseDoubleClickEvent(self, event):
if self.pdf_info:
self.doubleClicked.emit(self.pdf_info.path)
super().mouseDoubleClickEvent(event)
def mousePressEvent(self, event):
self.dragStartPosition = event.position().toPoint()
super().mousePressEvent(event)
def mime_encode(self):
mime = QMimeData()
# 设置自定义格式的拖动数据(传入当前card在box中的索引)
main_window = self.pp()
if isinstance(main_window, box):
card_index = main_window.cards.index(self)
mime.setData("application/x-item", str(card_index).encode("utf-8"))
return mime
def mime_decode(self, mime: QMimeData):
if mime.hasFormat("application/x-item"):
return int(mime.data("application/x-item").data())
else:
return None
def mouseMoveEvent(self, event):
if not (event.buttons() & Qt.MouseButton.LeftButton):
return
offset = (event.position().toPoint() - self.dragStartPosition).manhattanLength()
if offset < QApplication.startDragDistance():
# 记录当前card的索引
mime = self.mime_encode()
drag = QDrag(self)
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
# 设置拖动预览图像
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.setHotSpot(event.position().toPoint())
drag.exec()
def dragEnterEvent(self, event):
mime = event.mimeData()
if mime.hasFormat("application/x-item"):
event.acceptProposedAction()
def dropEvent(self, event):
s = self.mime_decode(event.mimeData())
if s is not None:
parent = self.pp()
if parent and isinstance(parent, box):
target_index = parent.cards.index(self)
if s != target_index and s != target_index - 1:
self.cardDropped.emit(s, target_index)
event.accept()
def paintEvent(self, event):
super().paintEvent(event)
if not self.pdf_info:
return
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# 设置字体
font = painter.font()
font.setPointSize(8)
painter.setFont(font)
# 设置文本颜色
cl = painter.background().color()
painter.setPen(cl)
painter.setBrush(cl)
# 获取文本内容
text = self.pdf_info.path.name
# 计算文本绘制位置
text_rect = painter.fontMetrics().boundingRect(text)
if text_rect.width() > self.width() - 20:
text = text[:18] + "..."
text_rect = painter.fontMetrics().boundingRect(text)
x = (self.width() - text_rect.width()) // 2
y = self.height() - 5
d = 3
r2 = QRect(
x - d, y - 10 - d, text_rect.width() + 2 * d, text_rect.height() + 2 * d
)
painter.drawRect(r2)
painter.setPen(Qt.GlobalColor.black)
painter.setBrush(Qt.BrushStyle.NoBrush)
# 绘制文本
painter.drawText(x, y, text)
class EndLabel(QLabel):
cardDropped = Signal(int)
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("background-color: lightgray; border: 1px solid gray;")
self.setFixedSize(200, 150)
self.setAcceptDrops(True)
def dragEnterEvent(self, event):
mime = event.mimeData()
if mime.hasFormat("application/x-item"):
event.acceptProposedAction()
def dropEvent(self, event):
mime = event.mimeData()
if mime.hasFormat("application/x-item"):
source_index = int(mime.data("application/x-item").data())
self.cardDropped.emit(source_index)
event.accept()
class c1(QLabel):
sg_dropped = Signal(list)
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setFixedSize(200, 100)
self.setProperty("level", "funcCard1")
self.setAcceptDrops(True)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event):
urls = event.mimeData().urls()
if urls:
ret = []
for file in urls:
file_path = file.toLocalFile()
if file_path.lower().endswith(".pdf"):
ret.append(pathlib.Path(file_path).absolute())
if ret:
self.sg_dropped.emit(ret)
event.accept()
class c2(QLabel):
sg_dropped = Signal(int)
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setFixedSize(200, 100)
self.setProperty("level", "funcCard2")
self.setAcceptDrops(True)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
def dragEnterEvent(self, event):
mime = event.mimeData()
if mime.hasFormat("application/x-item"):
event.acceptProposedAction()
def dropEvent(self, event):
mime = event.mimeData()
if mime.hasFormat("application/x-item"):
source_index = int(mime.data("application/x-item").data())
self.sg_dropped.emit(source_index)
event.accept()
class c3(QLabel):
sg_dropped = Signal(int)
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setFixedSize(200, 100)
self.setProperty("level", "funcCard3")
self.setAcceptDrops(True)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.temp_pdf = None
self.dragStartPosition = QPoint()
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.dragStartPosition = event.position().toPoint()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if not (event.buttons() & Qt.MouseButton.LeftButton):
return
# 检查拖动距离是否足够
if (
event.position().toPoint() - self.dragStartPosition
).manhattanLength() < QApplication.startDragDistance():
return
try:
# 合并所有PDF文件
output_path = pathlib.Path("temp.pdf").absolute()
doc = fitz.open()
parent = self.parent()
if isinstance(parent, mainw) and parent.w1.cards:
for card in parent.w1.cards:
if hasattr(card, "pdf_info"):
try:
src_doc = fitz.open(card.pdf_info.path)
doc.insert_pdf(src_doc)
src_doc.close()
except Exception as e:
print(f"Error processing {card.pdf_info.path}: {e}")
if len(doc) > 0:
doc.save(output_path)
print(f"PDF合并完成,保存到: {output_path}")
doc.close()
self.temp_pdf = output_path
# 创建拖拽操作
drag = QDrag(self)
mime = QMimeData()
urls = [QUrl.fromLocalFile(str(output_path))]
print(urls)
mime.setUrls(urls)
drag.setMimeData(mime)
# 设置拖拽预览图像
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.setHotSpot(event.position().toPoint())
# 执行拖拽操作
result = drag.exec(
Qt.DropAction.CopyAction | Qt.DropAction.MoveAction
)
print(f"拖拽操作结果: {result}")
else:
print("没有可合并的PDF文件")
doc.close()
except Exception as e:
print(f"拖拽操作出错: {e}")
class box(QScrollArea):
def __init__(self, parent=None):
super().__init__(parent)
# self.setAcceptDrops(True)
self.cards: list[card] = []
# 创建容器widget和布局
self.container = QWidget()
self.grid = QGridLayout(self.container)
self.container.setLayout(self.grid)
# 设置滚动区域属性
self.setWidgetResizable(True)
self.setWidget(self.container)
self.setMinimumSize(450, 350) # 设置最小窗口尺寸
# 添加始终显示在末尾的特殊EndLabel
self.end_label = EndLabel("拖到此处移动到最后", self.container)
self.end_label.cardDropped.connect(self.move_card_to_end)
def setui(self):
# self.load_pdfs(r"C:\Users\Administrator\Desktop\ZJ\5.6")
return self
def load_pdfs(self, folder_path):
pp = pathlib.Path(folder_path).absolute()
for i, pdf_file in enumerate(pp.glob("*.pdf")):
self.add_pdf_item(pdf_file, i)
def add_pdf_item(self, pdf_path, pos):
pdf_info = PDFInfo(pdf_path)
pdf_info.load_pixmap()
cd = card(self).setui()
cd.setdata(pdf_info)
cd.lb.setPixmap(pdf_info.load_pixmap())
cd.doubleClicked.connect(self.on_item_double_clicked)
cd.cardDropped.connect(self.move_card)
row = pos // (self.width() // 200)
col = pos % (self.width() // 200)
self.grid.addWidget(cd, row, col)
self.cards.append(cd)
def resizeEvent(self, event):
self._resize()
super().resizeEvent(event)
def _resize(self):
self.rearrange_items()
# 计算并设置高度: card高度*(行数+2)
cols = max(1, self.width() // 200)
rows = (len(self.cards) + cols - 1) // cols + 2 # 行数+2
self.container.setFixedHeight(150 * rows)
def rearrange_items(self):
cols = max(1, self.width() // 200)
# 清除布局
while self.grid.count():
item = self.grid.takeAt(0)
if item.widget():
item.widget().setParent(None)
# 添加所有普通card
cd: card
for i, cd in enumerate(self.cards):
row = i // cols
col = i % cols
self.grid.addWidget(cd, row, col)
cd.setTitle(cd.pdf_info.word(i + 1))
# 添加end_label到最后一行第一列
total_items = len(self.cards)
rows = (total_items + cols - 1) // cols
self.grid.addWidget(self.end_label, rows, 0)
def makepixmap(self, p: pathlib.Path):
pone = fitz.open(p).load_page(0)
pm = pone.get_pixmap() # type: ignore
imgfmt = (
QImage.Format.Format_RGBA8888 if pm.alpha else QImage.Format.Format_RGB888
)
pimg = QImage(pm.samples, pm.width, pm.height, pm.stride, imgfmt)
return QPixmap.fromImage(pimg)
def move_card_to_end(self, source_index):
if source_index != -1:
source_card = self.cards.pop(source_index)
self.cards.append(source_card)
self.rearrange_items()
def move_card(self, source_index, target_index):
if source_index == target_index:
return
# 从原位置移除源card
source_card = self.cards.pop(source_index)
# 在目标位置插入源card
if source_index < target_index:
target_index -= 1
self.cards.insert(target_index, source_card)
# 重新排列布局
self.rearrange_items()
def on_item_double_clicked(self, index):
print(f"Item {index} double clicked")
pinfo:PDFInfo=self.cards[index].pdf_info
cmd=['explorer',str(pinfo.path.absolute())]
subprocess.Popen(cmd)
class mainw(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("ZLPDF-PDF合并器")
ly = QVBoxLayout()
self.w1 = box().setui()
ly.addWidget(self.w1)
hb = QHBoxLayout()
self.c1 = c1("把要合并的PDF拖到我这里", self)
self.c2 = c2("把要去掉的PDF拖动到我这里", self)
self.c3 = c3("把合并后的文件从我这里拖走", self)
for i in [self.c1, self.c2, self.c3]:
hb.addWidget(i)
ly.addLayout(hb)
self.setLayout(ly)
self.c1.sg_dropped.connect(self.addpdfs)
self.c2.sg_dropped.connect(self.remove_pdf)
def addpdfs(self, ll: list[pathlib.Path]):
for pdf in ll:
self.w1.add_pdf_item(pdf, len(self.w1.cards))
self.w1._resize()
def remove_pdf(self, index: int):
"""移除指定索引的PDF卡片"""
if 0 <= index < len(self.w1.cards):
# 从布局中移除widget
item = self.w1.grid.takeAt(index)
if item.widget():
item.widget().deleteLater()
# 从cards列表中移除
self.w1.cards.pop(index)
# 重新排列剩余卡片
self.w1.rearrange_items()
sty = """
QLabel[level="funcCard1"]{border:3px solid orange;border-radius:10px;}
QLabel[level="funcCard2"]{border:3px solid red;border-radius:10px;}
QLabel[level="funcCard3"]{border:3px solid green;border-radius:10px;}
card{border:3px solid green;border-radius:6px;margin-bottom:5px;margin-top:8px;padding-top:-3px;}
card[muti='true']{border:3px solid orange;}
card::title{subcontrol-origin:margin;padding:0 3px;}
"""
app = QApplication()
qdarktheme.setup_theme("light")
mw = mainw()
mw.setStyleSheet(sty)
mw.show()
app.exec()