I'm trying to build an interface using custom widgets, and have run into the following problem.
I have a widget Rectangle which I want to use as an interactive element in my interface. To define a rectangle I just need to give it a parent, so it knows what window to draw itself in, and a position [x,y, width, height] defining its position and size. (I know that some of you will say "You should be using layouts as opposed to absolute positioning" but I am 100% sure that I need absolute positioning for this particular application).
from PySide.QtCore import *
from PySide.QtGui import *
import sys
class Rectangle(QWidget):
def __init__(self, parent, *args):
super(self.__class__,self).__init__(parent)
print parent, args
#expect args[0] is a list in the form [x,y,width,height]
self.setGeometry(*args[0])
def enterEvent(self, e):
print 'Enter'
def leaveEvent(self, e):
print 'Leave'
def paintEvent(self, e):
print 'Painted: ',self.pos
painter = QPainter(self)
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(200,100,100))
painter.drawRect(0,0,self.width()-1, self.height()-1)
painter.end()
I also have a Window widget which is the canvas on which my visualization is to be drawn. In the Window's __init__() definition I create a rectangle A at 20,40.
class Window(QWidget):
def __init__(self):
super(self.__class__, self).__init__()
self.widgets = [Rectangle(self,[20,40,100,80])]
self.setMouseTracking(True)
self.setGeometry(300,300,800,600)
self.setWindowTitle('Window')
self.show()
def addWidget(self,Widget, *args):
self.widgets += [Widget(self, *args)]
self.update()
def mousePressEvent(self, e):
for widget in self.widgets:
print widget.geometry()
Since I am building a visualization, I want to create my Window and then add widgets to it afterwords, so I create an instance mWindow, which should already have rectangle A defined. I then use my window's addWidget() method to add a second rectangle at 200,200 - call it rectangle B.
if __name__ == "__main__":
app= QApplication(sys.argv)
mWindow = Window()
mWindow.addWidget(Rectangle, [200,200,200,80])
sys.exit(app.exec_())
The issue I have is that only rectangle A actually gets drawn.
I know that both rectangle A and **rectangle B are getting instantiated and both have myWindow as their parent widgets, because of the output of print parent in the constructor for Rectangle.
However, when I resize the window to force it to repaint itself, the paintEvent() method is only called on rectangle A, not rectangle B. What am I missing?
You just forgot to show the rectangle. In addWidget, add this before self.update():
self.widgets[-1].show()
The reason why you don't need show for the first rectangle object is because it is
created in the Window constructor. Then, Qt itself is making sure objects are properly
shown (which is misleading, I agree...).
Related
Why my program is closing when I am using Slider in pyqt5? It starts normal but after using Slider it close. I have to change size of circle in first window by using Slider from second window. This is my full code, beacuse I don't know what could be wrong.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QPainter, QBrush, QPen
from PyQt5.QtCore import Qt
import sys
class ThirdWindow(QWidget):
def __init__(self):
super().__init__()
hbox = QHBoxLayout()
sld = QSlider(Qt.Horizontal, self)
sld.setRange(0, 100)
sld.setFocusPolicy(Qt.NoFocus)
sld.setPageStep(5)
sld.valueChanged.connect(self.updateLabel)
self.label = QLabel('0', self)
self.label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
self.label.setMinimumWidth(80)
hbox.addWidget(sld)
hbox.addSpacing(15)
hbox.addWidget(self.label)
self.setLayout(hbox)
self.setGeometry(600, 60, 500, 500)
self.setWindowTitle('QSlider')
self.show()
def updateLabel(self, value):
self.label.setText(str(value))
self.parentWidget().radius = value
self.parentWidget().repaint()
class Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowTitle("Dialogi")
self.w = ThirdWindow()
actionFile = self.menuBar().addMenu("Dialog")
action = actionFile.addAction("Zmień tło")
action1 = actionFile.addAction("Zmień grubość koła")
action1.triggered.connect(self.w.show)
self.setGeometry(100, 60, 300, 300)
self.setStyleSheet("background-color: Green")
self.radius = 100 # add a variable radius to keep track of the circle radius
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.gray, 8, Qt.SolidLine))
# change function to include radius
painter.drawEllipse(100, 100, self.radius, self.radius)
app = QApplication(sys.argv)
screen = Window()
screen.show()
sys.exit(app.exec_())
parentWidget allows access to a widget declared as parent for Qt.
Creating an object using self.w = ... doesn't make that object a child of the parent (self), you are only creating a reference (w) to it.
If you run your program in a terminal/prompt you'll clearly see the following traceback:
Exception "unhandled AttributeError"
'NoneType' object has no attribute 'radius'
The NoneType refers to the result of parentWidget(), and since no parent has been actually set, it returns None.
While you could use parentWidget if you correctly set the parent for that widget (by adding the parent to the widget constructor, or using setParent()), considering the structure of your program it wouldn't be a good choice, and it's usually better to avoid changes to a "parent" object directly from the "child": the signal/slot mechanism of Qt is exactly intended for the modularity of Object Oriented Programming, as a child should not really "care" about its parent.
The solution is to connect to the slider from the "parent", and update it from there.
class ThirdWindow(QWidget):
def __init__(self):
# ...
# make the slider an *instance member*, so that we can easily access it
# from outside
self.slider = QSlider(Qt.Horizontal, self)
# ...
class Window(QMainWindow):
def __init__(self):
# ...
self.w.slider.valueChanged.connect(self.updateLabel)
def updateLabel(self, value):
self.radius = value
self.update()
You obviously need to remove the valueChanged connection in the ThirdWindow class, as you don't need it anymore.
Also note that you rarely need repaint(), and update() should be preferred instead.
I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.
I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...
Here is a Minimal Reproducible Example :
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen
class NodeTag(QGraphicsTextItem):
def __init__(self,text):
QGraphicsTextItem.__init__(self,text)
self.text = text
self.setPos(0,0)
self.setTextInteractionFlags(Qt.TextEditorInteraction)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True) # All these flags are ignored...
# self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def boundingRect(self):
return QRectF(0,0,80,25)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
painter.drawText(self.boundingRect(),self.text)
def mousePressEvent(self, event):
print("CLICK!")
# self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
# self.setFocus()
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
tagItem = NodeTag("myText") # create a NodeTag item
self.addItem(tagItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__() # create default constructor for QWidget
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far.
Any help appreciated!
All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.
The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.
Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.
Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.
Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.
Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).
Here's a full implementation of what described above:
class NodeTag(QGraphicsTextItem):
def __init__(self, text):
QGraphicsTextItem.__init__(self, text)
self.startPos = None
self.isMoving = False
# the following is useless, not only because we are leaving the text
# painting to the base implementation, but also because the text is
# already accessible using toPlainText() or toHtml()
#self.text = text
# this is unnecessary too as all new items always have a (0, 0) position
#self.setPos(0, 0)
def boundingRect(self):
return super().boundingRect() | QRectF(0, 0, 80, 25)
def paint(self, painter, option, widget):
# draw the border *before* (as in "behind") the text
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
super().paint(painter, option, widget)
def shape(self):
shape = QPainterPath()
shape.addRect(self.boundingRect())
return shape
def focusOutEvent(self, event):
# this is required in order to allow movement using the mouse
self.setTextInteractionFlags(Qt.NoTextInteraction)
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.startPos:
delta = event.pos() - self.startPos
if (self.isMoving or
delta.manhattanLength() >= QApplication.startDragDistance()):
self.setPos(self.pos() + delta)
self.isMoving = True
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if (not self.isMoving and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.setTextInteractionFlags(Qt.TextEditorInteraction)
self.setFocus()
# the following lines are used to correctly place the text
# cursor at the mouse cursor position
cursorPos = self.document().documentLayout().hitTest(
event.pos(), Qt.FuzzyHit)
textCursor = self.textCursor()
textCursor.setPosition(cursorPos)
self.setTextCursor(textCursor)
super().mouseReleaseEvent(event)
self.startPos = None
self.isMoving = False
As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.
Try it:
...
class NodeTag(QGraphicsTextItem):
def __init__(self, text, parent=None):
super(NodeTag, self).__init__(parent)
self.text = text
self.setPlainText(text)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
def focusOutEvent(self, event):
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
super(NodeTag, self).focusOutEvent(event)
def mouseDoubleClickEvent(self, event):
if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
super(NodeTag, self).mouseDoubleClickEvent(event)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
# painter.drawText(self.boundingRect(),self.text)
super().paint(painter, option, widget)
...
I'm writing a simulation GUI, in which QtPainter is draws a Pixmap instantly when the windows is opening.
I use different buttons, that are supposed to result in painting on top of the pixmap (lines and other geometric forms)
I've tried writing functions that take the QtPainter object as an argument and use button.clicked.connect() to call the functions but the drawings never appear on screen.
As I am new to PyQt I am not sure with how it works, but I guess I can only paint by calling the paintEvent() function, but if I write all geometric forms in paintEvent(), how do I make sure they only appear when the button is pressed?
To draw directly onto a widget you can override it's paintEvent. The thing to remember when doing so is that paintEvent fires every time the Widget deems it necessary to redraw itself e.g. when it has been resized or moved. This means that when implementing your own version of paintEvent you need to draw all the shapes you want drawn on your widget. Note that paintEvent is rarely called directly. If you want the widget to redraw itself, you should call update() which will schedule a paintEvent for you.
Here is a simple example where arbitrary rectangles are added to a canvas when clicking on a button. The rectangles are stored in an array in the Canvas object. In Canvas.paintEvent, we create an instance of QPainter and use this object to draw all the rectangles in the array.
from PyQt5 import QtWidgets, QtGui, QtCore
from random import randrange
class Canvas(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rectangles = []
def add_rectangle(self, rect, color):
self.rectangles.append((rect, color))
self.update()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
brush = QtGui.QBrush(QtCore.Qt.white)
painter.setBrush(brush)
painter.drawRect(event.rect())
pen = QtGui.QPen()
pen.setWidth(3)
for rect, color in self.rectangles:
pen.setColor(color)
painter.setPen(pen)
brush.setColor(color)
painter.setBrush(brush)
painter.drawRect(rect)
class MyWindow(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.canvas = Canvas(self)
self.button = QtWidgets.QPushButton('Add rectangle')
self.button.clicked.connect(self.add_rectangle)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.addWidget(self.canvas)
self.layout.addWidget(self.button)
self.resize(500,500)
def add_rectangle(self):
w = self.canvas.width()
h = self.canvas.height()
x0, y0 = randrange(w), randrange(h)
x1, y1 = randrange(w), randrange(h)
shape = QtCore.QRect(min(x0,x1), min(y0,y1), abs(x0-x1), abs(y0-y1))
color = QtGui.QColor.fromRgb(*(randrange(256) for i in range(3)), 180)
self.canvas.add_rectangle(shape, color)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
window = MyWindow()
window.show()
app.exec()
I'm developing a custom widget (inheriting from QWidget) to use as a control. How can I fix the aspect-ratio of the widget to be square, but still allow it to be resized by the layout manager when both vertical and horizontal space allows?
I know that I can set the viewport of the QPainter so that it only draws in a central square area, but that still allows the user to click either side of the drawn area.
It seems like there is no universal way to keep a widget square under all circumstances.
You must choose one:
Make its height depend on its width:
class MyWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
policy.setHeightForWidth(True)
self.setSizePolicy(policy)
...
def heightForWidth(self, width):
return width
...
Make its minimal width depend on its height:
class MyWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
...
def resizeEvent(self, e):
setMinimumWidth(height())
...
Such a widget will be kept square as long as there is such a possibility.
For other cases you should indeed consider changing the viewport, as you mentioned. Mouse events shouldn't be that much of a problem, just find the center of the widget (divide dimensions by 2), find min(width, height) and go from there. You should be able to validate the mouse events by coordinate. It is nice to call QMouseEvent.accept, only if the event passed the validation and you used the event.
I'd go with BlaXpirit's method, but here's an alternative that I've used before.
If you subclass the custom widget's resiseEvent() you can adjust the requested size to make it a square and then set the widget's size manually.
import sys
from PyQt4 import QtCore, QtGui
class CustomWidget(QtGui.QFrame):
def __init__(self, parent=None):
QtGui.QFrame.__init__(self, parent)
# Give the frame a border so that we can see it.
self.setFrameStyle(1)
layout = QtGui.QVBoxLayout()
self.label = QtGui.QLabel('Test')
layout.addWidget(self.label)
self.setLayout(layout)
def resizeEvent(self, event):
# Create a square base size of 10x10 and scale it to the new size
# maintaining aspect ratio.
new_size = QtCore.QSize(10, 10)
new_size.scale(event.size(), QtCore.Qt.KeepAspectRatio)
self.resize(new_size)
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
layout = QtGui.QVBoxLayout()
self.custom_widget = CustomWidget()
layout.addWidget(self.custom_widget)
self.setLayout(layout)
app = QtGui.QApplication(sys.argv)
window = MainWidget()
window.show()
sys.exit(app.exec_())
Is it possible to scale (or "zoom") a QTextEdit area? I believe I read that placing QTextEdit inside QLayout can allow for scaling of the QTextEdit area, though did not find how to implement it. Couple of options...
CTRL + Roll of Mouse Wheel
Running the code below, holding down the CTRL (control) key and rolling the mouse wheel, the event is captured and the text does scale (at least on Windows), however, as the text grows larger the wheel has to move further and further for very much effect, so one goal is to be able to modify that somehow, maybe some math to increase the increments to a greater degree on increases in the plus direction.
(The setReadOnly()'s below are because it would seem textEdit has to be ReadOnly(False) for the mouse event to be captured, then True to be able to scale during roll of the mouse wheel, so it is then set back to original state of False again on release of the CTRL key).
Toolbar Button Click
The other option is toolbar buttons for zoom in and out.
onZoomInClicked() is called.
Some current problems with the code below
1. It prints: QLayout: Attempting to add QLayout "" to MainWindow "", which already has a layout and I don't have my head wrapped around that yet.
2. QtGui.QTextEdit(self.formLayout) instead of (self) to place the textEdit area inside the layout produces TypeError: 'PySide.QtGui.QTextEdit' called with wrong argument types
3. wheelEvent() could use some way to modify event.delta() maybe?
4. The toolbar button (text only) will currently run its def when clicked, however it only contains a print statement.
from PySide import QtGui, QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.formLayout = QtGui.QFormLayout(self)
self.textEdit = QtGui.QTextEdit(self)
self.toolBar = QtGui.QToolBar(self)
self.actionZoomIn = QtGui.QAction(self)
self.textEdit.setHtml('<font color=blue>Hello <b>world</b></font>')
self.setCentralWidget(self.textEdit)
self.addToolBar(self.toolBar)
self.toolBar.addAction(self.actionZoomIn)
self.actionZoomIn.setText('Zoom In')
self.actionZoomIn.connect(self.actionZoomIn,
QtCore.SIGNAL('triggered()'), self.onZoomInClicked)
def onZoomInClicked(self):
print "onZoomInClicked(self) needs code"
def wheelEvent(self, event):
print "wheelEvent() captured"
if (event.modifiers() & QtCore.Qt.ControlModifier):
self.textEdit.setReadOnly(True)
event.accept()
def keyReleaseEvent(self, evt):
if evt.key() == QtCore.Qt.Key_Control:
self.textEdit.setReadOnly(False)
if __name__ == '__main__':
app = QtGui.QApplication([])
frame = MainWindow()
frame.show()
app.exec_()
I've been grappling with this for days so would be great to have the more customizable QTextEdit scale/zoom working if it is even possible.
The two error messages can be expained as follows:
The QMainWidget automatically gets a layout, so the QFormLayout is redundant. If you want to add a layout, create a QWidget to be the central widget and make it the parent of the new layout. Other widgets can then be added to that new layout.
The parent of a QWidget subclass must itself be QWidget subclass, which QFormLayout isn't.
I've modified your example so that it does most of what you asked for. Note that QTextEdit.zoomIn and QTextEdit.zoomOut both take a range argument for controlling the degree of zoom.
from PySide import QtGui, QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.textEdit = Editor(self)
self.toolBar = QtGui.QToolBar(self)
self.actionZoomIn = QtGui.QAction('Zoom In', self)
self.actionZoomOut = QtGui.QAction('Zoom Out', self)
self.textEdit.setHtml('<font color=blue>Hello <b>world</b></font>')
self.setCentralWidget(self.textEdit)
self.addToolBar(self.toolBar)
self.toolBar.addAction(self.actionZoomIn)
self.toolBar.addAction(self.actionZoomOut)
self.actionZoomIn.triggered.connect(self.onZoomInClicked)
self.actionZoomOut.triggered.connect(self.onZoomOutClicked)
def onZoomInClicked(self):
self.textEdit.zoom(+1)
def onZoomOutClicked(self):
self.textEdit.zoom(-1)
class Editor(QtGui.QTextEdit):
def __init__(self, parent=None):
super(Editor, self).__init__(parent)
def zoom(self, delta):
if delta < 0:
self.zoomOut(1)
elif delta > 0:
self.zoomIn(5)
def wheelEvent(self, event):
if (event.modifiers() & QtCore.Qt.ControlModifier):
self.zoom(event.delta())
else:
QtGui.QTextEdit.wheelEvent(self, event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()