From 2f73c2cccf73658fc586704c55b9e498c4dbbb12 Mon Sep 17 00:00:00 2001 From: EricoDeMecha Date: Tue, 18 Mar 2025 15:17:09 +0300 Subject: [PATCH 1/6] (ch4): ported ch4/4-1 to qt quick --- .../4-1DragAndDropDemo/DragDropLabel.qml | 127 ++++++++++ .../4-1DragAndDropDemo/README.md | 229 ------------------ .../4-1DragAndDropDemo/dragdroplabel.py | 60 ----- .../4-1DragAndDropDemo/main.py | 85 ++++++- .../4-1DragAndDropDemo/main.qml | 116 +++++++++ .../4-1DragAndDropDemo/ui_widget.py | 66 ----- .../4-1DragAndDropDemo/widget.py | 54 ----- .../4-1DragAndDropDemo/widget.ui | 52 ---- 8 files changed, 323 insertions(+), 466 deletions(-) create mode 100644 Chap4-DragDropClipboard/4-1DragAndDropDemo/DragDropLabel.qml delete mode 100644 Chap4-DragDropClipboard/4-1DragAndDropDemo/README.md delete mode 100644 Chap4-DragDropClipboard/4-1DragAndDropDemo/dragdroplabel.py create mode 100644 Chap4-DragDropClipboard/4-1DragAndDropDemo/main.qml delete mode 100644 Chap4-DragDropClipboard/4-1DragAndDropDemo/ui_widget.py delete mode 100644 Chap4-DragDropClipboard/4-1DragAndDropDemo/widget.py delete mode 100644 Chap4-DragDropClipboard/4-1DragAndDropDemo/widget.ui diff --git a/Chap4-DragDropClipboard/4-1DragAndDropDemo/DragDropLabel.qml b/Chap4-DragDropClipboard/4-1DragAndDropDemo/DragDropLabel.qml new file mode 100644 index 0000000..a461388 --- /dev/null +++ b/Chap4-DragDropClipboard/4-1DragAndDropDemo/DragDropLabel.qml @@ -0,0 +1,127 @@ +import QtQuick +import QtQuick.Controls + +Rectangle { + id: root + + // Signal for Python backend + signal mimeDataReceived(string text, var urls, string html, bool hasImage) + signal dragStarted() + signal dragEnded() + + // Properties + width: 100 + height: 100 + color: dropArea.containsDrag ? palette.highlight : "#333333" + border.color: dropArea.containsDrag ? palette.highlight : palette.dark + border.width: 2 + radius: 6 + + // Colors + SystemPalette { + id: palette + } + + // Main label + Column { + anchors.centerIn: parent + spacing: 8 + visible: !droppedImage.visible + + Label { + id: dropLabel + anchors.horizontalCenter: parent.horizontalCenter + text: "DROP SPACE" + font.bold: true + font.pixelSize: 16 + color: palette.highlightedText + } + + Label { + id: instructionLabel + anchors.horizontalCenter: parent.horizontalCenter + text: "Drag files, text, or images here" + font.pixelSize: 12 + color: palette.highlightedText + width: parent.width - 20 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Image { + anchors.horizontalCenter: parent.horizontalCenter + source: "data:image/svg+xml," + width: 32 + height: 32 + } + } + + // Container for dropped images + Image { + id: droppedImage + anchors.fill: parent + anchors.margins: 5 + fillMode: Image.PreserveAspectFit + visible: false + } + + // Drop area + DropArea { + id: dropArea + anchors.fill: parent + + onEntered: { + dropLabel.text = "DROP YOUR DATA HERE" + instructionLabel.text = "Release to analyze content" + dragStarted() + } + + onExited: { + root.clear() + } + + onDropped: { + droppedImage.visible = false + + if (drop.hasImage) { + droppedImage.source = drop.image + droppedImage.visible = true + } else if (drop.hasText) { + dropLabel.text = drop.text + } else if (drop.hasUrls) { + var text = "" + for (var i = 0; i < drop.urls.length; i++) { + text += drop.urls[i] + "-----" + } + dropLabel.text = text + } else if (drop.hasHtml) { + dropLabel.text = "HTML content" + } else { + dropLabel.text = "Data cannot be displayed" + } + + // Signal the data to Python + var urlStrings = [] + if (drop.hasUrls) { + for (var j = 0; j < drop.urls.length; j++) { + urlStrings.push(drop.urls[j].toString()) + } + } + + mimeDataReceived( + drop.hasText ? drop.text : "", + urlStrings, + drop.hasHtml ? drop.html : "", + drop.hasImage + ) + } + } + + // Method to reset the label + function clear() { + dropLabel.text = "DROP SPACE" + instructionLabel.text = "Drag files, text, or images here" + droppedImage.visible = false + dragEnded() + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-1DragAndDropDemo/README.md b/Chap4-DragDropClipboard/4-1DragAndDropDemo/README.md deleted file mode 100644 index 9dbb3cf..0000000 --- a/Chap4-DragDropClipboard/4-1DragAndDropDemo/README.md +++ /dev/null @@ -1,229 +0,0 @@ -# Drag and Drop Demo in PySide6 - -This project demonstrates how to implement drag and drop functionality in PySide6. It allows you to drag text, images, HTML, and files onto a drop area and displays detailed information about the MIME data of the dragged items. - -## Project Overview - -This application illustrates: -1. Creating a custom QLabel that accepts drag and drop -2. Handling various drag and drop events -3. Processing different MIME types (text, HTML, images, URLs) -4. Displaying MIME data information in a structured way -5. Using Qt signals and slots for communication between widgets - -## Project Structure - -``` -project/ -├── main.py # Application entry point -├── widget.py # Main application widget -├── dragdroplabel.py # Custom label that accepts drops -├── ui_widget.py # Generated UI code from widget.ui -└── widget.ui # UI design file -``` - -## Key Components - -### DragDropLabel Widget - -The `DragDropLabel` class extends QLabel to implement drag and drop functionality: - -```python -class DragDropLabel(QLabel): - # Signal emitted when MIME data is received - mimeChanged = Signal(object) - - def __init__(self, parent=None): - super().__init__(parent) - self.setMinimumSize(100, 100) - self.setAlignment(Qt.AlignCenter) - self.setAcceptDrops(True) # Enable drop acceptance - self.setText("DROP SPACE") - self.setAutoFillBackground(True) -``` - -It overrides several event handlers to process drag and drop actions: - -- `dragEnterEvent`: Called when a drag operation enters the widget -- `dragMoveEvent`: Called when a drag operation moves within the widget -- `dragLeaveEvent`: Called when a drag operation leaves the widget -- `dropEvent`: Called when the content is dropped onto the widget - -### Main Widget - -The `Widget` class creates the main UI and processes MIME data: - -```python -class Widget(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_Widget() - self.ui.setupUi(self) - - # Create and add the drag drop label - self.dragDropLabel = DragDropLabel(self) - self.dragDropLabel.mimeChanged.connect(self.mimeChanged) - self.ui.labelLayout.addWidget(self.dragDropLabel) -``` - -It provides a method to process MIME data from drag and drop operations: - -```python -@Slot(object) -def mimeChanged(self, mimedata): - """Process and display MIME data information""" - self.ui.textEdit.clear() - if not mimedata: - return - - formats = mimedata.formats() - for i, format_name in enumerate(formats): - # Process different MIME types - # ... -``` - -## Drag and Drop Handling - -### Accepting Drops - -To make a widget accept drops, three things are needed: - -1. Enable drop acceptance with `setAcceptDrops(True)` -2. Override `dragEnterEvent` and call `event.acceptProposedAction()` -3. Override `dropEvent` to handle the dropped data - -```python -def dragEnterEvent(self, event: QDragEnterEvent): - self.setText("DROP YOUR DATA HERE") - self.setBackgroundRole(QPalette.Highlight) - event.acceptProposedAction() # This is essential - self.mimeChanged.emit(event.mimeData()) -``` - -### Processing MIME Data - -The application handles different types of MIME data: - -```python -def dropEvent(self, event: QDropEvent): - mimeData = event.mimeData() - - if mimeData.hasText(): - self.setText(mimeData.text()) - self.setTextFormat(Qt.PlainText) - elif mimeData.hasImage(): - self.setPixmap(mimeData.imageData()) - elif mimeData.hasHtml(): - self.setText(mimeData.html()) - self.setTextFormat(Qt.RichText) - elif mimeData.hasUrls(): - # Process URLs - # ... -``` - -## MIME Type Information - -The application displays detailed information about the MIME types of dragged data: - -```python -@Slot(object) -def mimeChanged(self, mimedata): - # ... - formats = mimedata.formats() - for i, format_name in enumerate(formats): - # Extract and format the data - data_string = f"{i} | Format: {format_name}\n | Data: {text}\n------------" - self.ui.textEdit.append(data_string) -``` - -Common MIME types include: -- `text/plain`: Plain text content -- `text/html`: HTML formatted content -- `text/uri-list`: List of file URLs -- `image/png`, `image/jpeg`: Image data - -## Visual Feedback - -The label provides visual feedback during drag operations: - -1. Changes text to "DROP YOUR DATA HERE" during drag -2. Changes background color to highlight color during drag -3. Resets to default state when drag leaves -4. Displays the dropped content after drop - -## Signal and Slot Connection - -The application uses Qt's signal and slot mechanism for communication: - -```python -# In Widget.__init__ -self.dragDropLabel.mimeChanged.connect(self.mimeChanged) -self.ui.clearButton.clicked.connect(self.on_clearButton_clicked) -``` - -## Running the Application - -1. Generate the UI Python file (if widget.ui changes): - ``` - pyside6-uic widget.ui -o ui_widget.py - ``` - -2. Ensure PySide6 is installed: - ``` - pip install PySide6 - ``` - -3. Run the application: - ``` - python main.py - ``` - -4. Use the application: - - Drag text from other applications onto the drop area - - Drag image files from a file browser - - Drag HTML content from a web browser - - Drag files or folders from a file browser - - View the MIME information in the text area - -## Implementation Notes - -### Byte Array Handling - -When processing raw MIME data, we need to carefully handle byte arrays: - -```python -data = mimedata.data(format_name) -for i in range(len(data)): - text += f"{ord(data[i:i+1])} " -``` - -### URL Processing - -When handling URLs, we need to extract the paths or complete URLs: - -```python -urlList = mimedata.urls() -for url in urlList: - text += url.toString() + " -/- " -``` - -### Signal Type Safety - -In PySide6, we can use the `@Slot(object)` decorator to specify the signal argument type: - -```python -@Slot(object) -def mimeChanged(self, mimedata): - # Process MIME data - # ... -``` - -## Extending the Project - -The project could be extended with: - -1. **File Drop Handling**: Add specific handling for files based on their extension -2. **Image Processing**: Add image processing for dropped images -3. **Drag Support**: Add support for dragging content from the application -4. **Multiple Drop Areas**: Add multiple drop targets for different types of content -5. **Custom MIME Types**: Add support for application-specific MIME types \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-1DragAndDropDemo/dragdroplabel.py b/Chap4-DragDropClipboard/4-1DragAndDropDemo/dragdroplabel.py deleted file mode 100644 index f04dd23..0000000 --- a/Chap4-DragDropClipboard/4-1DragAndDropDemo/dragdroplabel.py +++ /dev/null @@ -1,60 +0,0 @@ -from PySide6.QtWidgets import QLabel -from PySide6.QtCore import Qt, Signal -from PySide6.QtGui import QPalette, QDragEnterEvent, QDragMoveEvent, QDragLeaveEvent, QDropEvent - - -class DragDropLabel(QLabel): - """A QLabel that accepts drag and drop events""" - - # Define the signal with the same signature as in C++ - mimeChanged = Signal(object) - - def __init__(self, parent=None): - super().__init__(parent) - self.setMinimumSize(100, 100) - self.setAlignment(Qt.AlignCenter) - self.setAcceptDrops(True) - self.setText("DROP SPACE") - self.setAutoFillBackground(True) - - def dragEnterEvent(self, event: QDragEnterEvent): - """Handle drag enter events""" - self.setText("DROP YOUR DATA HERE") - self.setBackgroundRole(QPalette.Highlight) - event.acceptProposedAction() - self.mimeChanged.emit(event.mimeData()) - - def dragMoveEvent(self, event: QDragMoveEvent): - """Handle drag move events""" - event.acceptProposedAction() - - def dragLeaveEvent(self, event: QDragLeaveEvent): - """Handle drag leave events""" - self.clear() - - def dropEvent(self, event: QDropEvent): - """Handle drop events""" - mimeData = event.mimeData() - - if mimeData.hasText(): - self.setText(mimeData.text()) - self.setTextFormat(Qt.PlainText) - elif mimeData.hasImage(): - self.setPixmap(mimeData.imageData()) - elif mimeData.hasHtml(): - self.setText(mimeData.html()) - self.setTextFormat(Qt.RichText) - elif mimeData.hasUrls(): - urlList = mimeData.urls() - text = "" - for url in urlList: - text += url.path() + "-----" - self.setText(text) - else: - self.setText("Data cannot be displayed") - - def clear(self): - """Reset the label to its default state""" - self.setText("DROP SPACE") - self.setBackgroundRole(QPalette.Dark) - self.mimeChanged.emit(None) \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-1DragAndDropDemo/main.py b/Chap4-DragDropClipboard/4-1DragAndDropDemo/main.py index b51ec9c..1d9d4a8 100644 --- a/Chap4-DragDropClipboard/4-1DragAndDropDemo/main.py +++ b/Chap4-DragDropClipboard/4-1DragAndDropDemo/main.py @@ -1,12 +1,87 @@ import sys -from PySide6.QtWidgets import QApplication -from widget import Widget +from pathlib import Path +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtCore import QObject, Slot, Signal, Property, QUrl + +class DragDropController(QObject): + """Bridge class for handling MIME data in QML""" + + mimeDataChanged = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self._mimeEntries = [] + + @Slot(str, list, str, bool) + def processMimeData(self, text, urls, html, hasImage): + """Process mime data from QML""" + self._mimeEntries = [] + + # Process text/plain + if text: + self._mimeEntries.append({ + "format": "text/plain", + "data": text + }) + + # Process text/html + if html: + self._mimeEntries.append({ + "format": "text/html", + "data": html + }) + + # Process URLs + if urls and len(urls) > 0: + urlData = "" + for url in urls: + urlData += url + " -/- " + + self._mimeEntries.append({ + "format": "text/uri-list", + "data": urlData + }) + + # Mark the presence of image data + if hasImage: + self._mimeEntries.append({ + "format": "image/*", + "data": "[Image data not shown]" + }) + + self.mimeDataChanged.emit() + + @Slot() + def clearData(self): + """Clear mime data""" + self._mimeEntries = [] + self.mimeDataChanged.emit() + + @Property(list, notify=mimeDataChanged) + def mimeEntries(self): + return self._mimeEntries def main(): - app = QApplication(sys.argv) - window = Widget() - window.show() + app = QGuiApplication(sys.argv) + + # Create controller and expose to QML + controller = DragDropController() + + # Create QML engine + engine = QQmlApplicationEngine() + + # Expose controller to QML + engine.rootContext().setContextProperty("controller", controller) + + # Load QML file + qml_file = Path(__file__).resolve().parent / "main.qml" + engine.load(qml_file) + # Check if loading was successful + if not engine.rootObjects(): + sys.exit(-1) + return app.exec() if __name__ == "__main__": diff --git a/Chap4-DragDropClipboard/4-1DragAndDropDemo/main.qml b/Chap4-DragDropClipboard/4-1DragAndDropDemo/main.qml new file mode 100644 index 0000000..7d4b4f2 --- /dev/null +++ b/Chap4-DragDropClipboard/4-1DragAndDropDemo/main.qml @@ -0,0 +1,116 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts + +Window { + id: mainWindow + width: 400 + height: 300 + visible: true + title: "Drag & Drop Example" + + Component.onCompleted: { + // Set initial instructions + textEdit.text = "Information about dropped content will appear here.\n\n" + + "Try dragging and dropping:\n" + + "• Text from another application\n" + + "• Files from your file manager\n" + + "• Images from a browser\n" + + "• HTML content"; + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 11 + spacing: 6 + + // Drag and drop area + DragDropLabel { + id: dragDropLabel + Layout.fillWidth: true + Layout.preferredHeight: 150 + + onMimeDataReceived: function(text, urls, html, hasImage) { + controller.processMimeData(text, urls, html, hasImage) + } + + onDragStarted: { + // Processing started + } + + onDragEnded: { + // Processing ended + } + } + + // Text edit for displaying MIME data + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + TextArea { + id: textEdit + readOnly: true + wrapMode: TextEdit.WordWrap + textFormat: TextEdit.PlainText + + // Update when mimeData changes + Connections { + target: controller + function onMimeDataChanged() { + if (controller.mimeEntries.length === 0) { + textEdit.text = "Information about dropped content will appear here.\n\n" + + "Try dragging and dropping:\n" + + "• Text from another application\n" + + "• Files from your file manager\n" + + "• Images from a browser\n" + + "• HTML content"; + return; + } + + textEdit.clear() + + for (var i = 0; i < controller.mimeEntries.length; i++) { + var entry = controller.mimeEntries[i] + var format = entry.format + var data = entry.data + + var text = i + " | Format: " + format + + "\n | Data: " + data + + "\n------------" + + textEdit.append(text) + } + } + } + } + } + + // Clear button + RowLayout { + Layout.fillWidth: true + + Item { Layout.fillWidth: true } // Spacer + + Button { + id: clearButton + text: "Clear" + icon.source: "data:image/svg+xml," + onClicked: { + textEdit.clear() + dragDropLabel.clear() + controller.clearData() + + // Restore help text + textEdit.text = "Information about dropped content will appear here.\n\n" + + "Try dragging and dropping:\n" + + "• Text from another application\n" + + "• Files from your file manager\n" + + "• Images from a browser\n" + + "• HTML content"; + } + } + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-1DragAndDropDemo/ui_widget.py b/Chap4-DragDropClipboard/4-1DragAndDropDemo/ui_widget.py deleted file mode 100644 index 04c31b2..0000000 --- a/Chap4-DragDropClipboard/4-1DragAndDropDemo/ui_widget.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'widget.ui' -## -## Created by: Qt User Interface Compiler version 6.8.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QHBoxLayout, QPushButton, QSizePolicy, - QSpacerItem, QTextEdit, QVBoxLayout, QWidget) - -class Ui_Widget(object): - def setupUi(self, Widget): - if not Widget.objectName(): - Widget.setObjectName(u"Widget") - Widget.resize(400, 300) - self.verticalLayout_2 = QVBoxLayout(Widget) - self.verticalLayout_2.setSpacing(6) - self.verticalLayout_2.setContentsMargins(11, 11, 11, 11) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.labelLayout = QVBoxLayout() - self.labelLayout.setSpacing(6) - self.labelLayout.setObjectName(u"labelLayout") - - self.verticalLayout_2.addLayout(self.labelLayout) - - self.textEdit = QTextEdit(Widget) - self.textEdit.setObjectName(u"textEdit") - - self.verticalLayout_2.addWidget(self.textEdit) - - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setSpacing(6) - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - self.horizontalLayout.addItem(self.horizontalSpacer) - - self.clearButton = QPushButton(Widget) - self.clearButton.setObjectName(u"clearButton") - - self.horizontalLayout.addWidget(self.clearButton) - - - self.verticalLayout_2.addLayout(self.horizontalLayout) - - - self.retranslateUi(Widget) - - QMetaObject.connectSlotsByName(Widget) - # setupUi - - def retranslateUi(self, Widget): - Widget.setWindowTitle(QCoreApplication.translate("Widget", u"Widget", None)) - self.clearButton.setText(QCoreApplication.translate("Widget", u"Clear", None)) - # retranslateUi - diff --git a/Chap4-DragDropClipboard/4-1DragAndDropDemo/widget.py b/Chap4-DragDropClipboard/4-1DragAndDropDemo/widget.py deleted file mode 100644 index 1a4a44e..0000000 --- a/Chap4-DragDropClipboard/4-1DragAndDropDemo/widget.py +++ /dev/null @@ -1,54 +0,0 @@ -from PySide6.QtWidgets import QWidget -from PySide6.QtCore import Slot -from ui_widget import Ui_Widget -from dragdroplabel import DragDropLabel - - -class Widget(QWidget): - """Main application widget that hosts the drag and drop label""" - - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_Widget() - self.ui.setupUi(self) - - # Create and add the drag drop label - self.dragDropLabel = DragDropLabel(self) - self.dragDropLabel.mimeChanged.connect(self.mimeChanged) - self.ui.labelLayout.addWidget(self.dragDropLabel) - - # Connect the clear button - self.ui.clearButton.clicked.connect(self.on_clearButton_clicked) - - @Slot(object) - def mimeChanged(self, mimedata): - """Process and display MIME data information""" - self.ui.textEdit.clear() - if not mimedata: - return - - formats = mimedata.formats() - for i, format_name in enumerate(formats): - text = "" - if format_name == "text/plain": - text = mimedata.text().strip() - elif format_name == "text/html": - text = mimedata.html().strip() - elif format_name == "text/uri-list": - urlList = mimedata.urls() - for url in urlList: - text += url.toString() + " -/- " - else: - data = mimedata.data(format_name) - # Convert QByteArray to bytes and process each byte - byte_data = bytes(data) - for byte in byte_data: - text += f"{byte} " - - data_string = f"{i} | Format: {format_name}\n | Data: {text}\n------------" - self.ui.textEdit.append(data_string) - - @Slot() - def on_clearButton_clicked(self): - """Clear the text edit widget""" - self.ui.textEdit.clear() \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-1DragAndDropDemo/widget.ui b/Chap4-DragDropClipboard/4-1DragAndDropDemo/widget.ui deleted file mode 100644 index 84e63c9..0000000 --- a/Chap4-DragDropClipboard/4-1DragAndDropDemo/widget.ui +++ /dev/null @@ -1,52 +0,0 @@ - - - Widget - - - - 0 - 0 - 400 - 300 - - - - Widget - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Clear - - - - - - - - - - - From 9a83f1fdd475c8001a644843dd3bae5207c2ded4 Mon Sep 17 00:00:00 2001 From: EricoDeMecha Date: Tue, 18 Mar 2025 15:22:58 +0300 Subject: [PATCH 2/6] (ch4): ported ch4/4-2 to qt quick --- .../4-2DragDropImageDemo/README.md | 158 ------------------ .../4-2DragDropImageDemo/main.py | 37 +++- .../4-2DragDropImageDemo/main.qml | 106 ++++++++++++ .../4-2DragDropImageDemo/ui_widget.py | 45 ----- .../4-2DragDropImageDemo/widget.py | 69 -------- .../4-2DragDropImageDemo/widget.ui | 29 ---- 6 files changed, 134 insertions(+), 310 deletions(-) delete mode 100644 Chap4-DragDropClipboard/4-2DragDropImageDemo/README.md create mode 100644 Chap4-DragDropClipboard/4-2DragDropImageDemo/main.qml delete mode 100644 Chap4-DragDropClipboard/4-2DragDropImageDemo/ui_widget.py delete mode 100644 Chap4-DragDropClipboard/4-2DragDropImageDemo/widget.py delete mode 100644 Chap4-DragDropClipboard/4-2DragDropImageDemo/widget.ui diff --git a/Chap4-DragDropClipboard/4-2DragDropImageDemo/README.md b/Chap4-DragDropClipboard/4-2DragDropImageDemo/README.md deleted file mode 100644 index 0f8e44c..0000000 --- a/Chap4-DragDropClipboard/4-2DragDropImageDemo/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# Image Viewer with Drag and Drop in PySide6 - -This is a simple image viewer application built with PySide6 that allows users to drag and drop image files onto the application window to view them. - -## Project Overview - -This application demonstrates: -1. Implementing drag and drop functionality in a Qt application -2. Handling image files dropped onto a widget -3. Displaying and scaling images to fit a QLabel -4. Validating file types - -## Project Structure - -``` -project/ -├── main.py # Application entry point -├── widget.py # Main application widget -├── ui_widget.py # Generated UI code from widget.ui -└── widget.ui # UI design file -``` - -## Key Components - -### Widget Class - -The `Widget` class is the main application window that handles drag and drop operations: - -```python -class Widget(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_Widget() - self.ui.setupUi(self) - self.setAcceptDrops(True) # Enable drag and drop -``` - -### Drag and Drop Events - -The application implements four key event handlers for drag and drop: - -1. **dragEnterEvent**: Called when a drag operation enters the widget -2. **dragMoveEvent**: Called when a drag operation moves within the widget -3. **dragLeaveEvent**: Called when a drag operation leaves the widget -4. **dropEvent**: Called when the content is dropped onto the widget - -```python -def dropEvent(self, event: QDropEvent): - """Handle drop events, loading image files""" - if event.mimeData().hasUrls(): - urls = event.mimeData().urls() - if len(urls) > 1: - return - - file_path = urls[0].toLocalFile() - if self.isImage(file_path): - pixmap = QPixmap() - if pixmap.load(file_path): - # Scale the pixmap to fit the label - self.ui.label.setPixmap(pixmap.scaled( - self.ui.label.size(), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation - )) -``` - -### Image Handling - -The application checks if the dropped file is a supported image format: - -```python -def isImage(self, file_path): - """Check if the file is a supported image format""" - _, ext = os.path.splitext(file_path) - ext = ext.lower() - return ext in [".png", ".jpg", ".jpeg"] -``` - -## Responsive Image Scaling - -The application scales images to fit the widget while preserving their aspect ratio: - -```python -def resizeEvent(self, event): - """Handle resize events to scale the image""" - if self.ui.label.pixmap(): - # Get the original pixmap stored as a property - original_pixmap = getattr(self, '_original_pixmap', None) - if original_pixmap: - # Scale the original pixmap to the new size - self.ui.label.setPixmap(original_pixmap.scaled( - self.ui.label.size(), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation - )) - super().resizeEvent(event) -``` - -## Enabling Drag and Drop - -To enable drag and drop in a Qt application, two key steps are needed: - -1. Call `setAcceptDrops(True)` on the widget -2. Implement the necessary event handlers - -## Running the Application - -1. Generate the UI Python file (if widget.ui changes): - ``` - pyside6-uic widget.ui -o ui_widget.py - ``` - -2. Ensure PySide6 is installed: - ``` - pip install PySide6 - ``` - -3. Run the application: - ``` - python main.py - ``` - -4. Use the application: - - Drag and drop an image file (PNG, JPG, JPEG) onto the application window - - The image will be displayed and scaled to fit the window - - Resize the window to see the image scaled accordingly - -## Implementation Notes - -### File Path Handling - -When processing URLs from the MIME data, we need to convert them to local file paths: - -```python -file_path = urls[0].toLocalFile() -``` - -### Image Scaling - -For better visual quality, we use smooth transformation when scaling images: - -```python -pixmap.scaled( - self.ui.label.size(), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation -) -``` - -### Extending the Project - -This project could be extended with: - -1. **Multiple Image Support**: Allow dropping multiple images and provide navigation -2. **Additional Formats**: Add support for more image formats (GIF, WebP, etc.) -3. **Image Information**: Display details about the image (dimensions, file size, etc.) -4. **Edit Capabilities**: Add basic image editing functions -5. **Save/Export**: Add the ability to save or export viewed images \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-2DragDropImageDemo/main.py b/Chap4-DragDropClipboard/4-2DragDropImageDemo/main.py index 3432e80..3cce4fd 100644 --- a/Chap4-DragDropClipboard/4-2DragDropImageDemo/main.py +++ b/Chap4-DragDropClipboard/4-2DragDropImageDemo/main.py @@ -1,17 +1,36 @@ import sys -from PySide6.QtWidgets import QApplication -from PySide6.QtCore import Qt -from widget import Widget +from pathlib import Path +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtCore import QObject, Slot, Signal, QUrl + +class ImageController(QObject): + """Controller for handling image operations in QML""" + + @Slot(str, result=bool) + def isImage(self, file_path): + """Check if the file is a supported image format""" + ext = Path(file_path).suffix.lower() + return ext in [".png", ".jpg", ".jpeg", ".bmp", ".gif"] def main(): # Create the application - app = QApplication(sys.argv) + app = QGuiApplication(sys.argv) + + # Create controller and QML engine + controller = ImageController() + engine = QQmlApplicationEngine() + + # Expose controller to QML + engine.rootContext().setContextProperty("controller", controller) + + # Load QML file + qml_file = Path(__file__).resolve().parent / "main.qml" + engine.load(qml_file) - # Create and show the widget - window = Widget() - window.setWindowTitle("Image Viewer") - window.resize(600, 400) - window.show() + # Check if loading was successful + if not engine.rootObjects(): + sys.exit(-1) # Run the event loop return app.exec() diff --git a/Chap4-DragDropClipboard/4-2DragDropImageDemo/main.qml b/Chap4-DragDropClipboard/4-2DragDropImageDemo/main.qml new file mode 100644 index 0000000..24c99a4 --- /dev/null +++ b/Chap4-DragDropClipboard/4-2DragDropImageDemo/main.qml @@ -0,0 +1,106 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.platform as Platform + +Window { + id: mainWindow + width: 600 + height: 400 + visible: true + title: "Image Viewer" + color: "#f5f5f5" + + // Container for the image display + Rectangle { + id: imageContainer + anchors.fill: parent + color: "#ffffff" + border.color: "#cccccc" + border.width: 1 + + // Placeholder text when no image is loaded + Column { + anchors.centerIn: parent + spacing: 10 + visible: !imageViewer.source.toString() + + Image { + anchors.horizontalCenter: parent.horizontalCenter + source: "data:image/svg+xml," + width: 64 + height: 64 + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Drag and drop an image here" + font.pixelSize: 16 + color: "#666666" + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Supported formats: PNG, JPG, JPEG" + font.pixelSize: 12 + color: "#999999" + } + } + + // Image display with aspect ratio preserved + Image { + id: imageViewer + anchors.fill: parent + anchors.margins: 10 + fillMode: Image.PreserveAspectFit + asynchronous: true + cache: true + smooth: true + mipmap: true + } + + // Drop area for handling drag and drop + DropArea { + id: dropArea + anchors.fill: parent + + onEntered: { + // Highlight the drop area + imageContainer.border.color = "#4a90e2" + imageContainer.border.width = 2 + } + + onExited: { + // Reset the highlight + imageContainer.border.color = "#cccccc" + imageContainer.border.width = 1 + } + + onDropped: { + // Reset the highlight + imageContainer.border.color = "#cccccc" + imageContainer.border.width = 1 + + // Process dropped files + if (drop.hasUrls) { + for (var i = 0; i < drop.urls.length; i++) { + var url = drop.urls[i].toString() + + // Remove the "file:///" prefix on Windows or "file://" on Unix + var path = url.replace(/^file:\/\/\//, "").replace(/^file:\/\//, "") + + // Handle URL decoding for special characters + path = decodeURIComponent(path) + + // Check if it's an image + if (controller.isImage(path)) { + imageViewer.source = drop.urls[i] + break + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-2DragDropImageDemo/ui_widget.py b/Chap4-DragDropClipboard/4-2DragDropImageDemo/ui_widget.py deleted file mode 100644 index 3b517eb..0000000 --- a/Chap4-DragDropClipboard/4-2DragDropImageDemo/ui_widget.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'widget.ui' -## -## Created by: Qt User Interface Compiler version 6.8.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QLabel, QSizePolicy, QVBoxLayout, - QWidget) - -class Ui_Widget(object): - def setupUi(self, Widget): - if not Widget.objectName(): - Widget.setObjectName(u"Widget") - Widget.resize(400, 300) - self.verticalLayout = QVBoxLayout(Widget) - self.verticalLayout.setSpacing(6) - self.verticalLayout.setContentsMargins(11, 11, 11, 11) - self.verticalLayout.setObjectName(u"verticalLayout") - self.label = QLabel(Widget) - self.label.setObjectName(u"label") - - self.verticalLayout.addWidget(self.label) - - - self.retranslateUi(Widget) - - QMetaObject.connectSlotsByName(Widget) - # setupUi - - def retranslateUi(self, Widget): - Widget.setWindowTitle(QCoreApplication.translate("Widget", u"Widget", None)) - self.label.setText(QCoreApplication.translate("Widget", u"TextLabel", None)) - # retranslateUi - diff --git a/Chap4-DragDropClipboard/4-2DragDropImageDemo/widget.py b/Chap4-DragDropClipboard/4-2DragDropImageDemo/widget.py deleted file mode 100644 index 1910ee9..0000000 --- a/Chap4-DragDropClipboard/4-2DragDropImageDemo/widget.py +++ /dev/null @@ -1,69 +0,0 @@ -from PySide6.QtWidgets import QWidget -from PySide6.QtCore import QUrl, Qt -from PySide6.QtGui import (QDragEnterEvent, QDragMoveEvent, - QDragLeaveEvent, QDropEvent, QPixmap) -from ui_widget import Ui_Widget -import os - -class Widget(QWidget): - """Main application widget that accepts image drops""" - - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_Widget() - self.ui.setupUi(self) - self.setAcceptDrops(True) - - # Set an initial label text - self.ui.label.setText("Drag and drop an image here") - self.ui.label.setAlignment(Qt.AlignCenter) - - def dragEnterEvent(self, event: QDragEnterEvent): - """Handle drag enter events""" - event.accept() - - def dragMoveEvent(self, event: QDragMoveEvent): - """Handle drag move events""" - event.accept() - - def dragLeaveEvent(self, event: QDragLeaveEvent): - """Handle drag leave events""" - event.accept() - - def dropEvent(self, event: QDropEvent): - """Handle drop events, loading image files""" - if event.mimeData().hasUrls(): - urls = event.mimeData().urls() - if len(urls) > 1: - return - - file_path = urls[0].toLocalFile() - if self.isImage(file_path): - pixmap = QPixmap() - if pixmap.load(file_path): - # Scale the pixmap to fit the label while preserving aspect ratio - self.ui.label.setPixmap(pixmap.scaled( - self.ui.label.size(), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation - )) - - def isImage(self, file_path): - """Check if the file is a supported image format""" - _, ext = os.path.splitext(file_path) - ext = ext.lower() - return ext in [".png", ".jpg", ".jpeg"] - - def resizeEvent(self, event): - """Handle resize events to scale the image""" - if self.ui.label.pixmap(): - # Get the original pixmap stored as a property - original_pixmap = getattr(self, '_original_pixmap', None) - if original_pixmap: - # Scale the original pixmap to the new size - self.ui.label.setPixmap(original_pixmap.scaled( - self.ui.label.size(), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation - )) - super().resizeEvent(event) \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-2DragDropImageDemo/widget.ui b/Chap4-DragDropClipboard/4-2DragDropImageDemo/widget.ui deleted file mode 100644 index 2492927..0000000 --- a/Chap4-DragDropClipboard/4-2DragDropImageDemo/widget.ui +++ /dev/null @@ -1,29 +0,0 @@ - - - Widget - - - - 0 - 0 - 400 - 300 - - - - Widget - - - - - - TextLabel - - - - - - - - - From fb9dc5b2674e3be9490d5cc67b35b944f0be92c7 Mon Sep 17 00:00:00 2001 From: EricoDeMecha Date: Tue, 18 Mar 2025 15:46:49 +0300 Subject: [PATCH 3/6] (ch4): ported ch4/4-3 to qt quick --- .../ContainerArea.qml | 127 +++++++++ .../DraggableIcon.qml | 89 +++++++ .../README.md | 241 ------------------ .../container.py | 204 --------------- .../4-3DragDropBetweenWidgesInAppDemo/main.py | 50 +++- .../main.qml | 60 +++++ .../ui_widget.py | 44 ---- .../widget.py | 24 -- .../widget.ui | 25 -- 9 files changed, 319 insertions(+), 545 deletions(-) create mode 100644 Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/ContainerArea.qml create mode 100644 Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/DraggableIcon.qml delete mode 100644 Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/README.md delete mode 100644 Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/container.py create mode 100644 Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/main.qml delete mode 100644 Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/ui_widget.py delete mode 100644 Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/widget.py delete mode 100644 Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/widget.ui diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/ContainerArea.qml b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/ContainerArea.qml new file mode 100644 index 0000000..abe6d12 --- /dev/null +++ b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/ContainerArea.qml @@ -0,0 +1,127 @@ +import QtQuick + +Rectangle { + id: root + width: 300 + height: 300 + color: "transparent" + border.color: "black" + border.width: 1 + radius: 3 + + // Counter for generating unique IDs + property int iconCounter: 0 + + // Reference to created components + property var createdIcons: ({}) + + // Create icons on component initialization + Component.onCompleted: { + createInitialIcons() + } + + // Create the initial set of icons + function createInitialIcons() { + // Create Qt icon + createIcon(resourceController.qtIconPath, 20, 20) + + // Create C++ icon + createIcon(resourceController.cppIconPath, 150, 20) + + // Create Terminal icon + createIcon(resourceController.terminalIconPath, 20, 150) + } + + // Create a new icon at the specified position + function createIcon(source, x, y) { + var iconComponent = Qt.createComponent("DraggableIcon.qml") + if (iconComponent.status === Component.Ready) { + var iconId = "icon_" + iconCounter++ + var icon = iconComponent.createObject(root, { + "x": x, + "y": y, + "source": source, + "iconId": iconId + }) + + // Store reference to the created icon + createdIcons[iconId] = icon + return icon + } else { + console.error("Error creating icon component:", iconComponent.errorString()) + return null + } + } + + // DropArea to handle dropping icons + DropArea { + id: dropArea + anchors.fill: parent + + // Handle entering the drop area + onEntered: function(drag) { + console.log("Drag entered with keys:", Object.keys(drag.keys)) + + // Only highlight if it's our custom type + if (drag.keys.indexOf("application/x-draggableicon") >= 0) { + // Highlight the drop area + root.border.color = "blue" + root.border.width = 2 + drag.accepted = true + } else { + drag.accepted = false + } + } + + // Handle drag move events + onPositionChanged: function(drag) { + // We accept the drag if it's our custom type + if (drag.keys.indexOf("application/x-draggableicon") >= 0) { + drag.accepted = true + } else { + drag.accepted = false + } + } + + // Handle exiting the drop area + onExited: function() { + // Reset the highlight + root.border.color = "black" + root.border.width = 1 + } + + // Handle an item being dropped in the area + onDropped: function(drop) { + // Reset the highlight + root.border.color = "black" + root.border.width = 1 + + console.log("Drop received, formats:", Object.keys(drop.keys)) + + // Check if this is our custom mime type + if (drop.keys.indexOf("application/x-draggableicon") >= 0) { + var iconSource = drop.getDataAsString("iconSource") + var sourceId = drop.getDataAsString("iconId") + + console.log("Creating new icon with source:", iconSource) + + // Create a new icon at the drop position + var newIcon = createIcon( + iconSource, + drop.x - 32, // Center the icon at drop position + drop.y - 32 + ) + + // If the drag operation was a move, accept as move + if (drop.proposedAction === Qt.MoveAction) { + drop.acceptProposedAction() + } else { + drop.accept(Qt.CopyAction) + } + } else { + console.log("Drop with unsupported format") + drop.accepted = false + } + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/DraggableIcon.qml b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/DraggableIcon.qml new file mode 100644 index 0000000..a030dc1 --- /dev/null +++ b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/DraggableIcon.qml @@ -0,0 +1,89 @@ +import QtQuick + +Item { + id: root + width: 64 + height: 64 + + // Properties + property string source: "" // Image source + property string iconId: "" // Unique identifier for this icon + property bool dragging: false + + // The actual image + Image { + id: iconImage + anchors.fill: parent + source: root.source + sourceSize.width: 64 + sourceSize.height: 64 + smooth: true + + // Visual effect when being dragged + opacity: root.dragging ? 0.5 : 1.0 + } + + // Component to handle drag operation + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + Drag.keys: ["application/x-draggableicon"] + Drag.mimeData: ({ + "iconId": root.iconId, + "iconSource": root.source, + "application/x-draggableicon": "true" + }) + Drag.dragType: Drag.Automatic + Drag.supportedActions: Qt.CopyAction | Qt.MoveAction + Drag.proposedAction: Qt.CopyAction + + // Allow dragging when a mouse is pressed over the icon + MouseArea { + id: mouseArea + anchors.fill: parent + + drag.target: parent + drag.threshold: 5 + + onPressed: function(mouse) { + root.z = 10 // Bring to front when dragging + } + + onPositionChanged: function(mouse) { + if (pressed && !root.dragging && drag.active) { + root.dragging = true + + // Manually activate drag instead of binding + root.Drag.active = true + + // Set action based on Shift key (move vs copy) + if (mouse.modifiers & Qt.ShiftModifier) { + root.Drag.proposedAction = Qt.MoveAction + } else { + root.Drag.proposedAction = Qt.CopyAction + } + } + } + + onReleased: function() { + if (root.dragging) { + // End the active drag + var result = root.Drag.drop() + + // Turn off dragging flag + root.Drag.active = false + root.dragging = false + + // If this was dropped in a valid drop area with move action + if (result === Qt.MoveAction) { + // Only destroy the item if it was a move + root.destroy() + } else { + // Reset z-order + root.z = 1 + } + } else { + root.z = 1 + } + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/README.md b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/README.md deleted file mode 100644 index 4fa3a82..0000000 --- a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/README.md +++ /dev/null @@ -1,241 +0,0 @@ -# Custom Drag and Drop Demo in PySide6 - -This project demonstrates how to implement advanced drag and drop functionality in PySide6, including custom data serialization and inter-widget drag and drop operations. - -## Project Overview - -This application illustrates: -1. Creating custom drag and drop operations between container widgets -2. Serializing and deserializing custom data during drag and drop -3. Moving vs. copying items during drag operations -4. Using QDataStream for binary data serialization -5. Creating and managing dynamic widgets -6. Using Qt's resource system for loading images - -## Project Structure - -``` -project/ -├── main.py # Application entry point -├── widget.py # Main application widget -├── container.py # Custom container with drag and drop -├── ui_widget.py # Generated UI code from widget.ui -├── resources.qrc # Resource file for images -├── resource_rc.py # Generated resource code -└── images/ # Directory for icon images - ├── qt.png - ├── cpp.png - └── terminal.png -``` - -## Key Components - -### Container Widget - -The `Container` class is a custom widget that implements drag and drop functionality: - -```python -class Container(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.setMinimumSize(150, 150) - self.setAcceptDrops(True) # Enable drop acceptance - self.startPos = QPoint() - - # Create initial icon labels - # ... -``` - -### Drag Operations - -The drag operation is implemented in the `mouseMoveEvent` method: - -```python -def mouseMoveEvent(self, event: QMouseEvent): - if event.buttons() & Qt.LeftButton: - # Calculate distance moved - distance = (event.position().toPoint() - self.startPos).manhattanLength() - - # Start drag if distance exceeds threshold - if distance >= QApplication.startDragDistance(): - # Get child widget at position - child = self.childAt(event.position().toPoint()) - - # Serialize the drag data - ba = QByteArray() - data_stream = QDataStream(ba, QIODevice.WriteOnly) - data_stream.writeQPixmap(pixmap) - data_stream.writeQPoint(offset) - - # Create and execute drag operation - # ... -``` - -### Data Serialization - -The application serializes the dragged item's pixmap and position offset: - -```python -# Serialization -ba = QByteArray() -data_stream = QDataStream(ba, QIODevice.WriteOnly) -data_stream.writeQPixmap(pixmap) -data_stream.writeQPoint(offset) - -# Deserialization -ba = event.mimeData().data("application/x-qtcustomitem") -data_stream = QDataStream(ba, QIODevice.ReadOnly) -pixmap = QPixmap() -data_stream.readQPixmap(pixmap) -offset = QPoint() -data_stream.readQPoint(offset) -``` - -### Drop Handling - -The application handles drops by creating new widgets with the deserialized data: - -```python -def dropEvent(self, event: QDropEvent): - if event.mimeData().hasFormat("application/x-qtcustomitem"): - # Deserialize data - # ... - - # Create new label with the deserialized pixmap - new_label = QLabel(self) - new_label.setPixmap(pixmap) - new_label.move(event.position().toPoint() - offset) - new_label.show() - new_label.setAttribute(Qt.WA_DeleteOnClose) - - # Set drop action - if event.source() == self: - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.acceptProposedAction() -``` - -## Resource System - -The application uses Qt's resource system to load images: - -```python -# In resources.qrc - - - images/qt.png - images/cpp.png - images/terminal.png - - - -# In Python code -import resource_rc # Import the compiled resource file -qtIcon.setPixmap(QPixmap(":/images/qt.png")) # Use resource path -``` - -## Custom MIME Type - -The application uses a custom MIME type to identify its drag and drop data: - -```python -mime_data.setData("application/x-qtcustomitem", ba) -``` - -## Move vs. Copy Operations - -The application supports both move and copy operations: - -```python -if drag.exec(Qt.MoveAction | Qt.CopyAction, Qt.CopyAction) == Qt.MoveAction: - # Move operation - close the original child widget - child.close() -else: - # Copy operation - keep the original - pass -``` - -## Running the Application - -1. Generate the resource Python file: - ``` - pyside6-rcc resources.qrc -o resource_rc.py - ``` - -2. Generate the UI Python file (if widget.ui changes): - ``` - pyside6-uic widget.ui -o ui_widget.py - ``` - -3. Ensure PySide6 is installed: - ``` - pip install PySide6 - ``` - -4. Run the application: - ``` - python main.py - ``` - -## Troubleshooting Resources - -If the icons don't appear: - -1. Make sure the resource_rc.py file is generated correctly from the .qrc file -2. Verify that resource_rc.py is imported in both container.py and main.py -3. Check that the image paths in the .qrc file point to existing image files -4. Ensure the resource paths in the code match those in the .qrc file - -## Using the Application - -1. Run the application to see two container widgets side by side -2. Each container has multiple colored icons -3. Drag an icon within its container to move it -4. Drag an icon to the other container to copy it -5. Notice how the icon follows the mouse cursor during drag - -## Implementation Notes - -### QDataStream Version Compatibility - -When using QDataStream for serialization, it's important to be aware of version compatibility: - -```python -# For complete compatibility, you could set the version: -data_stream.setVersion(QDataStream.Qt_6_0) -``` - -### Event Acceptance - -Proper event acceptance is crucial for drag and drop operations: - -```python -def dragEnterEvent(self, event: QDragEnterEvent): - if event.mimeData().hasFormat("application/x-qtcustomitem"): - if event.source() == self: - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.acceptProposedAction() - else: - event.ignore() -``` - -### Widget Management - -The application uses the `WA_DeleteOnClose` attribute to automatically manage memory: - -```python -new_label.setAttribute(Qt.WA_DeleteOnClose) -``` - -## Extending the Project - -This project could be extended with: - -1. **Custom Icons**: Add more icon types and behaviors -2. **Drag Feedback**: Enhanced visual feedback during drag operations -3. **Layout Management**: Snap-to-grid or automatic layout of dropped items -4. **Edit Capabilities**: Add the ability to edit or customize dropped items -5. **Persistence**: Save and restore the state of the containers \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/container.py b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/container.py deleted file mode 100644 index eb2252a..0000000 --- a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/container.py +++ /dev/null @@ -1,204 +0,0 @@ -from PySide6.QtWidgets import QWidget, QLabel, QApplication -from PySide6.QtCore import Qt, QPoint, QByteArray, QIODevice, QDataStream, QBuffer, QMimeData -from PySide6.QtGui import (QPainter, QMouseEvent, QDragEnterEvent, QDragMoveEvent, - QDragLeaveEvent, QDropEvent, QPixmap, QDrag, QColor, QImage) -import resource_rc # Import the resource file - -class Container(QWidget): - """Container widget that supports internal drag and drop operations""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setMinimumSize(150, 150) - self.setAcceptDrops(True) - self.startPos = QPoint() - - # Create initial icons - try: - # Qt icon - qtIcon = QLabel(self) - qtIcon.setPixmap(QPixmap(":/images/qt.png")) - qtIcon.move(20, 20) - qtIcon.show() - qtIcon.setAttribute(Qt.WA_DeleteOnClose) - - # C++ icon - cppIcon = QLabel(self) - cppIcon.setPixmap(QPixmap(":/images/cpp.png")) - cppIcon.move(150, 20) - cppIcon.show() - cppIcon.setAttribute(Qt.WA_DeleteOnClose) - - # Terminal icon - terminalIcon = QLabel(self) - terminalIcon.setPixmap(QPixmap(":/images/terminal.png")) - terminalIcon.move(20, 150) - terminalIcon.show() - terminalIcon.setAttribute(Qt.WA_DeleteOnClose) - - print("Icons loaded successfully") - except Exception as e: - print(f"Error loading icons: {e}") - # Fallback if images aren't available - self.createSampleLabels() - - def createSampleLabels(self): - """Create sample colored labels if images aren't available""" - # Red label - redLabel = QLabel(self) - redPixmap = QPixmap(64, 64) - redPixmap.fill(Qt.red) - redLabel.setPixmap(redPixmap) - redLabel.move(20, 20) - redLabel.show() - redLabel.setAttribute(Qt.WA_DeleteOnClose) - - # Green label - greenLabel = QLabel(self) - greenPixmap = QPixmap(64, 64) - greenPixmap.fill(Qt.green) - greenLabel.setPixmap(greenPixmap) - greenLabel.move(150, 20) - greenLabel.show() - greenLabel.setAttribute(Qt.WA_DeleteOnClose) - - # Blue label - blueLabel = QLabel(self) - bluePixmap = QPixmap(64, 64) - bluePixmap.fill(Qt.blue) - blueLabel.setPixmap(bluePixmap) - blueLabel.move(20, 150) - blueLabel.show() - blueLabel.setAttribute(Qt.WA_DeleteOnClose) - - print("Created colored label fallbacks") - - def mousePressEvent(self, event: QMouseEvent): - """Remember the start position when mouse is pressed""" - if event.button() == Qt.LeftButton: - self.startPos = event.position().toPoint() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event: QMouseEvent): - """Start drag operation if mouse moved beyond threshold""" - if event.buttons() & Qt.LeftButton: - # Calculate distance moved - distance = (event.position().toPoint() - self.startPos).manhattanLength() - - # Check if distance exceeds drag start distance - if distance >= QApplication.startDragDistance(): - # Get the child widget at the current position - child = self.childAt(event.position().toPoint()) - - if not child or not isinstance(child, QLabel): - return - - # Get pixmap from child - pixmap = child.pixmap() - if not pixmap: - return - - # Save pixmap to a byte array - pixmap_ba = QByteArray() - buffer = QBuffer(pixmap_ba) - buffer.open(QIODevice.WriteOnly) - pixmap.save(buffer, "PNG") - buffer.close() - - # Calculate hotspot offset - hotspot = event.position().toPoint() - child.pos() - - # Prepare data to be serialized (as bytes) - # We'll use simple format: pixmap data followed by x,y coordinates - mime_data = QMimeData() - - # Store the image data and hotspot directly in separate mime formats - mime_data.setData("application/x-qtcustompixmap", pixmap_ba) - - # Store hotspot as a separate mime type - hotspot_data = QByteArray() - hotspot_buffer = QDataStream(hotspot_data, QIODevice.WriteOnly) - hotspot_buffer << hotspot.x() << hotspot.y() - mime_data.setData("application/x-qtcustomhotspot", hotspot_data) - - # Create drag object - drag = QDrag(self) - drag.setMimeData(mime_data) - drag.setPixmap(pixmap) - drag.setHotSpot(hotspot) - - # Execute drag operation - if drag.exec(Qt.MoveAction | Qt.CopyAction, Qt.CopyAction) == Qt.MoveAction: - # Move operation - close the original child widget - print("Moving data") - child.close() - else: - # Copy operation - print("Copying data") - - def dragEnterEvent(self, event: QDragEnterEvent): - """Handle drag enter events""" - if event.mimeData().hasFormat("application/x-qtcustompixmap"): - if event.source() == self: - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.acceptProposedAction() - else: - event.ignore() - - def dragMoveEvent(self, event: QDragMoveEvent): - """Handle drag move events""" - if event.mimeData().hasFormat("application/x-qtcustompixmap"): - if event.source() == self: - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.acceptProposedAction() - else: - event.ignore() - - def dragLeaveEvent(self, event: QDragLeaveEvent): - """Handle drag leave events""" - super().dragLeaveEvent(event) - - def dropEvent(self, event: QDropEvent): - """Handle drop events, creating new label widgets""" - if event.mimeData().hasFormat("application/x-qtcustompixmap"): - # Get the pixmap data - pixmap_ba = event.mimeData().data("application/x-qtcustompixmap") - - # Get the hotspot data - hotspot_data = event.mimeData().data("application/x-qtcustomhotspot") - hotspot_stream = QDataStream(hotspot_data, QIODevice.ReadOnly) - - # Read the x,y coordinates - x, y = 0, 0 - hotspot_stream >> x >> y - hotspot = QPoint(x, y) - - # Recreate the pixmap from the data - pixmap = QPixmap() - pixmap.loadFromData(pixmap_ba) - - # Create new label with the deserialized pixmap - new_label = QLabel(self) - new_label.setPixmap(pixmap) - new_label.move(event.position().toPoint() - hotspot) - new_label.show() - new_label.setAttribute(Qt.WA_DeleteOnClose) - - # Set the drop action - if event.source() == self: - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.acceptProposedAction() - else: - event.ignore() - - def paintEvent(self, event): - """Paint a rounded rectangle border around the container""" - painter = QPainter(self) - painter.drawRoundedRect(0, 5, self.width() - 10, self.height() - 10, 3, 3) - super().paintEvent(event) \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/main.py b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/main.py index 1a70118..18054fd 100644 --- a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/main.py +++ b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/main.py @@ -1,14 +1,50 @@ import sys -from PySide6.QtWidgets import QApplication -from widget import Widget -import resource_rc +from pathlib import Path +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtCore import QObject, Slot, Signal, Property, QUrl + +import resource_rc + +class ResourceController(QObject): + """Controller class to provide resource paths to QML""" + + def __init__(self, parent=None): + super().__init__(parent) + + @Property(str) + def qtIconPath(self): + return "qrc:/images/qt.png" + + @Property(str) + def cppIconPath(self): + return "qrc:/images/cpp.png" + + @Property(str) + def terminalIconPath(self): + return "qrc:/images/terminal.png" def main(): - app = QApplication(sys.argv) - window = Widget() - window.resize(800, 600) - window.show() + # Create application + app = QGuiApplication(sys.argv) + + # Create controller and QML engine + controller = ResourceController() + engine = QQmlApplicationEngine() + + # Expose controller to QML + engine.rootContext().setContextProperty("resourceController", controller) + + # Load QML file + qml_file = Path(__file__).resolve().parent / "main.qml" + engine.load(qml_file) + + # Check if loading was successful + if not engine.rootObjects(): + print("Error: Could not load QML file") + sys.exit(-1) + # Run the event loop return app.exec() if __name__ == "__main__": diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/main.qml b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/main.qml new file mode 100644 index 0000000..1d8a169 --- /dev/null +++ b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/main.qml @@ -0,0 +1,60 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts + +Window { + id: mainWindow + width: 800 + height: 600 + visible: true + title: "Drag and Drop Demo" + color: "#f5f5f5" + + // Main layout + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + // Title text + Text { + Layout.fillWidth: true + text: "Drag and drop icons between containers" + font.pixelSize: 16 + horizontalAlignment: Text.AlignHCenter + } + + // Container with two drag-drop areas side by side + SplitView { + Layout.fillWidth: true + Layout.fillHeight: true + orientation: Qt.Horizontal + + // Left container + ContainerArea { + id: leftContainer + SplitView.minimumWidth: 150 + SplitView.preferredWidth: 380 + SplitView.fillHeight: true + } + + // Right container + ContainerArea { + id: rightContainer + SplitView.minimumWidth: 150 + SplitView.fillWidth: true + SplitView.fillHeight: true + } + } + + // Instructions text + Text { + Layout.fillWidth: true + text: "Drag icons to copy them. Hold Shift while dragging to move them instead." + font.pixelSize: 12 + color: "#666666" + horizontalAlignment: Text.AlignHCenter + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/ui_widget.py b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/ui_widget.py deleted file mode 100644 index 7868ac8..0000000 --- a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/ui_widget.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'widget.ui' -## -## Created by: Qt User Interface Compiler version 6.8.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QSizePolicy, QVBoxLayout, QWidget) - -class Ui_Widget(object): - def setupUi(self, Widget): - if not Widget.objectName(): - Widget.setObjectName(u"Widget") - Widget.resize(621, 570) - self.verticalLayout_2 = QVBoxLayout(Widget) - self.verticalLayout_2.setSpacing(6) - self.verticalLayout_2.setContentsMargins(11, 11, 11, 11) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.verticalLayout = QVBoxLayout() - self.verticalLayout.setSpacing(6) - self.verticalLayout.setObjectName(u"verticalLayout") - - self.verticalLayout_2.addLayout(self.verticalLayout) - - - self.retranslateUi(Widget) - - QMetaObject.connectSlotsByName(Widget) - # setupUi - - def retranslateUi(self, Widget): - Widget.setWindowTitle(QCoreApplication.translate("Widget", u"Widget", None)) - # retranslateUi - diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/widget.py b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/widget.py deleted file mode 100644 index 7eb776f..0000000 --- a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/widget.py +++ /dev/null @@ -1,24 +0,0 @@ -from PySide6.QtWidgets import QWidget, QSplitter -from ui_widget import Ui_Widget -from container import Container - -class Widget(QWidget): - """Main widget that contains multiple container widgets""" - - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_Widget() - self.ui.setupUi(self) - - # Create a splitter with two container widgets - splitter = QSplitter(self) - - # Add two container widgets to the splitter - splitter.addWidget(Container(self)) - splitter.addWidget(Container(self)) - - # Add the splitter to the layout - self.ui.verticalLayout.addWidget(splitter) - - # Set window title - self.setWindowTitle("Drag and Drop Demo") \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/widget.ui b/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/widget.ui deleted file mode 100644 index c63e94b..0000000 --- a/Chap4-DragDropClipboard/4-3DragDropBetweenWidgesInAppDemo/widget.ui +++ /dev/null @@ -1,25 +0,0 @@ - - - Widget - - - - 0 - 0 - 621 - 570 - - - - Widget - - - - - - - - - - - From dda054eeb92b3ac20b37d472d704fcac65c986a3 Mon Sep 17 00:00:00 2001 From: EricoDeMecha Date: Tue, 18 Mar 2025 17:22:00 +0300 Subject: [PATCH 4/6] (ch4): ported ch4/4-4 to qt quick --- .../ContainerArea.qml | 106 +++++++++ .../DraggableIcon.qml | 91 ++++++++ .../README.md | 212 ------------------ .../container.py | 188 ---------------- .../main.py | 33 ++- .../main.qml | 60 +++++ .../pixmapmime.py | 51 ----- .../resource_controller.py | 30 +++ .../ui_widget.py | 44 ---- .../widget.py | 24 -- .../widget.ui | 25 --- 11 files changed, 313 insertions(+), 551 deletions(-) create mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/ContainerArea.qml create mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/DraggableIcon.qml delete mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/README.md delete mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/container.py create mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/main.qml delete mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/pixmapmime.py create mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/resource_controller.py delete mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/ui_widget.py delete mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/widget.py delete mode 100644 Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/widget.ui diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/ContainerArea.qml b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/ContainerArea.qml new file mode 100644 index 0000000..db7a386 --- /dev/null +++ b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/ContainerArea.qml @@ -0,0 +1,106 @@ +import QtQuick + +Rectangle { + id: root + width: 300 + height: 300 + color: "transparent" + + // Counter for generating unique IDs + property int iconCounter: 0 + + // Reference to created components + property var createdIcons: ({}) + + // Create icons on component initialization + Component.onCompleted: { + createInitialIcons() + } + + // Draw a border with rounded corners + Rectangle { + anchors.fill: parent + anchors.margins: 5 + radius: 3 + color: "transparent" + border.color: dropArea.containsDrag ? "blue" : "black" + border.width: dropArea.containsDrag ? 2 : 1 + } + + // Create the initial set of icons + function createInitialIcons() { + // Create Qt icon + createIcon(resourceController.qtIconPath, 20, 20) + + // Create C++ icon + createIcon(resourceController.cppIconPath, 150, 20) + + // Create Terminal icon + createIcon(resourceController.terminalIconPath, 20, 150) + } + + // Create a new icon at the specified position + function createIcon(source, x, y) { + var iconComponent = Qt.createComponent("DraggableIcon.qml") + if (iconComponent.status === Component.Ready) { + var iconId = "icon_" + iconCounter++ + var icon = iconComponent.createObject(root, { + "x": x, + "y": y, + "source": source, + "iconId": iconId, + "description": resourceController.getIconDescription(source) + }) + + // Store reference to the created icon + createdIcons[iconId] = icon + return icon + } else { + console.error("Error creating icon component:", iconComponent.errorString()) + return null + } + } + + // DropArea to handle dropping icons + DropArea { + id: dropArea + anchors.fill: parent + + // Accept the drop if it has our custom format + onEntered: function(drag) { + if (drag.formats.indexOf("application/x-qml-icon-source") >= 0) { + drag.accept(); + return; + } + drag.accepted = false; + } + + // Handle an item being dropped in the area + onDropped: function(drop) { + // Check if this has our custom mime type + if (drop.formats.indexOf("application/x-qml-icon-source") >= 0) { + var iconSource = drop.getDataAsString("application/x-qml-icon-source"); + var description = drop.getDataAsString("text/plain"); + + console.log("Creating new icon with source:", iconSource); + + // Create a new icon at the drop position + var newIcon = createIcon( + iconSource, + drop.x - 32, // Center the icon at drop position + drop.y - 32 + ); + + // If the drag operation was a move, accept as move + if (drop.proposedAction === Qt.MoveAction) { + drop.acceptProposedAction(); + } else { + drop.accept(Qt.CopyAction); + } + } else { + console.log("Drop with unsupported format"); + drop.accepted = false; + } + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/DraggableIcon.qml b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/DraggableIcon.qml new file mode 100644 index 0000000..02ae0da --- /dev/null +++ b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/DraggableIcon.qml @@ -0,0 +1,91 @@ +import QtQuick + +Item { + id: root + width: 64 + height: 64 + + // Properties + property string source: "" // Image source + property string iconId: "" // Unique identifier for this icon + property string description: "" // Description of the icon + property bool dragging: false + + // The actual image + Image { + id: iconImage + anchors.fill: parent + source: root.source + sourceSize.width: 64 + sourceSize.height: 64 + smooth: true + + // Visual effect when being dragged + opacity: root.dragging ? 0.5 : 1.0 + } + + // Component to handle drag operation + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + + // Use mimeData to store our custom data + Drag.mimeData: { + "text/plain": root.description, + "application/x-qml-icon-source": root.source + } + + Drag.dragType: Drag.Automatic + Drag.supportedActions: Qt.CopyAction | Qt.MoveAction + Drag.proposedAction: Qt.CopyAction + + // Allow dragging when a mouse is pressed over the icon + MouseArea { + id: mouseArea + anchors.fill: parent + + drag.target: parent + drag.threshold: 5 + + onPressed: function(mouse) { + root.z = 10 // Bring to front when dragging + } + + onPositionChanged: function(mouse) { + if (pressed && !root.dragging && drag.active) { + root.dragging = true + + // Manually activate drag instead of binding + root.Drag.active = true + + // Set action based on Shift key (move vs copy) + if (mouse.modifiers & Qt.ShiftModifier) { + root.Drag.proposedAction = Qt.MoveAction + } else { + root.Drag.proposedAction = Qt.CopyAction + } + } + } + + onReleased: function() { + if (root.dragging) { + // End the active drag + var result = root.Drag.drop() + + // Turn off dragging flag + root.Drag.active = false + root.dragging = false + + // If this was dropped in a valid drop area with move action + if (result === Qt.MoveAction) { + // Only destroy the item if it was a move + root.destroy() + } else { + // Reset z-order + root.z = 1 + } + } else { + root.z = 1 + } + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/README.md b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/README.md deleted file mode 100644 index 84d44b3..0000000 --- a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/README.md +++ /dev/null @@ -1,212 +0,0 @@ -# Custom MIME Data Drag and Drop Demo in PySide6 - -This project demonstrates how to implement advanced drag and drop functionality in PySide6 using a custom QMimeData subclass to provide enhanced data transfer capabilities. - -## Project Overview - -This application illustrates: -1. Creating a custom `QMimeData` subclass to store complex data -2. Implementing drag and drop with direct object references -3. Visual feedback during drag operations using semi-transparent overlays -4. Dragging and dropping between multiple container widgets -5. Distinguishing between move and copy operations -6. Providing additional data formats (HTML and plain text) - -## Project Structure - -``` -project/ -├── main.py # Application entry point -├── widget.py # Main application widget -├── container.py # Custom container with drag and drop -├── pixmapmime.py # Custom QMimeData subclass -├── ui_widget.py # Generated UI code from widget.ui -├── resources.qrc # Resource file for images -├── resource_rc.py # Generated resource code -└── images/ # Directory for icon images - ├── qt.png - ├── cpp.png - └── terminal.png -``` - -## Key Components - -### Custom QMimeData Subclass - -The `PixmapMime` class extends QMimeData to directly store a pixmap and offset information: - -```python -class PixmapMime(QMimeData): - def __init__(self, pix, offset, description): - super().__init__() - self.mPix = pix - self.mOffset = offset - self.description = description - self.mimeFormats = ["text/html", "text/plain"] - - def pix(self): - """Get the stored pixmap""" - return self.mPix - - def offset(self): - """Get the stored offset point""" - return self.mOffset - - # Other methods for MIME data handling -``` - -This approach allows direct access to custom data without serialization/deserialization. - -### Container Widget - -The `Container` class implements the drag and drop functionality: - -```python -def mouseMoveEvent(self, event: QMouseEvent): - if event.buttons() & Qt.LeftButton: - # Calculate distance moved - distance = (event.position().toPoint() - self.startPos).manhattanLength() - - if distance >= QApplication.startDragDistance(): - # Start drag with custom MIME data - child = self.childAt(event.position().toPoint()) - pixmap = child.pixmap() - offset = event.position().toPoint() - child.pos() - - # Create custom mime data - mime_data = PixmapMime(pixmap, offset, "Item icon") - - # Configure and execute the drag operation - # ... -``` - -### Visual Feedback - -The application provides visual feedback during drag operations: - -```python -# Apply blur effect to original label during drag -temp_pixmap = QPixmap(pixmap) -painter = QPainter(temp_pixmap) -painter.fillRect(temp_pixmap.rect(), QColor(127, 127, 127, 127)) -painter.end() -child.setPixmap(temp_pixmap) - -# Execute drag operation -if drag.exec(Qt.MoveAction | Qt.CopyAction, Qt.CopyAction) == Qt.MoveAction: - # Move operation - close the original widget - child.close() -else: - # Copy operation - restore the original pixmap - child.setPixmap(pixmap) -``` - -### Type Checking - -The application uses Python's `isinstance()` function to check the MIME data type: - -```python -def dragEnterEvent(self, event: QDragEnterEvent): - mime_data = event.mimeData() - if isinstance(mime_data, PixmapMime): - # Accept the drag operation - # ... - else: - event.ignore() -``` - -## Multiple Data Formats - -The custom MIME data provides data in multiple formats: - -```python -def retrieveData(self, mimetype, preferredType): - if mimetype == "text/plain": - return self.description - elif mimetype == "text/html": - html_string = "

" + self.description + "

" - return html_string - else: - return super().retrieveData(mimetype, preferredType) -``` - -## Running the Application - -1. Generate the resource Python file: - ``` - pyside6-rcc resources.qrc -o resource_rc.py - ``` - -2. Generate the UI Python file (if widget.ui changes): - ``` - pyside6-uic widget.ui -o ui_widget.py - ``` - -3. Ensure PySide6 is installed: - ``` - pip install PySide6 - ``` - -4. Run the application: - ``` - python main.py - ``` - -## Using the Application - -1. Run the application to see two container widgets side by side -2. Each container has three icons (Qt, C++, and Terminal) -3. Drag an icon within its container to move it -4. Drag an icon to the other container to copy it -5. Notice the semi-transparent effect applied to the original icon during dragging - -## Implementation Notes - -### Subclassing QMimeData - -The key advantage of subclassing QMimeData is direct data access: - -```python -# In dropEvent -mime_data = event.mimeData() -if isinstance(mime_data, PixmapMime): - pixmap = mime_data.pix() # Direct access to the pixmap - offset = mime_data.offset() # Direct access to the offset - # ... -``` - -This approach: -- Avoids serialization/deserialization overhead -- Provides type-safe data access -- Allows for custom methods and properties - -### Runtime Type Checking - -In PySide6, we use Python's `isinstance()` function to check types: - -```python -if isinstance(mime_data, PixmapMime): - # It's our custom MIME data -``` - -This replaces the C++ `qobject_cast` used in the original code. - -### Multiple MIME Types - -The custom QMimeData subclass provides data in multiple formats: - -1. Direct access to pixmap and offset via custom methods -2. Plain text description via "text/plain" MIME type -3. HTML formatted description via "text/html" MIME type - -This demonstrates how to make drag and drop data compatible with different targets. - -## Extending the Project - -This project could be extended with: - -1. **Additional Data Types**: Include more complex data structures in the custom MIME data -2. **Custom Rendering**: Implement custom rendering during drag operations -3. **Filtering**: Add criteria to control which targets accept drops -4. **Undo/Redo Support**: Track drag and drop operations for undo/redo functionality -5. **External Drop Support**: Enable dropping data from external applications \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/container.py b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/container.py deleted file mode 100644 index 9956928..0000000 --- a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/container.py +++ /dev/null @@ -1,188 +0,0 @@ -from PySide6.QtWidgets import QWidget, QLabel, QApplication -from PySide6.QtCore import Qt, QPoint -from PySide6.QtGui import (QPainter, QMouseEvent, QDragEnterEvent, QDragMoveEvent, - QDragLeaveEvent, QDropEvent, QPixmap, QDrag, QColor) -from pixmapmime import PixmapMime -import resource_rc # Import the resource file - -class Container(QWidget): - """Container widget that supports internal drag and drop operations""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setMinimumSize(150, 150) - self.setAcceptDrops(True) - self.startPos = QPoint() - - # Create initial icons - try: - # Qt icon - qtIcon = QLabel(self) - qtIcon.setPixmap(QPixmap(":/images/qt.png")) - qtIcon.move(20, 20) - qtIcon.show() - qtIcon.setAttribute(Qt.WA_DeleteOnClose) - - # C++ icon - cppIcon = QLabel(self) - cppIcon.setPixmap(QPixmap(":/images/cpp.png")) - cppIcon.move(150, 20) - cppIcon.show() - cppIcon.setAttribute(Qt.WA_DeleteOnClose) - - # Terminal icon - terminalIcon = QLabel(self) - terminalIcon.setPixmap(QPixmap(":/images/terminal.png")) - terminalIcon.move(20, 150) - terminalIcon.show() - terminalIcon.setAttribute(Qt.WA_DeleteOnClose) - - print("Icons loaded successfully") - except Exception as e: - print(f"Error loading icons: {e}") - # Fallback if images aren't available - self.createSampleLabels() - - def createSampleLabels(self): - """Create sample colored labels if images aren't available""" - # Red label - redLabel = QLabel(self) - redPixmap = QPixmap(64, 64) - redPixmap.fill(Qt.red) - redLabel.setPixmap(redPixmap) - redLabel.move(20, 20) - redLabel.show() - redLabel.setAttribute(Qt.WA_DeleteOnClose) - - # Green label - greenLabel = QLabel(self) - greenPixmap = QPixmap(64, 64) - greenPixmap.fill(Qt.green) - greenLabel.setPixmap(greenPixmap) - greenLabel.move(150, 20) - greenLabel.show() - greenLabel.setAttribute(Qt.WA_DeleteOnClose) - - # Blue label - blueLabel = QLabel(self) - bluePixmap = QPixmap(64, 64) - bluePixmap.fill(Qt.blue) - blueLabel.setPixmap(bluePixmap) - blueLabel.move(20, 150) - blueLabel.show() - blueLabel.setAttribute(Qt.WA_DeleteOnClose) - - print("Created colored label fallbacks") - - def mousePressEvent(self, event: QMouseEvent): - """Remember the start position when mouse is pressed""" - if event.button() == Qt.LeftButton: - self.startPos = event.position().toPoint() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event: QMouseEvent): - """Start drag operation if mouse moved beyond threshold""" - if event.buttons() & Qt.LeftButton: - # Calculate distance moved - distance = (event.position().toPoint() - self.startPos).manhattanLength() - - # Check if distance exceeds drag start distance - if distance >= QApplication.startDragDistance(): - # Get the child widget at the current position - child = self.childAt(event.position().toPoint()) - - if not child or not isinstance(child, QLabel): - return - - # Get pixmap from child - pixmap = child.pixmap() - if not pixmap: - return - - # Calculate the offset (hotspot) - offset = event.position().toPoint() - child.pos() - - # Create custom mime data - mime_data = PixmapMime(pixmap, offset, "Item icon") - - # Create drag object - drag = QDrag(self) - drag.setMimeData(mime_data) - drag.setPixmap(pixmap) - drag.setHotSpot(offset) - - # Apply blur effect to original label during drag - temp_pixmap = QPixmap(pixmap) - painter = QPainter(temp_pixmap) - painter.fillRect(temp_pixmap.rect(), QColor(127, 127, 127, 127)) - painter.end() - child.setPixmap(temp_pixmap) - - # Execute drag operation - if drag.exec(Qt.MoveAction | Qt.CopyAction, Qt.CopyAction) == Qt.MoveAction: - # Move operation - close the original child widget - print("Moving data") - child.close() - else: - # Copy operation - restore the original pixmap - print("Copying data") - child.setPixmap(pixmap) - - def dragEnterEvent(self, event: QDragEnterEvent): - """Handle drag enter events""" - mime_data = event.mimeData() - if isinstance(mime_data, PixmapMime): - if event.source() == self: - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.acceptProposedAction() - else: - event.ignore() - - def dragMoveEvent(self, event: QDragMoveEvent): - """Handle drag move events""" - mime_data = event.mimeData() - if isinstance(mime_data, PixmapMime): - if event.source() == self: - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.acceptProposedAction() - else: - event.ignore() - - def dragLeaveEvent(self, event: QDragLeaveEvent): - """Handle drag leave events""" - super().dragLeaveEvent(event) - - def dropEvent(self, event: QDropEvent): - """Handle drop events, creating new label widgets""" - mime_data = event.mimeData() - - if isinstance(mime_data, PixmapMime): - # Get pixmap and offset from the mime data - pixmap = mime_data.pix() - offset = mime_data.offset() - - # Create new label with the pixmap - new_label = QLabel(self) - new_label.setPixmap(pixmap) - new_label.move(event.position().toPoint() - offset) - new_label.show() - new_label.setAttribute(Qt.WA_DeleteOnClose) - - # Set the drop action - if event.source() == self: - event.setDropAction(Qt.MoveAction) - event.accept() - else: - event.acceptProposedAction() - else: - event.ignore() - - def paintEvent(self, event): - """Paint a rounded rectangle border around the container""" - painter = QPainter(self) - painter.drawRoundedRect(0, 5, self.width() - 10, self.height() - 10, 3, 3) - super().paintEvent(event) \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/main.py b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/main.py index ef60b41..23a740c 100644 --- a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/main.py +++ b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/main.py @@ -1,14 +1,33 @@ import sys -from PySide6.QtWidgets import QApplication -from widget import Widget -import resource_rc # Import the resource file +from pathlib import Path +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtCore import QObject, Slot, Signal, Property, QUrl +from resource_controller import ResourceController + +import resource_rc def main(): - app = QApplication(sys.argv) - window = Widget() - window.resize(800, 600) - window.show() + # Create application + app = QGuiApplication(sys.argv) + + # Create controller and QML engine + controller = ResourceController() + engine = QQmlApplicationEngine() + + # Expose controller to QML + engine.rootContext().setContextProperty("resourceController", controller) + + # Load QML file + qml_file = Path(__file__).resolve().parent / "main.qml" + engine.load(qml_file) + + # Check if loading was successful + if not engine.rootObjects(): + print("Error: Could not load QML file") + sys.exit(-1) + # Run the event loop return app.exec() if __name__ == "__main__": diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/main.qml b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/main.qml new file mode 100644 index 0000000..334f664 --- /dev/null +++ b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/main.qml @@ -0,0 +1,60 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts + +Window { + id: mainWindow + width: 800 + height: 600 + visible: true + title: "Drag and Drop with Custom MIME Data" + color: "#f5f5f5" + + // Main layout + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + // Title text + Text { + Layout.fillWidth: true + text: "Drag and drop icons between containers" + font.pixelSize: 16 + horizontalAlignment: Text.AlignHCenter + } + + // Container with two drag-drop areas side by side + SplitView { + Layout.fillWidth: true + Layout.fillHeight: true + orientation: Qt.Horizontal + + // Left container + ContainerArea { + id: leftContainer + SplitView.minimumWidth: 150 + SplitView.preferredWidth: 380 + SplitView.fillHeight: true + } + + // Right container + ContainerArea { + id: rightContainer + SplitView.minimumWidth: 150 + SplitView.fillWidth: true + SplitView.fillHeight: true + } + } + + // Instructions text + Text { + Layout.fillWidth: true + text: "Drag icons to copy them. Hold Shift while dragging to move them instead." + font.pixelSize: 12 + color: "#666666" + horizontalAlignment: Text.AlignHCenter + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/pixmapmime.py b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/pixmapmime.py deleted file mode 100644 index 09bb51c..0000000 --- a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/pixmapmime.py +++ /dev/null @@ -1,51 +0,0 @@ -from PySide6.QtCore import QMimeData - -class PixmapMime(QMimeData): - """Custom QMimeData subclass that can store a pixmap and offset information""" - - def __init__(self, pix, offset, description): - """ - Constructor for PixmapMime - - Args: - pix (QPixmap): The pixmap to store - offset (QPoint): The offset point (hotspot) - description (str): A text description of the item - """ - super().__init__() - self.mPix = pix - self.mOffset = offset - self.description = description - self.mimeFormats = ["text/html", "text/plain"] - - def pix(self): - """Get the stored pixmap""" - return self.mPix - - def offset(self): - """Get the stored offset point""" - return self.mOffset - - def formats(self): - """Override to provide the supported MIME formats""" - # In PySide6, we return a list of strings instead of QStringList - return self.mimeFormats - - def retrieveData(self, mimetype, preferredType): - """ - Override to provide data for each supported MIME type - - Args: - mimetype (str): The requested MIME type - preferredType (QMetaType): The preferred data type - - Returns: - QVariant: The data in the requested format - """ - if mimetype == "text/plain": - return self.description - elif mimetype == "text/html": - html_string = "

" + self.description + "

" - return html_string - else: - return super().retrieveData(mimetype, preferredType) \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/resource_controller.py b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/resource_controller.py new file mode 100644 index 0000000..085ae25 --- /dev/null +++ b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/resource_controller.py @@ -0,0 +1,30 @@ +from PySide6.QtCore import QObject, Slot, Signal, Property + +class ResourceController(QObject): + """Controller class to provide resource paths to QML""" + + def __init__(self, parent=None): + super().__init__(parent) + + @Property(str) + def qtIconPath(self): + return "qrc:/images/qt.png" + + @Property(str) + def cppIconPath(self): + return "qrc:/images/cpp.png" + + @Property(str) + def terminalIconPath(self): + return "qrc:/images/terminal.png" + + @Slot(str, result=str) + def getIconDescription(self, source): + """Get a description for an icon based on its source path""" + if source.endswith("qt.png"): + return "Qt Icon" + elif source.endswith("cpp.png"): + return "C++ Icon" + elif source.endswith("terminal.png"): + return "Terminal Icon" + return "Unknown Icon" \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/ui_widget.py b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/ui_widget.py deleted file mode 100644 index 7868ac8..0000000 --- a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/ui_widget.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'widget.ui' -## -## Created by: Qt User Interface Compiler version 6.8.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QSizePolicy, QVBoxLayout, QWidget) - -class Ui_Widget(object): - def setupUi(self, Widget): - if not Widget.objectName(): - Widget.setObjectName(u"Widget") - Widget.resize(621, 570) - self.verticalLayout_2 = QVBoxLayout(Widget) - self.verticalLayout_2.setSpacing(6) - self.verticalLayout_2.setContentsMargins(11, 11, 11, 11) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.verticalLayout = QVBoxLayout() - self.verticalLayout.setSpacing(6) - self.verticalLayout.setObjectName(u"verticalLayout") - - self.verticalLayout_2.addLayout(self.verticalLayout) - - - self.retranslateUi(Widget) - - QMetaObject.connectSlotsByName(Widget) - # setupUi - - def retranslateUi(self, Widget): - Widget.setWindowTitle(QCoreApplication.translate("Widget", u"Widget", None)) - # retranslateUi - diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/widget.py b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/widget.py deleted file mode 100644 index a53997d..0000000 --- a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/widget.py +++ /dev/null @@ -1,24 +0,0 @@ -from PySide6.QtWidgets import QWidget, QSplitter -from ui_widget import Ui_Widget -from container import Container - -class Widget(QWidget): - """Main widget that contains multiple container widgets""" - - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_Widget() - self.ui.setupUi(self) - - # Create a splitter with two container widgets - splitter = QSplitter(self) - - # Add two container widgets to the splitter - splitter.addWidget(Container(self)) - splitter.addWidget(Container(self)) - - # Add the splitter to the layout - self.ui.verticalLayout.addWidget(splitter) - - # Set window title - self.setWindowTitle("Drag and Drop with Custom MIME Data") \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/widget.ui b/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/widget.ui deleted file mode 100644 index c63e94b..0000000 --- a/Chap4-DragDropClipboard/4-4DataTransferSubclassMimeDataDemo/widget.ui +++ /dev/null @@ -1,25 +0,0 @@ - - - Widget - - - - 0 - 0 - 621 - 570 - - - - Widget - - - - - - - - - - - From 32b22b6c4f6b10a61ea688017c867c30b75e9e9f Mon Sep 17 00:00:00 2001 From: EricoDeMecha Date: Tue, 18 Mar 2025 17:31:19 +0300 Subject: [PATCH 5/6] (ch4): ported ch4/4-5 to qt quick --- .../4-5ClipboardDemo/README.md | 0 .../4-5ClipboardDemo/clipboard_controller.py | 78 +++++++++++++++ .../4-5ClipboardDemo/main.py | 30 ++++-- .../4-5ClipboardDemo/main.qml | 77 +++++++++++++++ .../4-5ClipboardDemo/ui_widget.py | 45 --------- .../4-5ClipboardDemo/widget.py | 95 ------------------- .../4-5ClipboardDemo/widget.ui | 29 ------ 7 files changed, 178 insertions(+), 176 deletions(-) delete mode 100644 Chap4-DragDropClipboard/4-5ClipboardDemo/README.md create mode 100644 Chap4-DragDropClipboard/4-5ClipboardDemo/clipboard_controller.py create mode 100644 Chap4-DragDropClipboard/4-5ClipboardDemo/main.qml delete mode 100644 Chap4-DragDropClipboard/4-5ClipboardDemo/ui_widget.py delete mode 100644 Chap4-DragDropClipboard/4-5ClipboardDemo/widget.py delete mode 100644 Chap4-DragDropClipboard/4-5ClipboardDemo/widget.ui diff --git a/Chap4-DragDropClipboard/4-5ClipboardDemo/README.md b/Chap4-DragDropClipboard/4-5ClipboardDemo/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/Chap4-DragDropClipboard/4-5ClipboardDemo/clipboard_controller.py b/Chap4-DragDropClipboard/4-5ClipboardDemo/clipboard_controller.py new file mode 100644 index 0000000..7de5320 --- /dev/null +++ b/Chap4-DragDropClipboard/4-5ClipboardDemo/clipboard_controller.py @@ -0,0 +1,78 @@ +from PySide6.QtCore import QObject, Slot, Signal, Property, QUrl, QTemporaryFile, QDir +from PySide6.QtGui import QGuiApplication +import os + +class ClipboardController(QObject): + """Controller class to handle clipboard operations""" + + # Signal to notify QML when a new image is available + imageChanged = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self._imageUrl = "" + self._hasImage = False + self._tempFile = None + + @Property(bool, notify=imageChanged) + def hasImage(self): + """Property that indicates if an image is available""" + return self._hasImage + + @Property(str, notify=imageChanged) + def imageUrl(self): + """Property that provides the image URL""" + return self._imageUrl + + @Slot(result=bool) + def paste(self): + """Handle paste operation - paste image from clipboard""" + # Get the clipboard and its MIME data + clipboard = QGuiApplication.clipboard() + mime_data = clipboard.mimeData() + + # Check if the clipboard contains URLs (e.g., files) + if mime_data.hasUrls(): + urls = mime_data.urls() + if len(urls) != 1: + return False + + # Get the file path from the URL + file_path = urls[0].toLocalFile() + + # Check if it's an image and display it + if self.isImage(file_path): + self._imageUrl = QUrl.fromLocalFile(file_path).toString() + self._hasImage = True + self.imageChanged.emit() + return True + + # Also check for image data directly + elif mime_data.hasImage(): + # Save the image to a temporary file + image = mime_data.imageData() + if not image.isNull(): + # Clean up previous temp file if it exists + if self._tempFile: + self._tempFile.remove() + + # Create a new temporary file with the .png extension + self._tempFile = QTemporaryFile(QDir.tempPath() + "/XXXXXX.png") + if self._tempFile.open(): + # Save the image to the temporary file + image.save(self._tempFile.fileName(), "PNG") + self._tempFile.close() + + # Set the image URL to the temporary file + self._imageUrl = QUrl.fromLocalFile(self._tempFile.fileName()).toString() + self._hasImage = True + self.imageChanged.emit() + return True + + return False + + def isImage(self, file_path): + """Check if a file is a supported image format""" + _, ext = os.path.splitext(file_path) + ext = ext.lower() + return ext in [".png", ".jpg", ".jpeg"] \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-5ClipboardDemo/main.py b/Chap4-DragDropClipboard/4-5ClipboardDemo/main.py index 23ea817..479fbba 100644 --- a/Chap4-DragDropClipboard/4-5ClipboardDemo/main.py +++ b/Chap4-DragDropClipboard/4-5ClipboardDemo/main.py @@ -1,14 +1,30 @@ import sys -from PySide6.QtWidgets import QApplication -from widget import Widget +from pathlib import Path +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine +from clipboard_controller import ClipboardController def main(): - app = QApplication(sys.argv) - window = Widget() - window.setWindowTitle("Clipboard Image Paste Demo") - window.resize(600, 400) - window.show() + # Create application + app = QGuiApplication(sys.argv) + # Create controller and QML engine + controller = ClipboardController() + engine = QQmlApplicationEngine() + + # Expose controller to QML + engine.rootContext().setContextProperty("clipboardController", controller) + + # Load QML file + qml_file = Path(__file__).resolve().parent / "main.qml" + engine.load(qml_file) + + # Check if loading was successful + if not engine.rootObjects(): + print("Error: Could not load QML file") + sys.exit(-1) + + # Run the event loop return app.exec() if __name__ == "__main__": diff --git a/Chap4-DragDropClipboard/4-5ClipboardDemo/main.qml b/Chap4-DragDropClipboard/4-5ClipboardDemo/main.qml new file mode 100644 index 0000000..0848a16 --- /dev/null +++ b/Chap4-DragDropClipboard/4-5ClipboardDemo/main.qml @@ -0,0 +1,77 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts + +Window { + id: mainWindow + width: 600 + height: 400 + visible: true + title: "Clipboard Image Paste Demo" + color: "#f5f5f5" + + // Create a global shortcut handler + Shortcut { + sequence: StandardKey.Paste + onActivated: { + console.log("Paste shortcut activated") + clipboardController.paste() + } + } + + // Main layout + ColumnLayout { + id: mainLayout + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + // Image display area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "white" + border.color: "lightgray" + border.width: 1 + + // Label when no image is available + Text { + anchors.centerIn: parent + text: "Press Ctrl+V to paste an image from clipboard" + font.pixelSize: 14 + visible: !clipboardController.hasImage + } + + // Image component for displaying pasted images + Image { + id: pastedImage + anchors.fill: parent + anchors.margins: 5 + source: clipboardController.imageUrl + fillMode: Image.PreserveAspectFit + visible: clipboardController.hasImage + + // When the source is invalid, hide the image + onStatusChanged: { + if (status === Image.Error) { + visible = false; + } + } + } + + // Make it clickable to get focus + MouseArea { + anchors.fill: parent + onClicked: parent.forceActiveFocus() + } + } + + // Alternative paste button + Button { + text: "Paste (Ctrl+V)" + Layout.alignment: Qt.AlignHCenter + onClicked: clipboardController.paste() + } + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-5ClipboardDemo/ui_widget.py b/Chap4-DragDropClipboard/4-5ClipboardDemo/ui_widget.py deleted file mode 100644 index 3b517eb..0000000 --- a/Chap4-DragDropClipboard/4-5ClipboardDemo/ui_widget.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'widget.ui' -## -## Created by: Qt User Interface Compiler version 6.8.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QLabel, QSizePolicy, QVBoxLayout, - QWidget) - -class Ui_Widget(object): - def setupUi(self, Widget): - if not Widget.objectName(): - Widget.setObjectName(u"Widget") - Widget.resize(400, 300) - self.verticalLayout = QVBoxLayout(Widget) - self.verticalLayout.setSpacing(6) - self.verticalLayout.setContentsMargins(11, 11, 11, 11) - self.verticalLayout.setObjectName(u"verticalLayout") - self.label = QLabel(Widget) - self.label.setObjectName(u"label") - - self.verticalLayout.addWidget(self.label) - - - self.retranslateUi(Widget) - - QMetaObject.connectSlotsByName(Widget) - # setupUi - - def retranslateUi(self, Widget): - Widget.setWindowTitle(QCoreApplication.translate("Widget", u"Widget", None)) - self.label.setText(QCoreApplication.translate("Widget", u"TextLabel", None)) - # retranslateUi - diff --git a/Chap4-DragDropClipboard/4-5ClipboardDemo/widget.py b/Chap4-DragDropClipboard/4-5ClipboardDemo/widget.py deleted file mode 100644 index 2fd660b..0000000 --- a/Chap4-DragDropClipboard/4-5ClipboardDemo/widget.py +++ /dev/null @@ -1,95 +0,0 @@ -from PySide6.QtWidgets import QWidget, QApplication -from PySide6.QtCore import Qt, QUrl, QFileInfo -from PySide6.QtGui import QKeyEvent, QKeySequence, QPixmap -from ui_widget import Ui_Widget -import os - -class Widget(QWidget): - """Main widget that demonstrates clipboard operations""" - - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_Widget() - self.ui.setupUi(self) - - # Set focus policy to enable key events - self.setFocusPolicy(Qt.StrongFocus) - - # Set a default text for the label - self.ui.label.setText("Press Ctrl+V to paste an image from clipboard") - self.ui.label.setAlignment(Qt.AlignCenter) - - def keyPressEvent(self, event: QKeyEvent): - """Handle key press events for clipboard operations""" - if event.matches(QKeySequence.Copy): - self.copy() - event.accept() - print("Copy sequence detected") - elif event.matches(QKeySequence.Paste): - self.paste() - event.accept() - print("Paste sequence detected") - else: - super().keyPressEvent(event) - - def isImage(self, file_path): - """Check if a file is a supported image format""" - _, ext = os.path.splitext(file_path) - ext = ext.lower() - return ext in [".png", ".jpg", ".jpeg"] - - def copy(self): - """Handle copy operation - currently not implemented""" - # No operation in this example - pass - - def paste(self): - """Handle paste operation - paste image from clipboard""" - # Get the clipboard and its MIME data - clipboard = QApplication.clipboard() - mime_data = clipboard.mimeData() - - # Check if the clipboard contains URLs (e.g., files) - if mime_data.hasUrls(): - urls = mime_data.urls() - if len(urls) != 1: - return - - # Get the file path from the URL - file_path = urls[0].toLocalFile() - - # Check if it's an image and display it - if self.isImage(file_path): - pixmap = QPixmap(file_path) - if not pixmap.isNull(): - # Scale the pixmap to fit the label while maintaining aspect ratio - self.ui.label.setPixmap(pixmap.scaled( - self.ui.label.size(), - Qt.KeepAspectRatio, - Qt.SmoothTransformation - )) - # Also check for image data directly - elif mime_data.hasImage(): - pixmap = QPixmap(mime_data.imageData()) - if not pixmap.isNull(): - self.ui.label.setPixmap(pixmap.scaled( - self.ui.label.size(), - Qt.KeepAspectRatio, - Qt.SmoothTransformation - )) - - def resizeEvent(self, event): - """Handle resize events to scale the image""" - if self.ui.label.pixmap() and not self.ui.label.pixmap().isNull(): - # Store the original pixmap if we haven't already - if not hasattr(self, 'original_pixmap'): - self.original_pixmap = self.ui.label.pixmap() - - # Scale the pixmap to fit the new size - self.ui.label.setPixmap(self.original_pixmap.scaled( - self.ui.label.size(), - Qt.KeepAspectRatio, - Qt.SmoothTransformation - )) - - super().resizeEvent(event) \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-5ClipboardDemo/widget.ui b/Chap4-DragDropClipboard/4-5ClipboardDemo/widget.ui deleted file mode 100644 index 2492927..0000000 --- a/Chap4-DragDropClipboard/4-5ClipboardDemo/widget.ui +++ /dev/null @@ -1,29 +0,0 @@ - - - Widget - - - - 0 - 0 - 400 - 300 - - - - Widget - - - - - - TextLabel - - - - - - - - - From 20630b953fa7dc5cbb81b56fb5d16168093c3f01 Mon Sep 17 00:00:00 2001 From: EricoDeMecha Date: Tue, 18 Mar 2025 17:39:22 +0300 Subject: [PATCH 6/6] (ch4): ported ch4/4-6 to qt quick --- .../4-6PainterAppClipboard/README.md | 190 ----------- .../4-6PainterAppClipboard/main.py | 37 ++- .../4-6PainterAppClipboard/main.qml | 247 ++++++++++++++ .../4-6PainterAppClipboard/mainwindow.py | 131 -------- .../4-6PainterAppClipboard/mainwindow.ui | 40 --- .../4-6PainterAppClipboard/paint_canvas.py | 263 +++++++++++++++ .../paint_controller.py | 64 ++++ .../4-6PainterAppClipboard/paintcanvas.py | 305 ------------------ .../4-6PainterAppClipboard/ui_mainwindow.py | 48 --- 9 files changed, 605 insertions(+), 720 deletions(-) delete mode 100644 Chap4-DragDropClipboard/4-6PainterAppClipboard/README.md create mode 100644 Chap4-DragDropClipboard/4-6PainterAppClipboard/main.qml delete mode 100644 Chap4-DragDropClipboard/4-6PainterAppClipboard/mainwindow.py delete mode 100644 Chap4-DragDropClipboard/4-6PainterAppClipboard/mainwindow.ui create mode 100644 Chap4-DragDropClipboard/4-6PainterAppClipboard/paint_canvas.py create mode 100644 Chap4-DragDropClipboard/4-6PainterAppClipboard/paint_controller.py delete mode 100644 Chap4-DragDropClipboard/4-6PainterAppClipboard/paintcanvas.py delete mode 100644 Chap4-DragDropClipboard/4-6PainterAppClipboard/ui_mainwindow.py diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/README.md b/Chap4-DragDropClipboard/4-6PainterAppClipboard/README.md deleted file mode 100644 index f2a1fdb..0000000 --- a/Chap4-DragDropClipboard/4-6PainterAppClipboard/README.md +++ /dev/null @@ -1,190 +0,0 @@ -# Paint Application with Clipboard Support in PySide6 - -This is a drawing application built with PySide6 that includes clipboard support for copying and pasting images. It provides various drawing tools, color options, and clipboard integration. - -## Features - -- **Drawing Tools**: Pen, Rectangle, Ellipse, and Eraser -- **Color Options**: Customizable pen and fill colors -- **Clipboard Integration**: Copy canvas content to clipboard and paste images from clipboard -- **Interactive UI**: Toolbar with drawing options and status bar feedback - -## Project Structure - -``` -project/ -├── main.py # Application entry point -├── mainwindow.py # Main window with toolbar and UI -├── paintcanvas.py # Custom drawing canvas with tools and clipboard support -├── ui_mainwindow.py # Generated UI code from mainwindow.ui -├── resource_rc.py # Generated resource code from resource.qrc -└── images/ # Directory for tool icons - ├── about.png - ├── circle.png - ├── close.png - ├── eraser.png - ├── open.png - ├── pen.png - ├── rectangle.png - └── save.png -``` - -## Key Components - -### PaintCanvas Widget - -The `PaintCanvas` class is a custom widget that handles: -- Drawing operations with different tools -- Clipboard operations (copy/paste) -- Image management and rendering - -```python -class PaintCanvas(QWidget): - # Tool type enum - Pen, Rect, Ellipse, Eraser = range(4) - - # Methods for drawing and clipboard operations - def copy(self): - # Copy canvas to clipboard - - def paste(self): - # Paste image from clipboard -``` - -### Clipboard Integration - -The application implements copy and paste operations using Qt's clipboard system: - -```python -def copy(self): - """Copy the canvas image to clipboard""" - clipboard = QApplication.clipboard() - mime_data = clipboard.mimeData() - mime_data.setImageData(self.image) - clipboard.setMimeData(mime_data) - -def paste(self): - """Paste image from clipboard to canvas""" - # Get data from clipboard - mime_data = QApplication.clipboard().mimeData() - - # Handle different clipboard data types - if mime_data.hasUrls(): - # Handle image files - elif mime_data.hasImage(): - # Handle direct image data -``` - -### Main Window - -The `MainWindow` class creates the UI and connects the tools: - -```python -class MainWindow(QMainWindow): - def __init__(self, parent=None): - # Create canvas - self.canvas = PaintCanvas(self) - self.setCentralWidget(self.canvas) - - # Create and connect toolbar controls - # ... -``` - -## Drawing Tools - -The application supports four drawing tools: - -1. **Pen Tool**: Freeform drawing with the current pen color and width -2. **Rectangle Tool**: Draw rectangles with optional fill -3. **Ellipse Tool**: Draw ellipses with optional fill -4. **Eraser Tool**: Erase content in a rectangular area - -## Clipboard Usage - -The application supports the following clipboard operations: - -- **Copy (Ctrl+C)**: Copy the entire canvas to the clipboard -- **Paste (Ctrl+V)**: - - Paste an image file from the clipboard (if a file was copied) - - Paste an image directly from the clipboard (if image data was copied) - -## Running the Application - -1. Generate the UI Python file (if mainwindow.ui changes): - ``` - pyside6-uic mainwindow.ui -o ui_mainwindow.py - ``` - -2. Generate the resource Python file (if resources.qrc changes): - ``` - pyside6-rcc resource.qrc -o resource_rc.py - ``` - -3. Ensure PySide6 is installed: - ``` - pip install PySide6 - ``` - -4. Run the application: - ``` - python main.py - ``` - -## Using the Application - -1. **Drawing**: - - Select a tool from the toolbar - - Choose pen width, colors, and fill options - - Draw on the canvas using the mouse - -2. **Clipboard Operations**: - - Press Ctrl+C to copy the entire canvas to the clipboard - - Press Ctrl+V to paste an image from the clipboard - - You can copy images from other applications or files and paste them into the canvas - -## Implementation Notes - -### Event Handling - -The application uses Qt's event system to handle keyboard shortcuts: - -```python -def keyPressEvent(self, event): - """Handle key press events for clipboard operations""" - if event.matches(QKeySequence.Copy): - self.copy() - event.accept() - elif event.matches(QKeySequence.Paste): - self.paste() - event.accept() - else: - super().keyPressEvent(event) -``` - -### Image Format Validation - -When pasting from clipboard URLs, the application validates image formats: - -```python -def isImage(self, file_path): - """Check if a file is a supported image format""" - _, ext = os.path.splitext(file_path) - ext = ext.lower() - return ext in [".png", ".jpg", ".jpeg"] -``` - -### QImage vs QPixmap - -- `QImage` is used for the main canvas since it provides direct pixel access -- `QPixmap` is used for pasting operations since it's optimized for display - -## Extending the Project - -This project could be extended with: - -1. **File Operations**: Add save and load functionality -2. **More Tools**: Add line, polygon, or text tools -3. **Selection Tool**: Add a tool to select and move parts of the drawing -4. **Layers**: Implement a layer system for more complex drawings -5. **Undo/Redo**: Add undo and redo capabilities -6. **More Clipboard Formats**: Support for other clipboard formats like SVG or text \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/main.py b/Chap4-DragDropClipboard/4-6PainterAppClipboard/main.py index fdc47b0..c1769c9 100644 --- a/Chap4-DragDropClipboard/4-6PainterAppClipboard/main.py +++ b/Chap4-DragDropClipboard/4-6PainterAppClipboard/main.py @@ -1,13 +1,38 @@ import sys -from PySide6.QtWidgets import QApplication -from mainwindow import MainWindow +from pathlib import Path +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType +from PySide6.QtCore import QUrl + +from paint_canvas import PaintCanvas +from paint_controller import PaintController + +import resource_rc def main(): - app = QApplication(sys.argv) - window = MainWindow() - window.resize(950, 684) - window.show() + # Create application + app = QGuiApplication(sys.argv) + + # Register custom types + qmlRegisterType(PaintCanvas, "PaintApp", 1, 0, "PaintCanvas") + + # Create QML engine + engine = QQmlApplicationEngine() + + # Create and expose controller to QML + controller = PaintController() + engine.rootContext().setContextProperty("paintController", controller) + + # Load QML file + qml_file = Path(__file__).resolve().parent / "main.qml" + engine.load(qml_file) + + # Check if loading was successful + if not engine.rootObjects(): + print("Error: Could not load QML file") + sys.exit(-1) + # Run the event loop return app.exec() if __name__ == "__main__": diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/main.qml b/Chap4-DragDropClipboard/4-6PainterAppClipboard/main.qml new file mode 100644 index 0000000..a7cdc7c --- /dev/null +++ b/Chap4-DragDropClipboard/4-6PainterAppClipboard/main.qml @@ -0,0 +1,247 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs +import PaintApp 1.0 + +Window { + id: mainWindow + width: 950 + height: 684 + visible: true + title: "Paint with Clipboard Support" + color: "#f5f5f5" + + // Create global shortcut handlers + Shortcut { + sequences: [StandardKey.Copy] + onActivated: { + console.log("Copy shortcut activated") + canvas.copy() + } + } + + Shortcut { + sequences: [StandardKey.Paste] + onActivated: { + console.log("Paste shortcut activated") + canvas.paste() + } + } + + // Main layout + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Toolbar component + ToolBar { + id: toolbar + Layout.fillWidth: true + Layout.preferredHeight: 50 + + background: Rectangle { + color: "#f0f0f0" + border.color: "#d0d0d0" + border.width: 1 + } + + RowLayout { + anchors.fill: parent + anchors.margins: 5 + spacing: 10 + + // Pen width controls + Label { + text: "Pen Width" + Layout.alignment: Qt.AlignVCenter + } + + SpinBox { + id: penWidthSpinBox + from: 1 + to: 15 + value: canvas.penWidth + onValueChanged: canvas.penWidth = value + Layout.alignment: Qt.AlignVCenter + } + + // Pen color controls + Label { + text: "Pen Color" + Layout.alignment: Qt.AlignVCenter + } + + Rectangle { + id: penColorButton + width: 30 + height: 30 + color: canvas.penColor + border.color: "black" + border.width: 1 + Layout.alignment: Qt.AlignVCenter + + MouseArea { + anchors.fill: parent + onClicked: penColorDialog.open() + } + } + + // Fill color controls + Label { + text: "Fill Color" + Layout.alignment: Qt.AlignVCenter + } + + Rectangle { + id: fillColorButton + width: 30 + height: 30 + color: canvas.fillColor + border.color: "black" + border.width: 1 + Layout.alignment: Qt.AlignVCenter + + MouseArea { + anchors.fill: parent + onClicked: fillColorDialog.open() + } + } + + // Fill checkbox + CheckBox { + id: fillCheckBox + text: "Fill Shape" + checked: canvas.fill + onCheckedChanged: canvas.fill = checked + Layout.alignment: Qt.AlignVCenter + } + + // Spacer + Item { + Layout.fillWidth: true + } + + // Tool buttons + ToolButton { + id: penButton + icon.source: paintController.penIconPath + checked: canvas.tool === 0 + onClicked: { + canvas.tool = 0; + paintController.setTool(0); + } + ToolTip.visible: hovered + ToolTip.text: "Pen Tool" + } + + ToolButton { + id: rectButton + icon.source: paintController.rectIconPath + checked: canvas.tool === 1 + onClicked: { + canvas.tool = 1; + paintController.setTool(1); + } + ToolTip.visible: hovered + ToolTip.text: "Rectangle Tool" + } + + ToolButton { + id: ellipseButton + icon.source: paintController.ellipseIconPath + checked: canvas.tool === 2 + onClicked: { + canvas.tool = 2; + paintController.setTool(2); + } + ToolTip.visible: hovered + ToolTip.text: "Ellipse Tool" + } + + ToolButton { + id: eraserButton + icon.source: paintController.eraserIconPath + checked: canvas.tool === 3 + onClicked: { + canvas.tool = 3; + paintController.setTool(3); + } + ToolTip.visible: hovered + ToolTip.text: "Eraser Tool" + } + } + } + + // Canvas area + PaintCanvas { + id: canvas + Layout.fillWidth: true + Layout.fillHeight: true + + // Connect mouse events to canvas methods + MouseArea { + id: canvasMouseArea + anchors.fill: parent + hoverEnabled: true + + onPressed: function(mouse) { + canvas.handleMousePress(Qt.point(mouse.x, mouse.y)) + } + + onPositionChanged: function(mouse) { + if (pressed) { + canvas.handleMouseMove(Qt.point(mouse.x, mouse.y)) + } + } + + onReleased: function(mouse) { + canvas.handleMouseRelease(Qt.point(mouse.x, mouse.y)) + } + } + } + + // Status bar + Rectangle { + id: statusBar + Layout.fillWidth: true + Layout.preferredHeight: 30 + color: "#f0f0f0" + border.color: "#d0d0d0" + border.width: 1 + + Text { + anchors.fill: parent + anchors.margins: 5 + text: "Current tool: " + paintController.getToolName(paintController.currentTool) + + " | Press Ctrl+C to copy and Ctrl+V to paste images" + verticalAlignment: Text.AlignVCenter + } + } + } + + // Color dialogs + ColorDialog { + id: penColorDialog + title: "Select Pen Color" + selectedColor: canvas.penColor + onAccepted: canvas.penColor = selectedColor + } + + ColorDialog { + id: fillColorDialog + title: "Select Fill Color" + selectedColor: canvas.fillColor + onAccepted: canvas.fillColor = selectedColor + } + + // Resize the canvas image when window size changes + onWidthChanged: canvas.resizeImage(width, height) + onHeightChanged: canvas.resizeImage(width, height) + + Component.onCompleted: { + // Initial resize of the canvas + canvas.resizeImage(width, height) + } +} \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/mainwindow.py b/Chap4-DragDropClipboard/4-6PainterAppClipboard/mainwindow.py deleted file mode 100644 index 710048c..0000000 --- a/Chap4-DragDropClipboard/4-6PainterAppClipboard/mainwindow.py +++ /dev/null @@ -1,131 +0,0 @@ -from PySide6.QtWidgets import (QMainWindow, QLabel, QSpinBox, QColorDialog, - QPushButton, QCheckBox) -from PySide6.QtGui import QIcon, QColor -from PySide6.QtCore import Slot -from ui_mainwindow import Ui_MainWindow -from paintcanvas import PaintCanvas -import resource_rc # Import resources - -class MainWindow(QMainWindow): - """Main window for the Paint Application""" - - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_MainWindow() - self.ui.setupUi(self) - - # Create and set the canvas as central widget - self.canvas = PaintCanvas(self) - self.setCentralWidget(self.canvas) - - # Create toolbar controls - - # Pen width controls - penWidthLabel = QLabel("Pen Width", self) - penWidthSpinBox = QSpinBox(self) - penWidthSpinBox.setValue(2) - penWidthSpinBox.setRange(1, 15) - - # Pen color controls - penColorLabel = QLabel("Pen Color", self) - self.penColorButton = QPushButton(self) - - # Fill color controls - fillColorLabel = QLabel("Fill Color", self) - self.fillColorButton = QPushButton(self) - - # Fill checkbox - self.fillCheckBox = QCheckBox("Fill Shape", self) - - # Tool buttons - rectButton = QPushButton(self) - rectButton.setIcon(QIcon(":/images/rectangle.png")) - - penButton = QPushButton(self) - penButton.setIcon(QIcon(":/images/pen.png")) - - ellipseButton = QPushButton(self) - ellipseButton.setIcon(QIcon(":/images/circle.png")) - - eraserButton = QPushButton(self) - eraserButton.setIcon(QIcon(":/images/eraser.png")) - - # Connect tool button signals - rectButton.clicked.connect(lambda: self.setTool(PaintCanvas.Rect)) - penButton.clicked.connect(lambda: self.setTool(PaintCanvas.Pen)) - ellipseButton.clicked.connect(lambda: self.setTool(PaintCanvas.Ellipse)) - eraserButton.clicked.connect(lambda: self.setTool(PaintCanvas.Eraser)) - - # Connect other control signals - penWidthSpinBox.valueChanged.connect(self.penWidthChanged) - self.penColorButton.clicked.connect(self.changePenColor) - self.fillColorButton.clicked.connect(self.changeFillColor) - self.fillCheckBox.clicked.connect(self.changeFillProperty) - - # Add widgets to toolbar - self.ui.mainToolBar.addWidget(penWidthLabel) - self.ui.mainToolBar.addWidget(penWidthSpinBox) - self.ui.mainToolBar.addWidget(penColorLabel) - self.ui.mainToolBar.addWidget(self.penColorButton) - self.ui.mainToolBar.addWidget(fillColorLabel) - self.ui.mainToolBar.addWidget(self.fillColorButton) - self.ui.mainToolBar.addWidget(self.fillCheckBox) - self.ui.mainToolBar.addSeparator() - self.ui.mainToolBar.addWidget(penButton) - self.ui.mainToolBar.addWidget(rectButton) - self.ui.mainToolBar.addWidget(ellipseButton) - self.ui.mainToolBar.addWidget(eraserButton) - - # Set initial button colors - css = f"background-color: {self.canvas.getPenColor().name()}" - self.penColorButton.setStyleSheet(css) - - css = f"background-color: {self.canvas.getFillColor().name()}" - self.fillColorButton.setStyleSheet(css) - - # Set window title - self.setWindowTitle("Paint with Clipboard Support") - - # Show clipboard usage instructions in status bar - self.ui.statusBar.showMessage("Press Ctrl+C to copy and Ctrl+V to paste images") - - def setTool(self, tool): - """Set the current drawing tool""" - self.canvas.setTool(tool) - - # Update status bar message - tool_names = { - PaintCanvas.Pen: "Pen", - PaintCanvas.Rect: "Rectangle", - PaintCanvas.Ellipse: "Ellipse", - PaintCanvas.Eraser: "Eraser" - } - self.ui.statusBar.showMessage(f"Current tool: {tool_names[tool]} | Ctrl+C to copy, Ctrl+V to paste") - - @Slot(int) - def penWidthChanged(self, width): - """Handle pen width change""" - self.canvas.setPenWidth(width) - - @Slot() - def changePenColor(self): - """Handle pen color change""" - color = QColorDialog.getColor(self.canvas.getPenColor()) - if color.isValid(): - self.canvas.setPenColor(color) - css = f"background-color: {color.name()}" - self.penColorButton.setStyleSheet(css) - - @Slot() - def changeFillColor(self): - """Handle fill color change""" - color = QColorDialog.getColor(self.canvas.getFillColor()) - if color.isValid(): - self.canvas.setFillColor(color) - css = f"background-color: {color.name()}" - self.fillColorButton.setStyleSheet(css) - - @Slot() - def changeFillProperty(self): - """Handle fill property change""" - self.canvas.setFill(self.fillCheckBox.isChecked()) \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/mainwindow.ui b/Chap4-DragDropClipboard/4-6PainterAppClipboard/mainwindow.ui deleted file mode 100644 index ec451e3..0000000 --- a/Chap4-DragDropClipboard/4-6PainterAppClipboard/mainwindow.ui +++ /dev/null @@ -1,40 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 950 - 684 - - - - MainWindow - - - - - - 0 - 0 - 950 - 26 - - - - - - TopToolBarArea - - - false - - - - - - - - diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/paint_canvas.py b/Chap4-DragDropClipboard/4-6PainterAppClipboard/paint_canvas.py new file mode 100644 index 0000000..c503a94 --- /dev/null +++ b/Chap4-DragDropClipboard/4-6PainterAppClipboard/paint_canvas.py @@ -0,0 +1,263 @@ +from PySide6.QtQuick import QQuickPaintedItem +from PySide6.QtGui import (QPainter, QPen, QColor, QBrush, QImage, QPixmap) +from PySide6.QtCore import (Qt, QPoint, QRect, QRectF, QSize, Property, Signal, + Slot, QPointF) +from PySide6.QtWidgets import QApplication + +import os + +class PaintCanvas(QQuickPaintedItem): + """Canvas item for drawing with various tools""" + + # Signals + penColorChanged = Signal() + fillColorChanged = Signal() + penWidthChanged = Signal() + fillChanged = Signal() + toolChanged = Signal() + + # Tool type enum (match the values with QML) + PEN, RECT, ELLIPSE, ERASER = range(4) + + def __init__(self, parent=None): + super().__init__(parent) + + # Initialize properties + self._tool = self.PEN + self._fill = False + self._drawing = False + self._penWidth = 3 + self._fillColor = QColor(Qt.red) + self._penColor = QColor(Qt.green) + self._lastPoint = QPointF() + self._lastRect = QRectF(0, 0, 0, 0) + self._lastEraserRect = QRectF(0, 0, 0, 0) + + # Create a white image to paint on + self._image = QImage(1200, 800, QImage.Format.Format_RGB32) + self._image.fill(Qt.white) + + # Enable mouse tracking + self.setAcceptedMouseButtons(Qt.LeftButton) + self.setAcceptHoverEvents(True) + + def paint(self, painter): + """Paint the image on the item""" + painter.drawImage(0, 0, self._image) + + @Property(int, notify=toolChanged) + def tool(self): + """Get the current tool""" + return self._tool + + @tool.setter + def tool(self, value): + """Set the current tool""" + if self._tool != value: + self._tool = value + self.toolChanged.emit() + + @Property(bool, notify=fillChanged) + def fill(self): + """Get the fill property""" + return self._fill + + @fill.setter + def fill(self, value): + """Set the fill property""" + if self._fill != value: + self._fill = value + self.fillChanged.emit() + + @Property(int, notify=penWidthChanged) + def penWidth(self): + """Get the pen width""" + return self._penWidth + + @penWidth.setter + def penWidth(self, value): + """Set the pen width""" + if self._penWidth != value: + self._penWidth = value + self.penWidthChanged.emit() + + @Property(QColor, notify=fillColorChanged) + def fillColor(self): + """Get the fill color""" + return self._fillColor + + @fillColor.setter + def fillColor(self, value): + """Set the fill color""" + if self._fillColor != value: + self._fillColor = QColor(value) + self.fillColorChanged.emit() + + @Property(QColor, notify=penColorChanged) + def penColor(self): + """Get the pen color""" + return self._penColor + + @penColor.setter + def penColor(self, value): + """Set the pen color""" + if self._penColor != value: + self._penColor = QColor(value) + self.penColorChanged.emit() + + @Slot() + def copy(self): + """Copy the canvas image to clipboard""" + clipboard = QApplication.clipboard() + clipboard.setImage(self._image) + print("Image copied to clipboard") + + @Slot() + def paste(self): + """Paste image from clipboard to canvas""" + # Get data from the clipboard + mime_data = QApplication.clipboard().mimeData() + + if mime_data.hasUrls(): + urls = mime_data.urls() + if len(urls) != 1: + return + + file_path = urls[0].toLocalFile() + + if self.isImage(file_path): + # Build the image object + pixmap = QPixmap(file_path) + + # Paint it on the canvas + painter = QPainter(self._image) + painter.setPen(QPen(self._penColor, self._penWidth, Qt.SolidLine, + Qt.RoundCap, Qt.RoundJoin)) + painter.setRenderHint(QPainter.Antialiasing, True) + + painter.drawPixmap( + QRect(10, 10, 300, 300), + pixmap.scaled(300, 300, Qt.KeepAspectRatio), + QRect(0, 0, 300, 300) + ) + + self.update() + elif mime_data.hasImage(): + # If clipboard contains an image, paste it directly + image = mime_data.imageData() + if not image.isNull(): + # Paint it on the canvas + painter = QPainter(self._image) + painter.setPen(QPen(self._penColor, self._penWidth, Qt.SolidLine, + Qt.RoundCap, Qt.RoundJoin)) + painter.setRenderHint(QPainter.Antialiasing, True) + + painter.drawImage( + QRect(10, 10, 300, 300), + image, + image.rect() + ) + + self.update() + + def isImage(self, file_path): + """Check if a file is a supported image format""" + _, ext = os.path.splitext(file_path) + ext = ext.lower() + return ext in [".png", ".jpg", ".jpeg"] + + @Slot(QPointF, QPointF) + def drawLineTo(self, startPoint, endPoint): + """Draw a line from start point to end point""" + painter = QPainter(self._image) + painter.setPen(QPen(self._penColor, self._penWidth, Qt.SolidLine, + Qt.RoundCap, Qt.RoundJoin)) + painter.setRenderHint(QPainter.Antialiasing, True) + painter.drawLine(startPoint, endPoint) + + self.update() + + @Slot(QPointF, QPointF, bool) + def drawRectTo(self, startPoint, endPoint, ellipse=False): + """Draw a rectangle/ellipse from start point to end point""" + painter = QPainter(self._image) + painter.setPen(QPen(self._penColor, self._penWidth, Qt.SolidLine, + Qt.RoundCap, Qt.RoundJoin)) + + # Set brush based on fill property + if self._fill: + painter.setBrush(self._fillColor) + else: + painter.setBrush(Qt.NoBrush) + + # Draw rect or ellipse + rect = QRectF(startPoint, endPoint) + if not ellipse: + painter.drawRect(rect) + else: + painter.drawEllipse(rect) + + self.update() + + @Slot(QPointF) + def eraseAt(self, point): + """Erase content at a specific point""" + painter = QPainter(self._image) + + # Erase the content at the point + eraserSize = 50 # Size of the eraser + rect = QRectF(point.x() - eraserSize/2, point.y() - eraserSize/2, + eraserSize, eraserSize) + + painter.setBrush(Qt.white) + painter.setPen(Qt.white) + painter.drawRect(rect) + + self.update() + + @Slot(QPointF) + def handleMousePress(self, position): + """Handle mouse press from QML""" + self._lastPoint = position + self._drawing = True + + @Slot(QPointF) + def handleMouseMove(self, position): + """Handle mouse move from QML""" + if self._drawing: + if self._tool == self.PEN: + self.drawLineTo(self._lastPoint, position) + self._lastPoint = position + elif self._tool == self.ERASER: + self.eraseAt(position) + + @Slot(QPointF) + def handleMouseRelease(self, position): + """Handle mouse release from QML""" + if self._drawing: + if self._tool == self.PEN: + self.drawLineTo(self._lastPoint, position) + elif self._tool == self.RECT: + self.drawRectTo(self._lastPoint, position, False) + elif self._tool == self.ELLIPSE: + self.drawRectTo(self._lastPoint, position, True) + elif self._tool == self.ERASER: + self.eraseAt(position) + + self._drawing = False + self._lastRect = QRectF(0, 0, 0, 0) + + @Slot(int, int) + def resizeImage(self, width, height): + """Resize the image to a new size""" + if (self._image.width() == width and self._image.height() == height): + return + + newImage = QImage(width, height, QImage.Format.Format_RGB32) + newImage.fill(Qt.white) + + painter = QPainter(newImage) + painter.drawImage(QPoint(0, 0), self._image) + + self._image = newImage + self.update() \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/paint_controller.py b/Chap4-DragDropClipboard/4-6PainterAppClipboard/paint_controller.py new file mode 100644 index 0000000..e1fe1a5 --- /dev/null +++ b/Chap4-DragDropClipboard/4-6PainterAppClipboard/paint_controller.py @@ -0,0 +1,64 @@ +from PySide6.QtCore import QObject, Signal, Slot, Property +from PySide6.QtGui import QColor + +class PaintController(QObject): + """Controller class to provide tool and resource paths to QML""" + + # Signals + toolChanged = Signal() + resourcesChanged = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self._currentTool = 0 # PEN + # Initialize constant resource paths + self._penIconPath = "qrc:/images/pen.png" + self._rectIconPath = "qrc:/images/rectangle.png" + self._ellipseIconPath = "qrc:/images/circle.png" + self._eraserIconPath = "qrc:/images/eraser.png" + + @Property(int, notify=toolChanged) + def currentTool(self): + """Get the current tool""" + return self._currentTool + + @currentTool.setter + def currentTool(self, value): + """Set the current tool""" + if self._currentTool != value: + self._currentTool = value + self.toolChanged.emit() + + @Slot(int) + def setTool(self, tool): + """Set the tool and notify QML""" + self.currentTool = tool + + # Resource paths with NOTIFY signals + @Property(str, constant=True) + def penIconPath(self): + return self._penIconPath + + @Property(str, constant=True) + def rectIconPath(self): + return self._rectIconPath + + @Property(str, constant=True) + def ellipseIconPath(self): + return self._ellipseIconPath + + @Property(str, constant=True) + def eraserIconPath(self): + return self._eraserIconPath + + # Tool name mapping + @Slot(int, result=str) + def getToolName(self, tool): + """Get the name of a tool by its index""" + tool_names = { + 0: "Pen", + 1: "Rectangle", + 2: "Ellipse", + 3: "Eraser" + } + return tool_names.get(tool, "Unknown") \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/paintcanvas.py b/Chap4-DragDropClipboard/4-6PainterAppClipboard/paintcanvas.py deleted file mode 100644 index ecabec7..0000000 --- a/Chap4-DragDropClipboard/4-6PainterAppClipboard/paintcanvas.py +++ /dev/null @@ -1,305 +0,0 @@ -from PySide6.QtWidgets import QWidget, QApplication -from PySide6.QtGui import (QPainter, QMouseEvent, QPen, QColor, QBrush, - QImage, QPaintEvent, QResizeEvent, QKeyEvent, - QKeySequence, QPixmap) -from PySide6.QtCore import Qt, QPoint, QRect, QSize, QRectF - -import os - -class PaintCanvas(QWidget): - """Canvas widget for drawing with various tools""" - - # Tool type enum - Pen, Rect, Ellipse, Eraser = range(4) - - def __init__(self, parent=None): - super().__init__(parent) - - # Initialize properties - self.tool = self.Pen - self.fill = False - self.drawing = False - self.penWidth = 3 - self.fillColor = QColor(Qt.red) - self.penColor = QColor(Qt.green) - self.lastPoint = QPoint() - self.lastRect = QRectF(0, 0, 0, 0) - self.lastEraserRect = QRectF(0, 0, 0, 0) - - # Create a white image to paint on - self.image = QImage(self.size(), QImage.Format.Format_RGB32) - self.image.fill(Qt.white) - - # Set focus policy to enable key events - self.setFocusPolicy(Qt.StrongFocus) - - def getTool(self): - """Get the current tool""" - return self.tool - - def setTool(self, value): - """Set the current tool""" - self.tool = value - - def getFill(self): - """Get the fill property""" - return self.fill - - def setFill(self, value): - """Set the fill property""" - self.fill = value - - def getPenWidth(self): - """Get the pen width""" - return self.penWidth - - def setPenWidth(self, value): - """Set the pen width""" - self.penWidth = value - - def getFillColor(self): - """Get the fill color""" - return self.fillColor - - def setFillColor(self, value): - """Set the fill color""" - self.fillColor = value - - def getPenColor(self): - """Get the pen color""" - return self.penColor - - def setPenColor(self, value): - """Set the pen color""" - self.penColor = value - - def copy(self): - """Copy the canvas image to clipboard""" - clipboard = QApplication.clipboard() - mime_data = clipboard.mimeData() - mime_data.setImageData(self.image) - clipboard.setMimeData(mime_data) - print("Image copied to clipboard") - - def paste(self): - """Paste image from clipboard to canvas""" - # Get data from the clipboard - mime_data = QApplication.clipboard().mimeData() - - if mime_data.hasUrls(): - urls = mime_data.urls() - if len(urls) != 1: - return - - file_path = urls[0].toLocalFile() - - if self.isImage(file_path): - # Build the image object - pixmap = QPixmap(file_path) - - # Paint it on the canvas - painter = QPainter(self.image) - painter.setPen(QPen(self.penColor, self.penWidth, Qt.SolidLine, - Qt.RoundCap, Qt.RoundJoin)) - painter.setRenderHint(QPainter.Antialiasing, True) - - painter.drawPixmap( - QRect(10, 10, 300, 300), - pixmap.scaled(300, 300, Qt.KeepAspectRatio), - QRect(0, 0, 300, 300) - ) - - self.update() - elif mime_data.hasImage(): - # If clipboard contains an image, paste it directly - pixmap = QPixmap(mime_data.imageData()) - - painter = QPainter(self.image) - painter.setPen(QPen(self.penColor, self.penWidth, Qt.SolidLine, - Qt.RoundCap, Qt.RoundJoin)) - painter.setRenderHint(QPainter.Antialiasing, True) - - painter.drawPixmap( - QRect(10, 10, 300, 300), - pixmap.scaled(300, 300, Qt.KeepAspectRatio), - QRect(0, 0, 300, 300) - ) - - self.update() - - def isImage(self, file_path): - """Check if a file is a supported image format""" - _, ext = os.path.splitext(file_path) - ext = ext.lower() - return ext in [".png", ".jpg", ".jpeg"] - - def drawLineTo(self, endPoint): - """Draw a line from last point to current point""" - painter = QPainter(self.image) - painter.setPen(QPen(self.penColor, self.penWidth, Qt.SolidLine, - Qt.RoundCap, Qt.RoundJoin)) - painter.setRenderHint(QPainter.Antialiasing, True) - painter.drawLine(self.lastPoint, endPoint) - - # Update only the drawn part for efficiency - adjustment = self.penWidth + 2 - updateRect = QRect(self.lastPoint, endPoint).normalized().adjusted( - -adjustment, -adjustment, adjustment, adjustment) - self.update(updateRect) - - self.lastPoint = endPoint - - def drawRectTo(self, endPoint, ellipse=False): - """Draw a rectangle/ellipse from last point to current point""" - painter = QPainter(self.image) - painter.setPen(QPen(self.penColor, self.penWidth, Qt.SolidLine, - Qt.RoundCap, Qt.RoundJoin)) - - # Set brush based on fill property - if self.fill: - painter.setBrush(self.fillColor) - else: - painter.setBrush(Qt.NoBrush) - - # Draw rect or ellipse - if not ellipse: - painter.drawRect(QRect(self.lastPoint, endPoint)) - else: - painter.drawEllipse(QRect(self.lastPoint, endPoint)) - - # When still drawing, erase the last temporary shape - if self.drawing: - painter.setPen(QPen(Qt.white, self.penWidth+2, Qt.SolidLine, - Qt.RoundCap, Qt.RoundJoin)) - - if self.fill: - painter.setBrush(Qt.white) - else: - painter.setBrush(Qt.NoBrush) - - if not ellipse: - painter.drawRect(self.lastRect) - else: - painter.drawEllipse(self.lastRect) - - # Reset the pen and brush - painter.setPen(QPen(self.penColor, self.penWidth, Qt.SolidLine, - Qt.RoundCap, Qt.RoundJoin)) - if self.fill: - painter.setBrush(self.fillColor) - else: - painter.setBrush(Qt.NoBrush) - - self.lastRect = QRectF(self.lastPoint, endPoint) - self.update() - - def eraseUnder(self, topLeft): - """Erase content under a specific point""" - painter = QPainter(self.image) - - # Erase last eraser rect - painter.setBrush(Qt.white) - painter.setPen(Qt.white) - painter.drawRect(self.lastEraserRect) - - # Erase the content under current eraser rect - current_rect = QRect(topLeft, QSize(100, 100)) - painter.setBrush(Qt.white) - painter.setPen(Qt.white) - painter.drawRect(current_rect) - - # Draw current eraser rect - painter.setBrush(Qt.black) - painter.setPen(Qt.black) - painter.drawRect(current_rect) - - self.lastEraserRect = current_rect - - # If not drawing, erase the last eraser rect - if not self.drawing: - painter.setBrush(Qt.white) - painter.setPen(Qt.white) - painter.drawRect(self.lastEraserRect) - self.lastEraserRect = QRect(0, 0, 0, 0) - - self.update() - - def resizeImage(self, image, newSize): - """Resize the image to a new size""" - if image.size() == newSize: - return image - - newImage = QImage(newSize, QImage.Format.Format_RGB32) - newImage.fill(Qt.white) - painter = QPainter(newImage) - painter.drawImage(QPoint(0, 0), image) - - return newImage - - def mousePressEvent(self, event): - """Handle mouse press events""" - self.setFocus() # Ensure the widget has focus - if event.button() == Qt.LeftButton: - self.lastPoint = event.position().toPoint() - self.drawing = True - - def mouseMoveEvent(self, event): - """Handle mouse move events""" - if (event.buttons() & Qt.LeftButton) and self.drawing: - pos = event.position().toPoint() - - if self.tool == self.Pen: - self.drawLineTo(pos) - elif self.tool == self.Rect: - self.drawRectTo(pos) - elif self.tool == self.Ellipse: - self.drawRectTo(pos, True) - elif self.tool == self.Eraser: - self.eraseUnder(pos) - - def mouseReleaseEvent(self, event): - """Handle mouse release events""" - if event.button() == Qt.LeftButton and self.drawing: - pos = event.position().toPoint() - - self.drawing = False - if self.tool == self.Pen: - self.drawLineTo(pos) - elif self.tool == self.Rect: - self.drawRectTo(pos) - elif self.tool == self.Ellipse: - self.drawRectTo(pos, True) - elif self.tool == self.Eraser: - self.eraseUnder(pos) - - # Reset the last rect - self.lastRect = QRect(0, 0, 0, 0) - - def paintEvent(self, event): - """Paint event to display the image""" - painter = QPainter(self) - rectToDraw = event.rect() - painter.drawImage(rectToDraw, self.image, rectToDraw) - - def resizeEvent(self, event): - """Handle resize events""" - if self.width() > self.image.width() or self.height() > self.image.height(): - newWidth = max(self.width() + 128, self.image.width()) - newHeight = max(self.height() + 128, self.image.height()) - self.image = self.resizeImage(self.image, QSize(newWidth, newHeight)) - self.update() - - super().resizeEvent(event) - - def keyPressEvent(self, event): - """Handle key press events for clipboard operations""" - if event.matches(QKeySequence.Copy): - print("Copy sequence detected") - self.copy() - event.accept() - elif event.matches(QKeySequence.Paste): - print("Paste sequence detected") - self.paste() - event.accept() - else: - super().keyPressEvent(event) \ No newline at end of file diff --git a/Chap4-DragDropClipboard/4-6PainterAppClipboard/ui_mainwindow.py b/Chap4-DragDropClipboard/4-6PainterAppClipboard/ui_mainwindow.py deleted file mode 100644 index 619eae8..0000000 --- a/Chap4-DragDropClipboard/4-6PainterAppClipboard/ui_mainwindow.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'mainwindow.ui' -## -## Created by: Qt User Interface Compiler version 6.8.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QMainWindow, QMenuBar, QSizePolicy, - QStatusBar, QToolBar, QWidget) - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - if not MainWindow.objectName(): - MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(950, 684) - self.centralWidget = QWidget(MainWindow) - self.centralWidget.setObjectName(u"centralWidget") - MainWindow.setCentralWidget(self.centralWidget) - self.menuBar = QMenuBar(MainWindow) - self.menuBar.setObjectName(u"menuBar") - self.menuBar.setGeometry(QRect(0, 0, 950, 26)) - MainWindow.setMenuBar(self.menuBar) - self.mainToolBar = QToolBar(MainWindow) - self.mainToolBar.setObjectName(u"mainToolBar") - MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.mainToolBar) - self.statusBar = QStatusBar(MainWindow) - self.statusBar.setObjectName(u"statusBar") - MainWindow.setStatusBar(self.statusBar) - - self.retranslateUi(MainWindow) - - QMetaObject.connectSlotsByName(MainWindow) - # setupUi - - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) - # retranslateUi -