PyQt5 Place QImage in a widget - python

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

First of all, the following aspects must be considered:
paint events are received by any (visible) QWidget subclass
painting is always constricted to the geometry of the widget
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;

Related

How to prevent scrolling while 'ctrl' is pressed in PyQt5?

Using PyQt5 I am viewing an image in a QGraphicsView. I want to be able to zoom in/out while pressing ctrl and using the mouse wheel. I have this working, however if the image is too large, and there are scroll bars, it ignores the zoom functionality until you scroll to the top or bottom.
How can I fix this to where it does not scroll when ctrl is pressed, while allowing it to zoom in/out.
from PyQt5.QtWidgets import QFileDialog, QLineEdit, QWidget, QPushButton, QApplication, QVBoxLayout, QLabel, QGraphicsView, QGraphicsPixmapItem, QGraphicsScene
from PyQt5.QtCore import pyqtSignal, Qt
from pdf2image import convert_from_path
from PIL import ImageQt
import sys
class step1(QWidget):
changeViewSignal = pyqtSignal()
def __init__(self, parent=None):
super(step1, self).__init__(parent)
self.name = QLineEdit(self)
self.fileBtn = QPushButton("Select file", self)
self.nextBtn = QPushButton("Next", self)
self.graphicsView = QGraphicsView()
# self.graphicsView.setFrameShadow(QFrame.Raised)
# self.graphicsView.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.graphicsView.setHorizontalScrollBarPolicy()
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.name)
self.layout.addWidget(self.fileBtn)
self.layout.addWidget(self.nextBtn)
self.layout.addWidget(self.graphicsView)
self.fileBtn.clicked.connect(self.convert_file)
def wheelEvent(self, event):
modifiers = QApplication.keyboardModifiers()
if modifiers == Qt.ControlModifier:
self.graphicsView.scrollContentsBy(0,0)
x = event.angleDelta().y() / 120
if x > 0:
self.graphicsView.scale(1.05, 1.05)
elif x < 0:
self.graphicsView.scale(.95, .95)
def convert_file(self):
fname = QFileDialog.getOpenFileName(self, 'Open File', 'c:\\', "PDF Files (*.pdf)")
if len(fname[0]) > 0:
pages = convert_from_path(fname[0])
images = []
qimage = ImageQt.toqpixmap(pages[0])
item = QGraphicsPixmapItem(qimage)
scene = QGraphicsScene(self)
scene.addItem(item)
self.graphicsView.setScene(scene)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = step1()
ex.show()
sys.exit(app.exec_())
The scrolling is first handled by the QGraphicsView before it would be propagated up to the parent widget where you are reimplementing the wheelEvent. This is why the scrolling occurs according to the normal QGraphicsView behavior when it has space to scroll.
A solution is to subclass QGraphicsView and reimplement the wheelEvent there instead.
class GraphicsView(QGraphicsView):
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
x = event.angleDelta().y() / 120
if x > 0:
self.scale(1.05, 1.05)
elif x < 0:
self.scale(.95, .95)
else:
super().wheelEvent(event)
Then use the subclass name here:
self.graphicsView = GraphicsView()
Besides the proper solution proposed by alec, there's also the option of using an event filter, which can be useful for UIs created in Designer without the need of using promoted widgets.
The important aspect to keep in mind is that the event filter must be installed on the view's viewport() (the widget in which the contents of the scene are actually rendered and, possibly, scrolled), because that is the widget that will receive the wheel event: input events are always sent to the widget under the mouse (or has keyboard focus)[1], and possibly propagated to their parents if the event is not handled[2].
class step1(QWidget):
def __init__(self, parent=None):
# ...
self.graphicsView.viewport().installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == event.Wheel and event.modifiers() & Qt.ControlModifier:
x = event.angleDelta().y() / 120
if x > 0:
self.graphicsView.scale(1.05, 1.05)
elif x < 0:
self.graphicsView.scale(.95, .95)
return True
return super().eventFilter(source, event)
Returning True means that the viewport has handled the event, and it should not be propagated to the parent; QGraphicsView is based on QAbstractScrollArea, and if a wheel event is not handled by the viewport it will call the base wheelEvent implementation of the viewport's parent (the graphics view), which by default will post the event to the scroll bars. If the filter returns True, it will avoid that propagation, thus preventing scrolling.
Note that you should not use scrollContentsBy unless you really know what you're doing; as the documentation explains: «Calling this function in order to scroll programmatically is an error, use the scroll bars instead».
[1] Mouse events are always sent to the topmost widget under the mouse, unless a modal child window is active, or there is a mouse grabber, which is a widget that has received a mouse button press event but didn't receive a mouse button release event yet, or a widget on which grabMouse() was explicitly called. Keyboard events are always sent to the widget of the active window that has current focus, or the widget on which grabKeyboard() has been explicitly called.
[2] "handled event" can be a confusing concept: it doesn't mean that the widget actually "does" something with the event, nor that it doesn't, no matter if the event then becomes accepted or ignored. A widget could "ignore" an event and still react to it in some way: for instance, you might need to notify the user that the event has been received, but let the parent manage it anyway.

