I would like to write a simple program in Python with PyQt.
I have a QGraphicsScene and I would like to do the following:
There are 2 options using two RadioButtons:
For generating points. This way if someone clicks on the scene an ellipse will appear.
For selecting points. This way if someone clicks on a point the selected point will be returned.
I'm kinda new at PyQt and also at GUI programming. My main problem is that I don't really understand how mouse events work in Qt.
If someone was so kind and patient to explain me the basics of mouse events and gave me some tipps for the problem explained above, I would be very grateful.
I attach a picture too, to visualize the problem.
I was trying to do the problem. For placing the widgets I used the Qt Designer, and then created a subclass called SimpleWindow.
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtGui import QPen, QBrush
from PyQt5.QtWidgets import QGraphicsScene
import points
class SimpleWindow(QtWidgets.QMainWindow, points.Ui_Dialog):
def __init__(self, parent=None):
super(SimpleWindow, self).__init__(parent)
self.setupUi(self)
self.graphicsView.scene = QGraphicsScene()
self.graphicsView.setScene(self.graphicsView.scene)
self.graphicsView.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
self.graphicsView.mousePressEvent = self.pixelSelect
def pixelSelect(self, event):
pen = QPen(QtCore.Qt.black)
brush = QBrush(QtCore.Qt.black)
x = event.x()
y = event.y()
if self.radioButton.isChecked():
print(x, y)
self.graphicsView.scene.addEllipse(x, y, 4, 4, pen, brush)
if self.radioButton_2.isChecked():
print(x, y)
app = QtWidgets.QApplication(sys.argv)
form = SimpleWindow()
form.show()
app.exec_()
This is the class generated by the Designer:
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(538, 269)
self.graphicsView = QtWidgets.QGraphicsView(Dialog)
self.graphicsView.setGeometry(QtCore.QRect(130, 10, 371, 221))
self.graphicsView.setObjectName("graphicsView")
self.radioButton = QtWidgets.QRadioButton(Dialog)
self.radioButton.setGeometry(QtCore.QRect(20, 30, 82, 31))
self.radioButton.setObjectName("radioButton")
self.radioButton_2 = QtWidgets.QRadioButton(Dialog)
self.radioButton_2.setGeometry(QtCore.QRect(20, 80, 82, 17))
self.radioButton_2.setObjectName("radioButton_2")
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.radioButton.setText(_translate("Dialog", "Generate"))
self.radioButton_2.setText(_translate("Dialog", "Select"))
Thank you.
In a QGraphicsView a QGraphicsScene is added, each one manages a system of different coordinates. the QGraphicsView is similar to a camera and a QGraphicsScene is similar to the world, when one adds an item to the scene it must be in its coordinate system.
As you want to add items when you click, it is better to overwrite the mousePressEvent method of QGraphicsScene, and get the position in the coordinates of the scene for which the scenePos() method is used.
Another thing to do is to initialize the attribute setSceneRect() which is the space that QGraphicsView can see.
A recommendation if several buttons are used use a QButtonGroup that maps the buttons making easy the easy handling of the signals.
class GraphicsScene(QGraphicsScene):
def __init__(self, parent=None):
QGraphicsScene.__init__(self, parent)
self.setSceneRect(-100, -100, 200, 200)
self.opt = ""
def setOption(self, opt):
self.opt = opt
def mousePressEvent(self, event):
pen = QPen(QtCore.Qt.black)
brush = QBrush(QtCore.Qt.black)
x = event.scenePos().x()
y = event.scenePos().y()
if self.opt == "Generate":
self.addEllipse(x, y, 4, 4, pen, brush)
elif self.opt == "Select":
print(x, y)
class SimpleWindow(QtWidgets.QMainWindow, points.Ui_Dialog):
def __init__(self, parent=None):
super(SimpleWindow, self).__init__(parent)
self.setupUi(self)
self.scene = GraphicsScene(self)
self.graphicsView.setScene(self.scene)
self.graphicsView.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
group = QButtonGroup(self)
group.addButton(self.radioButton)
group.addButton(self.radioButton_2)
group.buttonClicked.connect(lambda btn: self.scene.setOption(btn.text()))
self.radioButton.setChecked(True)
self.scene.setOption(self.radioButton.text())
Related
I have a set of routines that essentially mirror the solution found here, except that I have my plot function in its own class that I import. I do this instead of leaving the code as a method within the MyApp class as the solution has in the given link. I make the code to plot the data in its own file because the actual code is very verbose.
My goal: I am trying to make it such that clicking on the graph will change a label in the GUI.
I've tried researching signals/slots and inheritance, but can't grasp how they can connect or apply when a module load is involved. Keep in mind I'm creating the GUI in PyQtDesigner.
Here is my code
main.py
import sys
from PyQt5.QtWidgets import QMainWindow
from PyQt5 import QtWidgets
from timeseries import plotData
import design
class MyApp(QMainWindow, design.Ui_MainWindow, plotData):
def __init__(self, parent=None):
super(self.__class__, self).__init__()
self.setupUi(self)
self.x = [1,2,3,4,5]
self.y = [1,2,3,4,5]
# Button click plots the timeseries in the first timeseries plot widget
self.pushButton_plotData.clicked.connect(lambda: self.plot())
if __name__ == '__main__':
# Create GUI application
app = QtWidgets.QApplication(sys.argv)
form = MyApp()
form.show()
app.exec_()
mplwidget.py
from PyQt5 import QtWidgets
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as Canvas
import matplotlib
# Ensure using PyQt5 backend
matplotlib.use('QT5Agg')
# Matplotlib canvas class to create figure
class MplCanvas(Canvas):
def __init__(self):
self.fig = Figure(dpi=100)
self.ax = self.fig.add_subplot(111)
Canvas.__init__(self, self.fig)
Canvas.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
Canvas.updateGeometry(self)
# Call when user clicks in the plot window
Canvas.mpl_connect(self, s='button_press_event', func=self.get_coord)
def get_coord(self, event):
if event.button == 1:
# Get the x coordinate of the mouse cursor in data coordinates
ix = event.xdata
print(ix)
# GOAL: Want to be able to execute:
# self.textBrowser_X1.setText(str(ix[0]))
# where textBrowser_X1 is the name of a line edit widget
# Matplotlib widget
class MplWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent) # Inherit from QWidget
self.canvas = MplCanvas() # Create canvas object
self.vbl = QtWidgets.QVBoxLayout() # Set box for plotting
self.vbl.addWidget(self.canvas)
self.setLayout(self.vbl)
timeseries.py
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
class plotData(FigureCanvas):
def __init__(self, parent=None):
super().__init__()
def plot(self):
self.plotWidget.canvas.ax.plot(self.x, self.y)
self.plotWidget.canvas.draw()
design.py
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1216, 379)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.pushButton_plotData = QtWidgets.QPushButton(self.centralwidget)
self.pushButton_plotData.setGeometry(QtCore.QRect(216, 20, 96, 33))
font = QtGui.QFont()
font.setPointSize(14)
self.pushButton_plotData.setFont(font)
self.pushButton_plotData.setObjectName("pushButton_plotData")
self.plotWidget = MplWidget(self.centralwidget)
self.plotWidget.setGeometry(QtCore.QRect(20, 75, 1177, 234))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.plotWidget.sizePolicy().hasHeightForWidth())
self.plotWidget.setSizePolicy(sizePolicy)
self.plotWidget.setObjectName("plotWidget")
self.textBrowser_X1 = QtWidgets.QTextBrowser(self.centralwidget)
self.textBrowser_X1.setGeometry(QtCore.QRect(132, 21, 75, 30))
self.textBrowser_X1.setObjectName("textBrowser_X1")
self.label_X1 = QtWidgets.QLabel(self.centralwidget)
self.label_X1.setGeometry(QtCore.QRect(25, 22, 170, 25))
font = QtGui.QFont()
font.setPointSize(14)
self.label_X1.setFont(font)
self.label_X1.setObjectName("label_X1")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1216, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.pushButton_plotData.setText(_translate("MainWindow", "Plot Data"))
self.label_X1.setText(_translate("MainWindow", "X position 1"))
from mplwidget import MplWidget
I see that there are many redundant things. For example because it is necessary that PlotWidget is a QWidget that only has the canvas if the canvas is a QWidget, another example is the class plotData that inherits unnecessarily from FigureCanvas in addition to doing a redundant task.
Considering the above, the solution is:
mplwidget.py
from PyQt5 import QtWidgets
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as Canvas
import matplotlib
# Ensure using PyQt5 backend
matplotlib.use("QT5Agg")
class MplWidget(Canvas):
def __init__(self, parent=None):
Canvas.__init__(self, Figure(dpi=100))
self.setParent(parent)
self.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
self.updateGeometry()
self.ax = self.figure.add_subplot(111)
main.py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
import design
class MyApp(QMainWindow, design.Ui_MainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setupUi(self)
self.x = [1, 2, 3, 4, 5]
self.y = [1, 2, 3, 4, 5]
self.pushButton_plotData.clicked.connect(self.plot)
self.plotWidget.mpl_connect(s="button_press_event", func=self.get_coord)
def plot(self):
self.plotWidget.ax.plot(self.x, self.y)
self.plotWidget.draw()
def get_coord(self, event):
if event.button == 1:
ix = event.xdata
self.textBrowser_X1.setText(str(ix))
if __name__ == "__main__":
# Create GUI application
app = QApplication(sys.argv)
form = MyApp()
form.show()
app.exec_()
I have a problem, i have made a QGraphicsView with 2 rectangles inside. I wanted them to resize when i resize the Dialog so i rewrite a dialog class. It works but at start i have the wrong sized rectangle
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsEllipseItem
import sys
# Redéfinition de dialog pour utiliser resize event
class MyDialog (QtWidgets.QDialog) :
def resizeEvent(self, event):
print("resize")
#print(view.sceneRect())
view.fitInView(scene.sceneRect(), QtCore.Qt.KeepAspectRatio) ######## I DONT KNOW WHY THIS LINE MAKE THE VIEW TOO SMALL AT START
QtWidgets.QDialog.resizeEvent(self, event)
#view.fitInView(rectangle)
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(556, 580)
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName("verticalLayout")
self.graphicsView = QtWidgets.QGraphicsView(Dialog)
self.graphicsView.setObjectName("graphicsView")
self.verticalLayout.addWidget(self.graphicsView)
global view
view = self.graphicsView
triangle = QtWidgets.QGraphicsRectItem(10,10,200,200)
rectangle = QtWidgets.QGraphicsRectItem(100,100,200,200)
global scene
scene = QtWidgets.QGraphicsScene()
scene.addItem(rectangle)
scene.addItem(triangle)
self.graphicsView.setScene(scene)
#self.graphicsView.fitInView(scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
#view.fitInView(scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
self.graphicsView_2 = QtWidgets.QGraphicsView(Dialog)
self.graphicsView_2.setObjectName("graphicsView_2")
self.verticalLayout.addWidget(self.graphicsView_2)
self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = MyDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())
When i start the QDialog it looks like this
And when i resize the window it looks like i wanted it to look like
Note: It is recommended not to modify the code generated by Qt Designer, so for my answer to work you must regenerate the file using pyuic5: pyuic5 your_file.ui -o gui.py -x.
So that the fitInView method uses some property that is not updated until it makes the QGraphicsView visible, so the solution is to verify that it is visible:
main.py
from PyQt5 import QtCore, QtGui, QtWidgets
from gui import Ui_Dialog
class MyDialog(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
triangle = QtWidgets.QGraphicsRectItem(10, 10, 200, 200)
rectangle = QtWidgets.QGraphicsRectItem(100, 100, 200, 200)
scene = QtWidgets.QGraphicsScene()
scene.addItem(rectangle)
scene.addItem(triangle)
self.graphicsView.setScene(scene)
def resizeEvent(self, event):
if self.isVisible():
self.graphicsView.fitInView(
self.graphicsView.scene().sceneRect(), QtCore.Qt.KeepAspectRatio
)
super().resizeEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MyDialog()
w.show()
sys.exit(app.exec_())
Using Python , PYQT5 I want to draw a Polygon on top a Image, which is in a Qlabel widget. I used a simple Qmainwindow with a label widget generated in QT designer (code is below).
I am aware that there are several informations out abaut drawing in a Qmainwindow like here:
PyQT5: How to interactively paint on image within QLabel Widget? - which has no solution within a Qlabel widget.
Draw over image in a QLabel with PyQt5 - marked solution is unclear because also painting on top of Qlabel is not solved
PYQT5 drawing line - paintevent on Qlabel is working, but not on top of the image
Painting in a QLabel with paintEvent - but also not solved to draw on top of an image.
Let me know, if you have a solution for this problem.
import sys
from PyQt5.QtCore import Qt
from PyQt5 import QtWidgets, uic, QtCore, QtGui
from PyQt5.QtGui import QPixmap, QPainter, QPolygon, QPen, QBrush
from PyQt5.QtCore import QPoint
from polygon_ui import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, *args, obj=None, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setupUi(self)
pixmap = QPixmap("img.png")
self.label.setPixmap(pixmap)
self.label.mousePressEvent = self.getPixel
self.pol = []
def getPixel(self, event):
x = event.pos().x()
y = event.pos().y()
self.pol.append(QPoint(int(x),int(y)))
print(x,y, self.pol)
def paintEvent(self, event):
painter = QPainter(self)
#painter.drawPixmap(self.rect(), self.image)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.setBrush(QBrush(Qt.red, Qt.VerPattern))
#points = QPolygon([ QPoint(10,10), QPoint(10,100),
# QPoint(100,10), QPoint(100,100)])
points = QPolygon(self.pol)
painter.drawPolygon(points)
def mouseMoveEvent(self, event):
pass
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
the code for polygon_ui is here - was simply generated by QT-Designer using Mainwindow + Qlabel:
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(621, 641)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setGeometry(QtCore.QRect(10, 10, 600, 600))
self.label.setObjectName("label")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 621, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.label.setText(_translate("MainWindow", "TextLabel"))
If you want to add elements such as polygons, lines, circles, etc. on an image then do not complicate yourself with a QLabel since for example with your current code you are painting in the window that is below the QLabel so it will not be seen , a possible solution using with QLabel is to get the QPixmap and paint it on top.
A better alternative is to use the Qt Graphics Framework, where the image is set to a QGraphicsPixmapItem, and a polygon as a child of the QGraphicsPixmapItem as I show below:
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = QtWidgets.QGraphicsPixmapItem()
scene.addItem(self.pixmap_item)
self._polygon_item = QtWidgets.QGraphicsPolygonItem(self.pixmap_item)
self.polygon_item.setPen(QtGui.QPen(QtCore.Qt.black, 5, QtCore.Qt.SolidLine))
self.polygon_item.setBrush(QtGui.QBrush(QtCore.Qt.red, QtCore.Qt.VerPattern))
#property
def pixmap_item(self):
return self._pixmap_item
#property
def polygon_item(self):
return self._polygon_item
def setPixmap(self, pixmap):
self.pixmap_item.setPixmap(pixmap)
def resizeEvent(self, event):
self.fitInView(self.pixmap_item, QtCore.Qt.KeepAspectRatio)
super().resizeEvent(event)
def mousePressEvent(self, event):
sp = self.mapToScene(event.pos())
lp = self.pixmap_item.mapFromScene(sp)
poly = self.polygon_item.polygon()
poly.append(lp)
self.polygon_item.setPolygon(poly)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
view = GraphicsView()
self.setCentralWidget(view)
view.setPixmap(QtGui.QPixmap("img.png"))
self.resize(640, 480)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Update:
If you want to use the OP design, the implementation is trivial.
Option1:
Create a file called graphicsview.py where the GraphicsView logic is implemented:
graphicsview.py
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = QtWidgets.QGraphicsPixmapItem()
scene.addItem(self.pixmap_item)
self._polygon_item = QtWidgets.QGraphicsPolygonItem(self.pixmap_item)
self.polygon_item.setPen(QtGui.QPen(QtCore.Qt.black, 5, QtCore.Qt.SolidLine))
self.polygon_item.setBrush(QtGui.QBrush(QtCore.Qt.red, QtCore.Qt.VerPattern))
#property
def pixmap_item(self):
return self._pixmap_item
#property
def polygon_item(self):
return self._polygon_item
def setPixmap(self, pixmap):
self.pixmap_item.setPixmap(pixmap)
def resizeEvent(self, event):
self.fitInView(self.pixmap_item, QtCore.Qt.KeepAspectRatio)
super().resizeEvent(event)
def mousePressEvent(self, event):
sp = self.mapToScene(event.pos())
lp = self.pixmap_item.mapFromScene(sp)
poly = self.polygon_item.polygon()
poly.append(lp)
self.polygon_item.setPolygon(poly)
Replace QLabel with QGraphicsView in polygon_ui:
polygon_ui.py
from PyQt5 import QtCore, QtGui, QtWidgets
from graphicsview import GraphicsView
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(621, 641)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.label = GraphicsView(self.centralwidget)
self.label.setGeometry(QtCore.QRect(10, 10, 600, 600))
self.label.setObjectName("label")
# ...
Restore the main.py
main.py
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from polygon_ui import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, *args, obj=None, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setupUi(self)
pixmap = QtGui.QPixmap("img.png")
self.label.setPixmap(pixmap)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Option2:
Another approach is to promote the widget, and in SO there are many examples of that type so I will obviate showing the procedure:
How to insert video in ui file which made at qt designer?
Clear QLineEdit on click event
Create a widget to embed into QMainWindow
where do I write the class for a single promoted QWidget from Qt designer
Option3:
Another simpler alternative is to use QLabel as a container and set the GraphicsView with a layout:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from polygon_ui import Ui_MainWindow
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = QtWidgets.QGraphicsPixmapItem()
scene.addItem(self.pixmap_item)
self._polygon_item = QtWidgets.QGraphicsPolygonItem(self.pixmap_item)
self.polygon_item.setPen(QtGui.QPen(QtCore.Qt.black, 5, QtCore.Qt.SolidLine))
self.polygon_item.setBrush(QtGui.QBrush(QtCore.Qt.red, QtCore.Qt.VerPattern))
#property
def pixmap_item(self):
return self._pixmap_item
#property
def polygon_item(self):
return self._polygon_item
def setPixmap(self, pixmap):
self.pixmap_item.setPixmap(pixmap)
def resizeEvent(self, event):
self.fitInView(self.pixmap_item, QtCore.Qt.KeepAspectRatio)
super().resizeEvent(event)
def mousePressEvent(self, event):
sp = self.mapToScene(event.pos())
lp = self.pixmap_item.mapFromScene(sp)
poly = self.polygon_item.polygon()
poly.append(lp)
self.polygon_item.setPolygon(poly)
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, *args, obj=None, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setupUi(self)
self.graphicsview = GraphicsView()
lay = QtWidgets.QVBoxLayout(self.label)
lay.addWidget(self.graphicsview)
pixmap = QtGui.QPixmap("img.png")
self.graphicsview.setPixmap(pixmap)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
I am working on a simple software, which has a GUI. (I'm using PyQt)
I have 2 radiobuttons. If the first is selected, then by clicking on the Graphicsscene a GraphicsItem will be added to the scene.
I would like to have a button, which would change the color of these points by pressing it. The color doesn't matter. It could be red for example. How could I do that? Thank you!
For placing the widgets I used the Qt Designer, and then created a subclass called SimpleWindow.
Here is the code:
The points class:
from PyQt5.QtCore import QRectF, Qt
from PyQt5.QtWidgets import QGraphicsItem
class Point(QGraphicsItem):
def __init__(self, x, y):
super(Point, self).__init__()
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.rectF = QRectF(0, 0, 4, 4)
self.x=x
self.y=y
def boundingRect(self):
return self.rectF
def paint(self, painter=None, style=None, widget=None):
painter.fillRect(self.rectF, Qt.black)
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtGui import QPen, QBrush
from PyQt5.QtWidgets import QGraphicsScene
The scene:
class PointsGraphicsScene(QGraphicsScene):
def __init__(self, parent=None):
QGraphicsScene.__init__(self, parent)
self.setSceneRect(0, 0, 200, 200)
self.opt = ""
def setOption(self, opt):
self.opt = opt
def mousePressEvent(self, event):
pen = QPen(QtCore.Qt.black)
brush = QBrush(QtCore.Qt.black)
x = event.scenePos().x()
y = event.scenePos().y()
if self.opt == "Generate":
p = point.Point(x, y)
p.setPos(x, y)
self.addItem(p)
elif self.opt == "Select":
print(x, y)
The dialog window:
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QButtonGroup
import window
from scene import PointsGraphicsScene
class SimpleWindow(QtWidgets.QMainWindow, window.Ui_Dialog):
def __init__(self, parent=None):
super(SimpleWindow, self).__init__(parent)
self.setupUi(self)
self.scene = PointsGraphicsScene(self)
self.graphicsView.setScene(self.scene)
self.graphicsView.setAlignment(QtCore.Qt.AlignLeft |
QtCore.Qt.AlignTop)
group = QButtonGroup(self)
group.addButton(self.radioButton)
group.addButton(self.radioButton_2)
group.buttonClicked.connect(lambda btn:
self.scene.setOption(btn.text()))
self.radioButton.setChecked(True)
self.scene.setOption(self.radioButton.text())
app = QtWidgets.QApplication(sys.argv)
form = SimpleWindow()
form.show()
app.exec_()
This is the the class generated by the Designer.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(538, 269)
self.graphicsView = QtWidgets.QGraphicsView(Dialog)
self.graphicsView.setGeometry(QtCore.QRect(130, 10, 371, 221))
self.graphicsView.setObjectName("graphicsView")
self.radioButton = QtWidgets.QRadioButton(Dialog)
self.radioButton.setGeometry(QtCore.QRect(20, 30, 82, 31))
self.radioButton.setObjectName("radioButton")
self.radioButton_2 = QtWidgets.QRadioButton(Dialog)
self.radioButton_2.setGeometry(QtCore.QRect(20, 80, 82, 17))
self.radioButton_2.setObjectName("radioButton_2")
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.radioButton.setText(_translate("Dialog", "Generate"))
self.radioButton_2.setText(_translate("Dialog", "Select"))
The first thing is to create a method that changes the color, in this case we will call it setBrush():
class Point(QGraphicsItem):
def __init__(self, x, y):
super(Point, self).__init__()
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.rectF = QRectF(0, 0, 4, 4)
self.x=x
self.y=y
self._brush = QBrush(Qt.black)
def setBrush(self, brush):
self._brush = brush
self.update()
def boundingRect(self):
return self.rectF
def paint(self, painter=None, style=None, widget=None):
painter.fillRect(self.rectF, self._brush)
The second thing to do is create the button and locate it in some position. then the QPushButton clicked signal is connected to some slot. Then we get the items of the scene through the items() method and change the color with setBrush():
class SimpleWindow(QtWidgets.QMainWindow, window.Ui_Dialog):
def __init__(self, parent=None):
super(SimpleWindow, self).__init__(parent)
self.setupUi(self)
self.scene = PointsGraphicsScene(self)
self.graphicsView.setScene(self.scene)
self.graphicsView.setAlignment(QtCore.Qt.AlignLeft |
QtCore.Qt.AlignTop)
group = QButtonGroup(self)
group.addButton(self.radioButton)
group.addButton(self.radioButton_2)
group.buttonClicked.connect(lambda btn:
self.scene.setOption(btn.text()))
self.radioButton.setChecked(True)
self.scene.setOption(self.radioButton.text())
button = QPushButton("change color", self)
button.move(20, 140)
button.clicked.connect(self.onClicked)
def onClicked(self):
for item in self.scene.items():
item.setBrush(QColor("red"))
app = QtWidgets.QApplication(sys.argv)
form = SimpleWindow()
form.show()
app.exec_()
I wrote the following program which draws a minefield for game the sapper
# -*- coding: utf-8 -*-
import mainw, sys
from PyQt4 import QtCore, QtGui
class WindowSapper(QtGui.QMainWindow):
buttons=[]
def __init__(self, parent=None):
QtGui.QMainWindow.__init__(self,parent)
self.ui=mainw.Ui_mainwin()
self.ui.setupUi(self)
for i in xrange(10):
l=[]
for j in xrange(10):
b=QtGui.QPushButton()
l.append(b)
self.ui.gridLayout.addWidget(b, i, j, 1, 1)
self.buttons.append(l)
def main():
app=QtGui.QApplication(sys.argv)
window=WindowSapper()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I apply also the form module
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'mainw.ui'
#
# Created: Tue Nov 27 08:52:39 2012
# by: PyQt4 UI code generator 4.9.4
#
# WARNING! All changes made in this file will be lost!
from PyQt4 import QtCore, QtGui
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
_fromUtf8 = lambda s: s
class Ui_mainwin(object):
def setupUi(self, mainwin):
mainwin.setObjectName(_fromUtf8("mainwin"))
mainwin.resize(546, 530)
self.centralwidget = QtGui.QWidget(mainwin)
self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
self.gridLayoutWidget = QtGui.QWidget(self.centralwidget)
self.gridLayoutWidget.setGeometry(QtCore.QRect(10, 30, 521, 461))
self.gridLayoutWidget.setObjectName(_fromUtf8("gridLayoutWidget"))
self.gridLayout = QtGui.QGridLayout(self.gridLayoutWidget)
self.gridLayout.setMargin(0)
self.gridLayout.setHorizontalSpacing(6)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
mainwin.setCentralWidget(self.centralwidget)
self.menubar = QtGui.QMenuBar(mainwin)
self.menubar.setGeometry(QtCore.QRect(0, 0, 546, 21))
self.menubar.setObjectName(_fromUtf8("menubar"))
mainwin.setMenuBar(self.menubar)
self.statusbar = QtGui.QStatusBar(mainwin)
self.statusbar.setObjectName(_fromUtf8("statusbar"))
mainwin.setStatusBar(self.statusbar)
self.retranslateUi(mainwin)
QtCore.QMetaObject.connectSlotsByName(mainwin)
def retranslateUi(self, mainwin):
mainwin.setWindowTitle(QtGui.QApplication.translate("mainwin", "Сапер", None, QtGui.QApplication.UnicodeUTF8))
class mainwin(QtGui.QMainWindow, Ui_mainwin):
def __init__(self, parent=None, f=QtCore.Qt.WindowFlags()):
QtGui.QMainWindow.__init__(self, parent, f)
self.setupUi(self)
But it did not meet my expectations: the buttons are not completely filled GridLayout, among them there are free spaces, that is, they are not completely filled cell GridLayout. How to get rid of these gaps?
The first reason you are seeing a ton of spacing is actually not because of the QGridLayout, but because nothing is constraining your layout objects to make them bunch up together. What you would need to do is add a stretch to the layout to eat up as much space as possible, forcing the rest of the items to push together.
QGridLayout does allow you to add stretch items to it, but I think that makes the grid more complicated to navigate later because you always have to account for that spacer row/col. So instead you can just wrap the grid layout in a vertical/horizontal layout and add spacers to those.
Once you do this, you will notice a tiny amount of space left between the rows. Apparently this is just a known thing with QGridLayout (see this other question). But you can play with the size of the buttons, and the min size of the row and columns to get it right:
Here is an example (standalone - not needing your UI module)
class WindowSapper(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QMainWindow.__init__(self,parent)
self.resize(450,350)
self.centralwidget = QtGui.QWidget()
self.setCentralWidget(self.centralwidget)
self.vLayout = QtGui.QVBoxLayout(self.centralwidget)
self.hLayout = QtGui.QHBoxLayout()
self.gridLayout = QtGui.QGridLayout()
self.gridLayout.setSpacing(0)
# center the grid with stretch on both sides
self.hLayout.addStretch(1)
self.hLayout.addLayout(self.gridLayout)
self.hLayout.addStretch(1)
self.vLayout.addLayout(self.hLayout)
# push grid to the top of the window
self.vLayout.addStretch(1)
self.buttons = []
for i in xrange(10):
l=[]
for j in xrange(10):
b=QtGui.QPushButton()
b.setFixedSize(40,30)
l.append(b)
self.gridLayout.addWidget(b, i, j)
self.gridLayout.setColumnMinimumWidth(j, 40)
self.buttons.append(l)
self.gridLayout.setRowMinimumHeight(i, 26)