效果
直接上代码
[code]from PySide6.QtWidgets import ( QGridLayout, QGroupBox, QHBoxLayout, QVBoxLayout, QApplication, QWidget, QLabel, QScrollArea,)from PySide6.QtCore import Signalfrom PySide6.QtCore import Qt, QSize, Signal, QMimeData, QPoint, QRect, QUrlfrom PySide6.QtGui import QPixmap, QPainter, QImage, QDragimport subprocessimport pathlibimport fitz #pip install PyMuPdfimport weakrefimport qdarkthemeclass 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" DF合并完成,保存到: {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 DFInfo=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 |