Python PyQt5 how to show the full QMenuBar with a QWidget

I'm getting this weird result when using QMenuBar I've used this exact code before for the QMenuBar and it worked perfectly. But it doesn't show more than 1 QMenu
This is my code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
from functools import partial
class MainMenu(QWidget):
def __init__(self, parent = None):
super(MainMenu, self).__init__(parent)
# background = QWidget(self)
lay = QVBoxLayout(self)
lay.setContentsMargins(5, 35, 5, 5)
self.menu()
self.setWindowTitle('Control Panel')
self.setWindowIcon(self.style().standardIcon(getattr(QStyle, 'SP_DialogNoButton')))
self.grid = QGridLayout()
lay.addLayout(self.grid)
self.setLayout(lay)
self.setMinimumSize(400, 320)
def menu(self):
menubar = QMenuBar(self)
viewMenu = menubar.addMenu('View')
viewStatAct = QAction('Dark mode', self, checkable=True)
viewStatAct.setStatusTip('enable/disable Dark mode')
viewMenu.addAction(viewStatAct)
settingsMenu = menubar.addMenu('Configuration')
email = QAction('Set Email', self)
settingsMenu.addAction(email)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainMenu()
main.show()
sys.exit(app.exec_())
Result:
I am aware that I am using QWidget when I should be using QMainWindow But is there a workaround???
(I apologize in advance for the terrible quality of the image, there is no good way to take a picture of a QMenuBar)
The problem is that with a QWidget you are not using the "private" layout that a QMainWindow has, which automatically resizes specific children widgets (including the menubar, the statusbar, the dock widgets, the toolbars and, obviously, the "centralWidget").
Remember that a QMainWindow has its own layout (which can't and shouldn't be changed), because it needs that specific custom layout to lay out the aforementioned widgets. If you want to set a layout for the main window, you'll need to apply it to its centralWidget.
Read carefully how the Main Window Framework behaves; as the documentation reports:
Note: Creating a main window without a central widget is not supported. You must have a central widget even if it is just a placeholder.
In order to work around that when using a basic QWidget, you'll have to manually resize the children widgets accordingly. In your case, you only need to resize the menubar, as long as you have a reference to it:
def menu(self):
self.menubar = QMenuBar(self)
# any other function has to be run against the *self.menubar* object
viewMenu = self.menubar.addMenu('View')
# etcetera...
def resizeEvent(self, event):
# calling the base class resizeEvent function is not usually
# required, but it is for certain widgets (especially item views
# or scroll areas), so just call it anyway, just to be sure, as
# it's a good habit to do that for most widget classes
super(MainMenu, self).resizeEvent(event)
# now that we have a direct reference to the menubar widget, we are
# also able to resize it, allowing all actions to be shown (as long
# as they are within the provided size
self.menubar.resize(self.width(), self.menubar.height())
Note: you can also "find" the menubar by means of self.findChild(QtWidgets.QMenuBar) or using the objectName, but using an instance attribute is usually an easier and better solution.
Set minimum width
self.setMinimumSize(320,240)

treatment of mouse events opencv gui vs pyqt

