Drag an Drop Mime Format

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
py_lo
User
Beiträge: 61
Registriert: Freitag 20. März 2009, 09:12

Hallo Community,

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()
Antworten