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
Related
I'm working on creating python plugin that implements image viewer and use layout to plased elements in correct plases. I want to resize all elements in dialog window of this plugin using resizeEvent of QDialog, but I have one strange problem: when I want to make dialog window smaller - all works good like on image1, but, if I want to make window bigger I have seen a problem like on image2:
Here is my resizeEvent method for my dialog:
def resizeEvent(self, a0: QResizeEvent):
geom = list(self.geometry().getCoords())
x_shift = geom[2] - geom[0] #transform x coord from monitor coords to dialog window coords
y_shift = geom[3] - geom[1] #transform y coord from monitor coords to dialog window coords
self.verticalLayout.setGeometry(QRect(0, 0, x_shift, y_shift))
And here is my code how I connect my ui with python code using uic:
import os
from qgis.PyQt import uic
from qgis.PyQt import QtWidgets
FORM_CLASS, _ = uic.loadUiType(os.path.join(
os.path.dirname(__file__), 'name_of_ui'))
class nameDialog(QtWidgets.QDialog, FORM_CLASS):
def __init__(self, parent=None):
super(status_checkerDialog, self).__init__(parent)
self.setupUi(self)
In this script, I create the resize function. Also, I would like to add that I use to build the skeleton of the module named Plugin Builder. Maybe this information will help you point out exactly where I am making a mistake. I also add the code skeleton that implements all functions of the module:
class status_checker:
def __init__(self, iface):
"Constructor"
def add_action(
self,
icon_path,
text,
callback,
enabled_flag=True,
add_to_menu=True,
add_to_toolbar=True,
status_tip=None,
whats_this=None,
parent=None):
"""Add a toolbar icon to the toolbar"""
def initGui(self):
"""Create the menu entries and toolbar icons inside the QGIS GUI"""
def unload(self):
"""Removes the plugin menu item and icon from QGIS GUI."""
def run(self):
"""Run method that performs all the real work"""
# Create the dialog with elements (after translation) and keep reference
# Only create GUI ONCE in callback, so that it will only load when the plugin is started
if self.first_start == True:
self.first_start = False #self.first_start is a variable that is created in the initialization function and initially set to true
self.dlg = status_checkerDialog()
I am building a program to view a video and do image processing on it. I want to know the proper way to give my individual classes access to the ui elements.
The way I implemented it now is the following:
I designed a GUI in QT Creator and save the video.ui file, then I generate the Ui_video.py file from it with pyside6-uic.
My main.py then looks like this:
import sys
from PySide6.QtWidgets import QMainWindow, QApplication
from PySide6.QtCore import QCoreApplication
from ui_video import Ui_Video
class VideoViewerMain(QMainWindow):
def __init__(self):
super().__init__()
self.ui : Ui_Video = None
def closeEvent(self, event):
self.ui.video_view.closeEvent(event)
return super().closeEvent(event)
class App(QApplication):
def __init__(self, *args, **kwargs):
super(App,self).__init__(*args, **kwargs)
self.window = VideoViewerMain()
self.ui = Ui_Video() #
self.window.ui = self.ui
self.ui.setupUi(self.window)
self.window.show()
if __name__ == "__main__":
# init application
app = App(sys.argv)
app.processEvents()
# execute qt main loop
sys.exit(app.exec())
Then I implemented my video previewer, which inherits from QOpenGLWidget as a way to display the video. (I guess this could be any widget type that supports painting)
The Qt hierarchy looks like this:
In the QT Creator I set the corresponding widget as a custom class. This means my class is instantiated when the main class calls setupUI.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ui_video import Ui_Video
class VideoPreview(QOpenGLWidget):
"""
widget to display camera feed and overlay droplet approximations from opencv
"""
def __init__(self, parent=None):
super().__init__(parent)
self.ui: Ui_Video = self.window().ui
self.reader = VideoReader(r"some/video/path")
# adjust the seekBar range with video length, this throws error
self.ui.seekBar.setMaximum(self.reader.number_of_frames - 1)
self.ui.seekBar.setMinimum(0)
self._first_show = True
def showEvent(self, event: QtGui.QShowEvent) -> None:
if self._first_show:
self._first_show = False
self.ui.seekBar.setMaximum(self.reader.number_of_frames - 1)
self.ui.seekBar.setMinimum(0)
return super().showEvent(event)
When I try to initialize the ui seekBar in the init function, it throws an error
File "src/VideoProcessor\videopreview.py", line 56, in __init__
self.ui.seekBar.setMaximum(self.reader.number_of_frames - 1)
AttributeError: 'Ui_Video' object has no attribute 'seekBar'
This is because my custom class is instantiated before the seekBar in the setupUI function which I cannot change. Currently I use the workaround with the showEvent.
So my question is: How would a proper implementation of a custom widget look like in this context?
Should I divide the functionality of the video controls from the widget entirely?
How can I ensure that the classes have access to the ui elements?
The main thing I see is your use of colons in assignment statements.
In python colons are mainly used to for setting dictionary literal values...
my_dict = {"my_key1": 1, "my_key2": 2}
ending loops/condition statements...
for index in range(12):
if index == 5:
print("FIVE")
and the occasional lambda when necessary...
sorted_my_dict = sorted(my_dict.items(), key=lambda item: item[1])
In your case I see at least 2 places where you are using colons in ways I've never personally seen in python.
self.ui : Ui_Video = None
# and
self.ui: Ui_Video = self.window().ui
If you need to set a value to None just use
self.ui = None
when setting it to an existing object from another widget either utilize the parent argument or just pass the object in as a different argument...
self.ui = parent.ui
# or
class VideoPreview(QOpenGLWidget):
"""
widget to display camera feed and overlay droplet approximations from opencv
"""
def __init__(self, ui_object, parent=None):
super().__init__(parent)
self.ui = ui_object.ui
[...]
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;
I'm making a large program in Python and using PyQt for the GUI. The whole program is divided into different modules so that different people can work on it simultaneously without interfering with the other people's work.
I am working on 3 different modules right now. 1 is the main program window that handles the basic UI and assigns widgets so the main window (this is not important, just so you know why the code doesn't look like a full program.)
First is the widget:
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
from CustomButton import HoverButton #just a custom button class
from CustomGif import LblLoadingGif #where things go wrong
class Page1(QtGui.QWidget):
def __init__(self, parent=None):
super(Page1, self).__init__(parent)
self.lbl1GIF = LblLoadingGif(self)
self.lbl1GIF.move(400, 45)
self.btnStart = HoverButton(self)
self.btnStart.setText('Start')
self.btnStart.move(35, 400)
self.btnStart.clicked.connect(self.actStartGif)
#the code below works, but then I can only perform 1 action with each button
#self.btnStart.clicked.connect(self.lbl1GIF.actStart)
def actStartGif(self):
self.lbl1GIF.actStart
The code for the custom GIF looks as follows:
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
class LblLoadingGif(QtGui.QLabel):
def __init__(self, parent=None):
QtGui.QLabel.__init__(self, parent)
self.setStyleSheet('background: url();')
self.setScaledContents(True)
self.resize(100, 100)
self.movLoadGif = QtGui.QMovie('Resources_Images/Loading6.gif', QtCore.QByteArray())
self.movLoadGif.setCacheMode(QtGui.QMovie.CacheAll)
self.movLoadGif.setSpeed(100)
self.setMovie(self.movLoadGif)
self.hide()
def actStart(self, event):
#print('test1')
self.show()
self.movLoadGif.start()
def actStop(self, event):
#print('test1')
self.hide()
self.movLoadGif.stop()
So the problem is that I can use the actStart function just fine when I call it from the button click directly, but not when I call it through another function. I have used a lot of different variations of brackets, self, Page1 when calling the actStart of the custom gif from withing the actStartGif function.
Any help will be appreciated.
When you use connect it is necessary to pass the name of the function since internally it is in charge of calling it, in your case you have to call it directly so you will have to pass its parameters, in this case event:
self.lbl1GIF.actStart({your value for event})
I do not understand why you use event for what happens to you None:
def actStartGif(self):
self.lbl1GIF.actStart(None)
I created a tool that is able to dock in Maya's main ui, but I can't figure out a way to clean it up once it closes. The problem is if I create multiple instances of the tool then drag it in place to dock it, they will ALL show up when I right-click on Maya's window. How do I properly clean these up when the tool closes?
I already tried cmds.deleteUI, QObject.deleteLater() and at best I can only clear the tool's contents, but it will still exist in Maya. Here's an example of what I have so far:
from shiboken import wrapInstance
from PySide import QtGui, QtCore
from maya import OpenMayaUI as OpenMayaUI
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
class Window(MayaQWidgetDockableMixin, QtGui.QWidget):
def __init__(self, parent = None):
super(self.__class__, self).__init__(parent = parent)
mayaMainWindowPtr = OpenMayaUI.MQtUtil.mainWindow()
self.mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QtGui.QWidget)
self.setWindowFlags(QtCore.Qt.Window)
if cmds.window('myTool', q = True, ex = True):
cmds.deleteUI('myTool')
self.setObjectName('myTool')
self.setWindowTitle('My tool')
self.resize(200, 200)
self.myButton = QtGui.QPushButton('TEMP')
self.mainLayout = QtGui.QVBoxLayout()
self.mainLayout.addWidget(self.myButton)
self.setLayout(self.mainLayout)
def dockCloseEventTriggered(self):
self.deleteLater()
def run(self):
self.show(dockable = True)
myWin = Window()
myWin.run()
After digging around mayaMixin.py I managed to get a working solution with the behavior I'm after! The idea is that you need to dig through Maya's main window and delete any instances of it there.
The example below will cleanly delete any instances once the window is closed or a new instance is created. It doesn't matter if it's docked or floating, it seems to work ok. You could always tweak the behavior too if you don't want it to delete if it's closed while being docked. I left many comments in the code as there was a lot of "gotchas".
from shiboken import wrapInstance
from PySide import QtGui, QtCore
from maya import OpenMayaUI as OpenMayaUI
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
from maya.OpenMayaUI import MQtUtil
class MyWindow(MayaQWidgetDockableMixin, QtGui.QDialog):
toolName = 'myToolWidget'
def __init__(self, parent = None):
# Delete any previous instances that is detected. Do this before parenting self to main window!
self.deleteInstances()
super(self.__class__, self).__init__(parent = parent)
mayaMainWindowPtr = OpenMayaUI.MQtUtil.mainWindow()
self.mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QtGui.QMainWindow)
self.setObjectName(self.__class__.toolName) # Make this unique enough if using it to clear previous instance!
# Setup window's properties
self.setWindowFlags(QtCore.Qt.Window)
self.setWindowTitle('My tool')
self.resize(200, 200)
# Create a button and stuff it in a layout
self.myButton = QtGui.QPushButton('My awesome button!!')
self.mainLayout = QtGui.QVBoxLayout()
self.mainLayout.addWidget(self.myButton)
self.setLayout(self.mainLayout)
# If it's floating or docked, this will run and delete it self when it closes.
# You can choose not to delete it here so that you can still re-open it through the right-click menu, but do disable any callbacks/timers that will eat memory
def dockCloseEventTriggered(self):
self.deleteInstances()
# Delete any instances of this class
def deleteInstances(self):
mayaMainWindowPtr = OpenMayaUI.MQtUtil.mainWindow()
mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QtGui.QMainWindow) # Important that it's QMainWindow, and not QWidget/QDialog
# Go through main window's children to find any previous instances
for obj in mayaMainWindow.children():
if type( obj ) == maya.app.general.mayaMixin.MayaQDockWidget:
#if obj.widget().__class__ == self.__class__: # Alternatively we can check with this, but it will fail if we re-evaluate the class
if obj.widget().objectName() == self.__class__.toolName: # Compare object names
# If they share the same name then remove it
print 'Deleting instance {0}'.format(obj)
mayaMainWindow.removeDockWidget(obj) # This will remove from right-click menu, but won't actually delete it! ( still under mainWindow.children() )
# Delete it for good
obj.setParent(None)
obj.deleteLater()
# Show window with docking ability
def run(self):
self.show(dockable = True)
myWin = MyWindow()
myWin.run()
With this it's no longer polluting Maya's environment anymore. Hope it helps!