I was working with OpenCV gui functions for a while, and the possibilities are a little restricting for python users. Today I started with Pyqt and come across the following conclusion: qt is really confusing.
Now the question concerning mouse events:
In OpenCV I just do the following:
import cv2
cv2.namedWindow('Window',1)
def CallBackFunc(event,x,y,flags,param):
global xc,yc,evt,flg
xc,yc,evt,flg=x,y,event,flags
cv2.setMouseCallback('Window', CallBackFunc)
This opens a seperate thread, which constantly refreshes the global variables xc,yc,evt,flg, and I can access them anywhere, at anytime I want. If I want to stop the refreshing, I just do a cv2.setMouseCallback('Window',nothing), whereby nothing is
def nothing():
pass
It may not be the most beautiful way of dealing with mouse events, but I am fine with it. How can I achieve such freedom with PyQt?
EDIT:
For example, the following script is displaying a white circle, and constantly drawing a text into it.
import sys
from PySide import QtGui
import numpy as np
import cv2
class QCustomLabel (QtGui.QLabel):
def __init__ (self, parent = None):
super(QCustomLabel, self).__init__(parent)
self.setMouseTracking(True)
def mouseMoveEvent (self, eventQMouseEvent):
self.x,self.y=eventQMouseEvent.x(),eventQMouseEvent.y()
cvImg=np.zeros((900,900),dtype=np.uint8)
cv2.circle(cvImg,(449,449),100,255,-1)
cv2.putText(cvImg,"x at {}, y at {}".format(self.x,self.y),(375,455), cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,0),1,cv2.LINE_AA)
height, width= cvImg.shape
bytearr=cvImg.data
qImg = QtGui.QImage(bytearr, width, height, QtGui.QImage.Format_Indexed8)
self.setPixmap(QtGui.QPixmap.fromImage(qImg))
def mousePressEvent (self, eventQMouseEvent):
self.evt=eventQMouseEvent.button()
class QCustomWidget (QtGui.QWidget):
def __init__ (self, parent = None):
super(QCustomWidget, self).__init__(parent)
self.setWindowOpacity(1)
# Init QLabel
self.positionQLabel = QCustomLabel(self)
# Init QLayout
layoutQHBoxLayout = QtGui.QHBoxLayout()
layoutQHBoxLayout.addWidget(self.positionQLabel)
self.setLayout(layoutQHBoxLayout)
self.show()
if QtGui.QApplication.instance() is not None:
myQApplication=QtGui.QApplication.instance()
else:
myQApplication = QtGui.QApplication(sys.argv)
myQTestWidget = QCustomWidget()
myQTestWidget.show()
myQApplication.exec_()
The problem here is, that this is all executed inside the QCustomLabel Class, and inside the MouseMoveEvent function. But I want a seperate function, lets call it drawCircle, outside of that class, which has access to the mouse position and events. With opencv this would be no problem at all. And it would take only a fraction of the writing effort, which is needed for a pyqt implementation.
I think the right question is: Why dont I like pyqt yet?
You can use an event-filter to avoid having to subclass the QLabel:
class QCustomWidget (QtGui.QWidget):
def __init__ (self, parent = None):
super(QCustomWidget, self).__init__(parent)
self.setWindowOpacity(1)
# Init QLabel
self.positionQLabel = QtGui.QLabel(self)
self.positionQLabel.setMouseTracking(True)
self.positionQLabel.installEventFilter(self)
# Init QLayout
layoutQHBoxLayout = QtGui.QHBoxLayout()
layoutQHBoxLayout.addWidget(self.positionQLabel)
self.setLayout(layoutQHBoxLayout)
self.show()
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.MouseMove:
self.drawCircle(event.x(), event.y())
return super(QCustomWidget, self).eventFilter(source, event)
def drawCircle(self, x, y):
# whatever

Properly Positioning Popup Widgets in PyQt

