-1

I created a small GUI that allows me to draw a number. That number is supposed to be classified with a CNN. The CNN is not connected to this GUI yet. Will do that later on. I am still very new to PyQt5 and used some code that i found online for the drawing with QImage. It works, but at the moment i can draw all over the GUI. Is it possible to place that in a widget? So that I can only draw inside a specific frame and not all over the GUI?

So basically how can i get the self.image = QImage(...) iside a widget or something that i created earlier on my GUI? Is that possible somehow or would you even suggest to solve it in totaly different way?

import sys
from PyQt5 import QtWidgets
from PyQt5.QtGui import QIcon, QImage, QPainter, QPen, QBrush
from PyQt5.QtCore import Qt, QPoint
from UI.mainwindow_2 import Ui_MainWindow
import matplotlib.pyplot as plt
import numpy as np


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
     
        self.image = QImage(self.size(), QImage.Format_RGB32)
        self.image.fill(Qt.white)

        self.drawing = False
        self.brushSize = 28
        self.brushColor = Qt.black
        self.lastPoint = QPoint()
        self.ui.Clear.clicked.connect(self.clear)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.drawing = True
            self.lastPoint = event.pos()

    def mouseMoveEvent(self, event):
        if(event.buttons() & Qt.LeftButton) & self.drawing:
            painter = QPainter(self.image)
            painter.setPen(QPen(self.brushColor, self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
            painter.drawLine(self.lastPoint, event.pos())
            self.lastPoint = event.pos()
            self.update()

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.drawing = False

    def paintEvent(self, event):
        canvasPainter  = QPainter(self)
        canvasPainter.drawImage(self.rect(),self.image, self.image.rect())

    def clear(self):
        self.image.fill(Qt.white)
        self.update()


def main():
    app = QtWidgets.QApplication(sys.argv)

    window = MainWindow()
    window.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()
Marvin
  • 17
  • 1
  • 6
  • 1
    You say: *Is it possible to place that in a widget? So that I can only draw inside a specific frame and not all over the GUI?* and I ask what widget do you mean? You could provide a [mre] and maybe put an image of what you want to obtain since currently your question is unclear – eyllanesc Jan 11 '21 at 18:09
  • I just mean that currently i can draw all over the GUI, but that's not what i want. I want to have a specific Frame on my GUI where i can draw in. Like I built the GUI with QTDesigner and created a Container Widget on it. And I only want to be able to draw in that specific Widget and not all over the GUI. Right now I can even draw behind the Buttons and everywhere. I don't care if it is a Container Widget or any other Widget. Don't know what is the best for this purpose. – Marvin Jan 11 '21 at 18:39
  • Your question is unclear also your code is incomplete, so it is a candidate to be closed because it does not meet the SO rules – eyllanesc Jan 11 '21 at 18:41
  • @eyllanesc I believe that the OP is trying to understand how to allow mouse painting only on a specific child widget created in Designer, and is probably a bit confused about the parenthood of QWidgets and the implementation of paint events. While the question is a bit vague and the provided code not reproducible, I think it's still a valid question. – musicamante Jan 11 '21 at 18:53
  • 1
    @musicamante I also have that suspicion but a quality question requires that it not be vague, for that reason I have asked you to clarify the post and provide an MRE, if you do not provide it then IMO does not meet the minimum quality – eyllanesc Jan 11 '21 at 19:03

1 Answers1

0

First of all, the following aspects must be considered:

  1. paint events are received by any (visible) QWidget subclass
  2. painting is always constricted to the geometry of the widget
  3. painting always happens from the bottom to the top, so whenever a widget has some children, whatever has been painted on that parent widget will be potentially covered by those children

So, the first thing to do is to implement the paintEvent only on the actual widget for which you want to paint. Since you're using Designer, this makes things a bit more complex, as there is no way to subclass a widget that already exists in an ui.

Luckily, Qt has a concept called "promoted widgets": it allows to "expand" a certain widget by specifying the custom subclass that will be actually used when the ui will be finally generated in the program.

  • choose which widget in your ui will be used for painting; I suggest you to start from a basic QWidget, I'll explain more about this later;
  • right click on it, and select "Promote to..." from the context menu;
  • in the "Promoted class name" type the exact class name you are going to use (let's say "Canvas");
  • in the "Header file" field, type the file name that will contain that class, as it would appear in an import statement (meaning that it should not have the py extension!); assuming you want to do everything in a single script and your script is named "mycanvas.py", type "mycanvas" in that field;
  • ensure that the "Base class name" combo is set to the exact class type of the widget you've chosen (QWidget, in this case, which is usually automatically selected)
  • click "Add" and then "Promote";
  • save the ui and generate the file with pyuic;

Now, the implementation of your mycanvas.py file is simple:

class Canvas(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.image = QImage(self.size(), QImage.Format_RGB32)
        self.image.fill(Qt.white)

        self.drawing = False
        self.brushSize = 28
        self.brushColor = Qt.black
        self.lastPoint = QPoint()

    # ... all the painting related methods, as you did in your code


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui.Clear.clicked.connect(self.ui.canvas.clear)

def main():
    # ...

Two considerations:

  • a QFrame usually has some borders, and since painting can always happen within the whole widget size, you might end up painting on the borders too. If you want a QFrame and paint inside that widget, then add a QWidget as a child to that frame and ensure that a layout is set for the frame;
  • setting the QImage size within the init is not a good approach, as the widget might change its size or it could be initialized with a size smaller than it will eventually have; you either set a fixed size in the __init__ before creating the QImage, or you keep track of the points/lines in an internal container and continuously draw the contents in the paintEvent;
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thank you. Yes, i have the problem with the size. I set a fixed size for the widget in the __init__ of the UI_Mainwindow(object) class after i created an instance of the Canvas class. I could solve the problem by just using self.image = QImage(560, 560, QImage.Format_RGB32) instead of self.image = QImage(self.size(), QImage.Format_RGB32). But it doesn't seems to be the best solution. Like i case i change something about the GUI i always have to change the Canvas class aswell. Isn't there something else i can do? – Marvin Jan 11 '21 at 23:59
  • As said, with your implementation you can only set the fixed size of the QImage, as setting a fixed size in Designer won't work: when the ui file tries to set the fixed size, the custom widget instance has already been created (and so the image will already have a size set). If, and only **if** you're always going to have a fixed size for the image in the lifespan of the application (no matter if you want to change that size in designer in the future), then create an empty attribute for self.image (`self.image = None`), reimplement the `resizeEvent()`, and then check the `self.image` attribute – musicamante Jan 12 '21 at 00:23
  • `if self.image is None`, then you can create the new image by overwriting the `self.image` attribute, and it will have the new fixed size set. – musicamante Jan 12 '21 at 00:24
  • Okay, i see what you mean. Thank you very much. Just one more question. Nothing specific, everything works now, but i am just curious and want to learn. If you say "with your implementation you can only..." is there some smarter way to build this program or is the code more or less fine? This is for learning purpose obviously, but i want to try to code as clean as possible and don't write to much of spagetthi code. I feel like with the QtDesigner i am pretty limited here, so i am wondering if it would be smarter to code the whole GUI by hand? – Marvin Jan 12 '21 at 12:27