ich habe ein Problem, was mich zur Verzweiflung bringt. Ich möchte per D&D Dateien empfangen können, egal woher.
Bei normalen Dateien aus dem Explorer z.B. funktioniert das ganze auch wunderbar.
Nun ist die Herausforderung auch z.B. PDF aus Programmen wie Outlook zu bekommen.
Hier stoße ich an meine Grenzen - ich weiß es ist ein Mime-Format, die Datei wird auch gespeichert allerdings immer mit Größe 0.
Ich komme nicht an die Daten bzw. weiß nicht was ich noch versuchen soll.
Vielleicht kann mir ja jmd. einen guten Tipp geben?
Hier einmal das komplette Script:
Code: Alles auswählen
import sys
import os
import shutil
import tempfile
import struct
from datetime import datetime
from pathlib import Path
from typing import List
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from PyQt6.QtGui import *
class FileItem:
"""Klasse zur Repräsentation einer Datei in der Liste"""
def __init__(self, temp_path: Path):
self.temp_path = temp_path
self.file_name = temp_path.name
try:
self.file_size = temp_path.stat().st_size if temp_path.exists() else 0
except:
self.file_size = 0
self.file_type = temp_path.suffix.lower().lstrip('.') or "DATEI"
self.added_time = datetime.now()
@property
def size_mb(self) -> str:
"""Dateigröße in MB formatiert"""
if self.file_size > 0:
return f"{self.file_size / (1024*1024):.2f} MB"
return "0.00 MB"
class FileTableModel(QAbstractTableModel):
"""TableModel für die Dateiliste mit Sortierfunktion"""
def __init__(self):
super().__init__()
self.file_items: List[FileItem] = []
self.headers = ["Name", "Größe", "Typ"]
def rowCount(self, parent=QModelIndex()):
return len(self.file_items)
def columnCount(self, parent=QModelIndex()):
return len(self.headers)
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if not index.isValid() or index.row() >= len(self.file_items):
return None
item = self.file_items[index.row()]
if role == Qt.ItemDataRole.DisplayRole:
if index.column() == 0:
return item.file_name
elif index.column() == 1:
return item.size_mb
elif index.column() == 2:
return item.file_type.upper()
elif role == Qt.ItemDataRole.DecorationRole and index.column() == 0:
if item.temp_path.exists():
file_info = QFileInfo(str(item.temp_path))
icon_provider = QFileIconProvider()
return icon_provider.icon(file_info)
else:
return QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
elif role == Qt.ItemDataRole.UserRole:
return item
return None
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self.headers[section]
return None
def add_file(self, file_item: FileItem):
self.beginInsertRows(QModelIndex(), len(self.file_items), len(self.file_items))
self.file_items.append(file_item)
self.endInsertRows()
def remove_file(self, index: int):
if 0 <= index < len(self.file_items):
self.beginRemoveRows(QModelIndex(), index, index)
item = self.file_items.pop(index)
try:
if item.temp_path.exists():
item.temp_path.unlink(missing_ok=True)
except:
pass
self.endRemoveRows()
def clear_all(self):
self.beginResetModel()
for item in self.file_items:
try:
if item.temp_path.exists():
item.temp_path.unlink(missing_ok=True)
except:
pass
self.file_items.clear()
self.endResetModel()
def flags(self, index):
default_flags = super().flags(index)
if index.isValid():
return default_flags | Qt.ItemFlag.ItemIsDragEnabled
return default_flags
def mimeTypes(self):
return ['text/uri-list', 'application/x-qt-windows-mime;value="FileNameW"']
def mimeData(self, indexes):
mime_data = QMimeData()
urls = []
# Einzigartige Zeilen aus den Indexes extrahieren
rows = set()
file_items_to_drag = []
for index in indexes:
if index.isValid():
row = index.row()
if row not in rows:
rows.add(row)
item = self.file_items[row]
if item.temp_path.exists():
file_items_to_drag.append(item)
for item in file_items_to_drag:
url = QUrl.fromLocalFile(str(item.temp_path))
urls.append(url)
if urls:
mime_data.setUrls(urls)
try:
file_paths = [str(item.temp_path) for item in file_items_to_drag]
all_paths = '\0'.join(file_paths) + '\0\0'
data = all_paths.encode('utf-16le')
mime_data.setData('application/x-qt-windows-mime;value="FileNameW"', data)
except Exception:
pass
return mime_data
class FileDropWidget(QWidget):
"""Widget für Drag & Drop"""
def __init__(self, temp_dir: Path):
super().__init__()
self.temp_dir = temp_dir
self.setAcceptDrops(True)
self.setMinimumHeight(200)
self.setStyleSheet("""
FileDropWidget {
border: 3px dashed #555;
border-radius: 10px;
background-color: #2d2d2d;
}
FileDropWidget:hover {
border-color: #4a9eff;
background-color: #353535;
}
""")
self.layout = QVBoxLayout(self)
self.layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.icon_label = QLabel()
self.icon_label.setPixmap(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_FileDialogNewFolder
).pixmap(64, 64)
)
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.text_label = QLabel("Dateien hierher ziehen\n(Von überall: Ordner, E-Mail, etc.)")
self.text_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.text_label.setStyleSheet("color: #aaa; font-size: 14px;")
self.layout.addWidget(self.icon_label)
self.layout.addWidget(self.text_label)
def dragEnterEvent(self, event):
mime_data = event.mimeData()
accepted = (mime_data.hasUrls() or
mime_data.hasFormat("FileGroupDescriptorW") or
mime_data.hasFormat("FileContents") or
'x-qt-windows-mime' in ' '.join(mime_data.formats()))
if accepted:
event.acceptProposedAction()
self.setStyleSheet("""
FileDropWidget {
border: 3px dashed #4a9eff;
border-radius: 10px;
background-color: #353535;
}
""")
else:
event.ignore()
def dragLeaveEvent(self, event):
self.setStyleSheet("""
FileDropWidget {
border: 3px dashed #555;
border-radius: 10px;
background-color: #2d2d2d;
}
FileDropWidget:hover {
border-color: #4a9eff;
background-color: #353535;
}
""")
def dropEvent(self, event):
self.setStyleSheet("""
FileDropWidget {
border: 3px dashed #555;
border-radius: 10px;
background-color: #2d2d2d;
}
""")
mime_data = event.mimeData()
file_paths = self.extract_files_from_mime(mime_data)
if file_paths:
event.acceptProposedAction()
self.files_dropped.emit(file_paths)
else:
event.ignore()
def extract_files_from_mime(self, mime_data):
file_paths = []
if mime_data.hasUrls():
for url in mime_data.urls():
if url.isLocalFile():
file_path = Path(url.toLocalFile())
if file_path.exists():
file_paths.append(file_path)
if not file_paths and mime_data.hasFormat("FileGroupDescriptorW"):
file_paths = self.extract_Mime_files(mime_data)
return file_paths
def extract_Mime_files(self, mime_data):
file_paths = []
try:
descriptor_data = mime_data.data("FileGroupDescriptorW").data()
if not descriptor_data or len(descriptor_data) < 4:
return []
cItems = struct.unpack('<I', descriptor_data[:4])[0]
offset = 4
for i in range(cItems):
if offset + 592 > len(descriptor_data):
break
size_low_offset = offset + 60
size_high_offset = offset + 64
if size_high_offset + 4 <= len(descriptor_data):
size_low = struct.unpack('<I', descriptor_data[size_low_offset:size_low_offset+4])[0]
size_high = struct.unpack('<I', descriptor_data[size_high_offset:size_high_offset+4])[0]
file_size = (size_high << 32) | size_low
else:
file_size = 0
filename_offset = offset + 68
filename_end = filename_offset + 520
filename = None
if filename_end <= len(descriptor_data):
filename_bytes = descriptor_data[filename_offset:filename_end]
start_pos = 0
while start_pos < len(filename_bytes) and filename_bytes[start_pos] == 0:
start_pos += 1
if start_pos < len(filename_bytes):
end_pos = start_pos
while end_pos + 1 < len(filename_bytes):
if filename_bytes[end_pos] == 0 and filename_bytes[end_pos + 1] == 0:
break
end_pos += 2
string_bytes = filename_bytes[start_pos:end_pos]
try:
if len(string_bytes) % 2 == 1:
string_bytes = string_bytes[:-1]
filename = string_bytes.decode('utf-16le', errors='ignore')
except:
pass
if not filename or not filename.strip():
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if 31000000 <= file_size <= 32000000:
filename = f"File_{timestamp}.pdf"
else:
filename = f"Mime_attachment_{timestamp}_{i+1}.dat"
else:
if '\\' in filename:
parts = filename.split('\\')
for part in reversed(parts):
if part and '.' in part:
filename = part
break
file_data = self.extract_file_content(mime_data, i, file_size)
if file_data and len(file_data) > 0:
safe_filename = self.sanitize_filename(filename)
if not safe_filename or '.' not in safe_filename:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_filename = f"File_{timestamp}.pdf"
# Im eigenen Temp-Ordner speichern
temp_path = self.temp_dir / safe_filename
try:
# Überschreiben bei doppelten Namen
with open(temp_path, 'wb') as f:
f.write(file_data)
if temp_path.exists() and temp_path.stat().st_size > 0:
actual_size = temp_path.stat().st_size
if actual_size > 100:
file_paths.append(temp_path)
else:
temp_path.unlink(missing_ok=True)
except Exception as e:
print(f"Fehler beim Speichern der Mime-Datei: {e}")
offset += 592
except Exception as e:
print(f"Fehler beim Extrahieren von Mime-Dateien: {e}")
return file_paths
def extract_file_content(self, mime_data, index, expected_size):
try:
formats_to_try = [
"FileContents",
'application/x-qt-windows-mime;value="FileContents"',
f'application/x-qt-windows-mime;value="FileContents_{index}"',
f'FileContents_{index}',
]
for fmt in formats_to_try:
if mime_data.hasFormat(fmt):
ba = mime_data.data(fmt)
if ba:
data = ba.data()
if data and len(data) > 100:
if expected_size == 0 or abs(len(data) - expected_size) < 1000:
return data
for fmt in mime_data.formats():
if fmt not in formats_to_try and 'content' not in fmt.lower():
ba = mime_data.data(fmt)
if ba:
data = ba.data()
if data and len(data) > 100:
return data
return None
except Exception:
return None
def sanitize_filename(self, filename):
if not filename:
return ""
filename = os.path.basename(filename)
filename = ''.join(char for char in filename if ord(char) >= 32)
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
filename = filename.replace(char, '_')
filename = filename.strip('. ')
if len(filename) > 200:
name, ext = os.path.splitext(filename)
filename = name[:200-len(ext)] + ext
return filename
files_dropped = pyqtSignal(list)
class FileTableView(QTableView):
"""TableView mit Sortierfunktion und Drag & Drop"""
def __init__(self, parent=None):
super().__init__(parent)
self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly)
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.setAcceptDrops(True)
self.setDefaultDropAction(Qt.DropAction.CopyAction)
self.setSortingEnabled(True)
# Spalten konfigurieren
self.horizontalHeader().setStretchLastSection(True)
self.verticalHeader().setDefaultSectionSize(40)
# Setze Spaltenbreiten
self.setColumnWidth(0, 300) # Name
self.setColumnWidth(1, 100) # Größe
self.setColumnWidth(2, 80) # Typ
def startDrag(self, event):
# Erstelle Drag-Ereignis manuell
indexes = self.selectedIndexes()
if not indexes:
return
mime_data = self.model().mimeData(indexes)
if not mime_data:
return
drag = QDrag(self)
drag.setMimeData(mime_data)
# Erstelle ein Drag-Pixmap
rows = set(index.row() for index in indexes)
pixmap = self.createDragPixmap(len(rows))
if not pixmap.isNull():
drag.setPixmap(pixmap)
drag.setHotSpot(pixmap.rect().center())
drag.exec(Qt.DropAction.CopyAction)
def createDragPixmap(self, item_count):
pixmap = QPixmap(64, 64)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setBrush(QColor(42, 130, 218, 200))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(0, 0, 64, 64, 10, 10)
icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
icon.paint(painter, 12, 12, 40, 40)
if item_count > 1:
painter.setPen(Qt.GlobalColor.white)
painter.setBrush(QColor(255, 100, 100))
painter.drawEllipse(40, 0, 24, 24)
painter.setFont(QFont("Arial", 10, QFont.Weight.Bold))
count_text = str(min(item_count, 99))
painter.drawText(40, 0, 24, 24, Qt.AlignmentFlag.AlignCenter, count_text)
painter.end()
return pixmap
class FileClipboardWindow(QMainWindow):
"""Hauptfenster der Datei-Zwischenablage"""
def __init__(self):
super().__init__()
# Eigenen temporären Ordner erstellen
self.temp_dir = Path(tempfile.mkdtemp(prefix="file_clipboard_"))
print(f"Temp-Ordner: {self.temp_dir}")
self.init_ui()
self.apply_dark_theme()
def init_ui(self):
self.setWindowTitle("Datei-Zwischenablage")
self.setGeometry(100, 100, 800, 600)
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(15)
title_label = QLabel("📎 Datei-Zwischenablage")
title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #4a9eff;")
main_layout.addWidget(title_label)
desc_label = QLabel(
"Labetest"
)
desc_label.setStyleSheet("color: #aaa;")
desc_label.setWordWrap(True)
main_layout.addWidget(desc_label)
self.drop_widget = FileDropWidget(self.temp_dir)
self.drop_widget.files_dropped.connect(self.handle_dropped_files)
main_layout.addWidget(self.drop_widget)
main_layout.addWidget(QLabel("Gespeicherte Dateien:"))
self.tableView = FileTableView()
self.tableView.customContextMenuRequested.connect(self.show_context_menu)
self.model = FileTableModel()
self.tableView.setModel(self.model)
main_layout.addWidget(self.tableView)
button_layout = QHBoxLayout()
self.clear_btn = QPushButton("🗑️ Alles löschen")
self.clear_btn.clicked.connect(self.clear_all_files)
self.clear_btn.setStyleSheet("padding: 8px 16px;")
self.info_label = QLabel("0 Dateien")
self.info_label.setStyleSheet("color: #aaa;")
button_layout.addWidget(self.info_label)
button_layout.addStretch()
button_layout.addWidget(self.clear_btn)
main_layout.addLayout(button_layout)
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
def apply_dark_theme(self):
dark_palette = QPalette()
dark_palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
dark_palette.setColor(QPalette.ColorRole.WindowText, QColor(255, 255, 255))
dark_palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
dark_palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
dark_palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(25, 25, 25))
dark_palette.setColor(QPalette.ColorRole.ToolTipText, QColor(255, 255, 255))
dark_palette.setColor(QPalette.ColorRole.Text, QColor(255, 255, 255))
dark_palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
dark_palette.setColor(QPalette.ColorRole.ButtonText, QColor(255, 255, 255))
dark_palette.setColor(QPalette.ColorRole.BrightText, QColor(255, 0, 0))
dark_palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
dark_palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
dark_palette.setColor(QPalette.ColorRole.HighlightedText, QColor(35, 35, 35))
dark_palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor(127, 127, 127))
dark_palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(127, 127, 127))
dark_palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(127, 127, 127))
self.setPalette(dark_palette)
self.setStyleSheet("""
QMainWindow {
background-color: #353535;
}
QTableView {
background-color: #2d2d2d;
border: 1px solid #555;
border-radius: 5px;
gridline-color: #444;
}
QTableView::item {
padding: 5px;
border-bottom: 1px solid #444;
}
QTableView::item:hover {
background-color: #3a3a3a;
}
QTableView::item:selected {
background-color: #2a5caa;
}
QHeaderView::section {
background-color: #4a4a4a;
color: white;
padding: 5px;
border: 1px solid #555;
}
QPushButton {
background-color: #4a4a4a;
border: 1px solid #555;
border-radius: 5px;
padding: 5px 15px;
color: white;
}
QPushButton:hover {
background-color: #5a5a5a;
border-color: #666;
}
QPushButton:pressed {
background-color: #3a3a3a;
}
QStatusBar {
background-color: #2d2d2d;
color: #aaa;
}
""")
def handle_dropped_files(self, file_paths: List[Path]):
added_count = 0
for file_path in file_paths:
if file_path.exists():
if self.process_file(file_path):
added_count += 1
self.update_info()
if added_count > 0:
self.status_bar.showMessage(f"{added_count} Datei(en) hinzugefügt", 3000)
else:
self.status_bar.showMessage("Keine Dateien hinzugefügt", 3000)
def process_file(self, file_path: Path) -> bool:
try:
# Originalnamen verwenden
original_name = file_path.name
# Zielpfad im eigenen Temp-Ordner
temp_path = self.temp_dir / original_name
# Wenn es sich um eine Mime-Datei handelt, ist sie bereits im richtigen Ordner
if file_path.parent == self.temp_dir:
# Mime-Datei wurde bereits in unseren Temp-Ordner gespeichert
file_item = FileItem(file_path)
self.model.add_file(file_item)
return True
else:
# Normale Datei: Kopieren (überschreiben bei doppelten Namen)
shutil.copy2(file_path, temp_path)
# Item erstellen und zur Liste hinzufügen
file_item = FileItem(temp_path)
self.model.add_file(file_item)
return True
except Exception as e:
print(f"Fehler beim Verarbeiten der Datei: {e}")
return False
def show_context_menu(self, position):
menu = QMenu()
remove_action = menu.addAction("🗑️ Entfernen")
open_action = menu.addAction("📂 Im Explorer anzeigen")
menu.addSeparator()
copy_path_action = menu.addAction("📋 Pfad kopieren")
action = menu.exec(self.tableView.mapToGlobal(position))
if action == remove_action:
self.remove_selected_files()
elif action == open_action:
self.show_in_explorer()
elif action == copy_path_action:
self.copy_file_path()
def remove_selected_files(self):
indexes = self.tableView.selectionModel().selectedIndexes()
if indexes:
rows = set(index.row() for index in indexes)
for row in sorted(rows, reverse=True):
self.model.remove_file(row)
self.update_info()
def show_in_explorer(self):
indexes = self.tableView.selectionModel().selectedIndexes()
if indexes:
row = indexes[0].row()
item = self.model.file_items[row]
QDesktopServices.openUrl(QUrl.fromLocalFile(str(item.temp_path)))
def copy_file_path(self):
indexes = self.tableView.selectionModel().selectedIndexes()
if indexes:
row = indexes[0].row()
item = self.model.file_items[row]
clipboard = QApplication.clipboard()
clipboard.setText(str(item.temp_path))
self.status_bar.showMessage("Pfad kopiert", 2000)
def clear_all_files(self):
reply = QMessageBox.question(
self, "Bestätigung",
"Möchten Sie wirklich alle Dateien löschen?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.model.clear_all()
self.update_info()
self.status_bar.showMessage("Alle Dateien gelöscht", 3000)
def update_info(self):
count = self.model.rowCount()
total_size = sum(item.file_size for item in self.model.file_items if item.file_size)
total_size_mb = total_size / (1024 * 1024)
self.info_label.setText(f"{count} Dateien - {total_size_mb:.2f} MB gesamt")
def closeEvent(self, event):
try:
# Temporären Ordner löschen
shutil.rmtree(self.temp_dir, ignore_errors=True)
print(f"Temp-Ordner gelöscht: {self.temp_dir}")
except Exception as e:
print(f"Fehler beim Löschen des Temp-Ordners: {e}")
event.accept()
def main():
app = QApplication(sys.argv)
app.setApplicationName("Datei-Zwischenablage")
app.setOrganizationName("")
window = FileClipboardWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