his has plagued me for eons, mostly due to how many combinations of methodologies there are for moving widgets and whatnot. Essentially I have a simple widget that I'd like to be able to pop up in specific areas of my app. Problem is I can never seem to get it to pop up where I want it. Additionally, I'd like to set it up in a way where I can adjust the "pointer" side of it based on whether it's popping up to point at a widget in the top-left of the app versus, say, the bottom-right.
Ideally, I'd be able to place the popup nearly adjacent to the edges of the parent widget, and anchor it based on where it is. Here's what I've been trying.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class popup(QWidget):
def __init__(self, parent = None, widget=None):
QWidget.__init__(self, parent)
layout = QGridLayout(self)
button = QPushButton("Very Interesting Text Popup. Here's an arrow ^")
layout.addWidget(button)
self.move(widget.rect().bottomLeft())
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
self.button = QPushButton('Hit this button to show a popup', self)
self.button.clicked.connect(self.handleOpenDialog)
self.button.move(250, 50)
self.resize(600, 200)
def handleOpenDialog(self):
self.popup = popup(self, self.button)
self.popup.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
This code generates a button that's randomly in the middle of the widget. What I'm trying to get is, in this example, the popup to show under the button with its "pivot" in the top right such that the arrow in the popup button would be pointing to the bottom right corner of the widget. However it's popping up in the top left of the Window instead. In all of my messing around with .move, .setGeometry, and playing with QRect, I can't for the life of me figure this out. Huge kudos to whoever can lend a hand. Thanks!
I know this is old, but I was searching for this recently and this is the best answer; I have a useful addition (for anyone else searching for this recipe!)
I implemented it as a mixin, which I think gives more flexibility to your dialogs:
class PopupDialogMixin(object): # will not work (with PySide at least) unless implemented as 'new style' class. I.e inherit from object
def makePopup(callWidget):
"""
Turns the dialog into a popup dialog.
callWidget is the widget responsible for calling the dialog (e.g. a toolbar button)
"""
self.setContentsMargins(0,0,0,0)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup)
self.setObjectName('ImportDialog')
# Move the dialog to the widget that called it
point = callWidget.rect().bottomRight()
global_point = callWidget.mapToGlobal(point)
self.move(global_point - QtCore.QPoint(self.width(), 0))
Your custom dialog would then inherit from both QtCore.QDialog and PopupDialogMixin. This gives you the option to use your dialog in the 'normal' way or make it a popup dialog. e.g:
dlg = MyDialog(self)
dlg.makePopup(self.myButton)
I think implementing it as a mixin gives a number of benefits:
No need to write the 'popup' code for each custom dialog you want as a popup
'Default' behaviour of the dialog is preserved - e.g. if you want to reuse it somewhere else as a 'regular' dialog, you just use it like normal
No need to pass anything extra to __init__ other than parent.
Here you go - the comments kind of explain the logic behind it - since the question is an example and about the positioning, I kept the rest of the code the same except the popup class, but just to mention cause its a pet peeve - you shouldn't import * (ever) but especially with something as big as PyQt4.QtCore/QtGui...
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class popup(QWidget):
def __init__(self, parent = None, widget=None):
QWidget.__init__(self, parent)
layout = QGridLayout(self)
button = QPushButton("Very Interesting Text Popup. Here's an arrow ^")
layout.addWidget(button)
# adjust the margins or you will get an invisible, unintended border
layout.setContentsMargins(0, 0, 0, 0)
# need to set the layout
self.setLayout(layout)
self.adjustSize()
# tag this widget as a popup
self.setWindowFlags(Qt.Popup)
# calculate the botoom right point from the parents rectangle
point = widget.rect().bottomRight()
# map that point as a global position
global_point = widget.mapToGlobal(point)
# by default, a widget will be placed from its top-left corner, so
# we need to move it to the left based on the widgets width
self.move(global_point - QPoint(self.width(), 0))
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
self.button = QPushButton('Hit this button to show a popup', self)
self.button.clicked.connect(self.handleOpenDialog)
self.button.move(250, 50)
self.resize(600, 200)
def handleOpenDialog(self):
self.popup = popup(self, self.button)
self.popup.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())

PyQT4 WheelEvent? how to detect if the wheel have been use?

im trying to find out in PyQT how can i set the Mousewheel event?
i need it so i can attach it to the Qscroll area
the code im using is working fine. but the size is hardcoded. i need it to somehow dynamically adjust depending on how the wheel(on the mouse) is used.. like when i slide the mouse wheel up. the height of my frame extends(like 50pixels per tick) and vise versa.
self.scrollArea = QtGui.QScrollArea()
#set the parent of scrollArea on the frame object of the computers
self.scrollArea.setWidget(self.ui.Main_Body)
self.scrollArea.setWidgetResizable(True)
#add the verticalLayout a object on PYQT Designer (vlayout is the name)
#drag the frame object of the computers inside the verticalLayout
#adjust the size of the verticalLayout inside the size of the frame
#add the scrollArea sa verticalLayout
self.ui.verticalLayout.addWidget(self.scrollArea)
self.ui.Main_Body.setMinimumSize(400, 14000)
the last part is what i want to enhance. i dont want it to be hardcoded into a 14000 value.
thanks to anyone who will help. and i hope that the given sample code can also help others in need.
)
I might be a little confused on your question, but here's an example on how to get access to wheel events that resize your window. If you're using a QScrollArea I don't know why you would want to do this though.
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys
class Main(QWidget):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
layout = QHBoxLayout(self)
layout.addWidget(Scroll(self))
class Scroll(QScrollArea):
def __init__(self, parent=None):
super(Scroll, self).__init__(parent)
self.parent = parent
def wheelEvent(self, event):
super(Scroll, self).wheelEvent(event)
print "wheelEvent", event.delta()
newHeight = self.parent.geometry().height() - event.delta()
width = self.parent.geometry().width()
self.parent.resize(width, newHeight)
app = QApplication(sys.argv)
main = Main()
main.show()
sys.exit(app.exec_())
If you look at the documentation for QScrollArea you'll see the line of inherited from the QWidget class which has a function called wheelEvent. You can put that in and overwrite the inherited function.

Categories

Resources