Ensure that view shows scrollbar and adapts - python

This is a follow-up question. Here is the link to my previous question. The answer there works but the problem however I faced was that in the beginning, the line would be drawn to the mouse press taking it as the endpoint instead of been drawn from the mouse press taking it as the starting point. As suggested by a user there explicitly setting the sceneRect solves the problem, but however after explicitly setting the sceneRect the view stops adapting and showing scrollbar.
Here's a demo before adding sceneRect
As you could see the viewport automatically adjusts, when the mouse is over any of the edges, giving the user more space to draw. (Also note how the line is been drawn to the mouse press at the very beginning)
And Here is after if used self.setSceneRect(QtCore.QRectF(0, 0, 500, 500)) inside the constructor of the Scene class
As you could see it doesn't adjust the viewport itself when the mouse is near the edges of the screen unlike how it happened previously.
Now coming to my question, is there a way to make the view adapts to the changes inside the scene automatically after setting the sceneRect, or should I change sceneRect manually like shown here?

One possible solution is to update the sceneRect based on the size of the QGraphicsView, in addition to setting the view alignment to topleft:
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setMouseTracking(True)
scene = Scene()
self.setScene(scene)
def resizeEvent(self, event):
super().resizeEvent(event)
self.setSceneRect(
QtCore.QRectF(QtCore.QPointF(0, 0), QtCore.QSizeF(event.size()))
)
def main():
app = QtWidgets.QApplication(sys.argv)
view = GraphicsView()
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
If you want to observe the scrollbars then you can calculate the maximum between the previous sceneRect and the sceneRect based on the size of the viewport:
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setMouseTracking(True)
scene = Scene()
self.setScene(scene)
def resizeEvent(self, event):
super().resizeEvent(event)
r = QtCore.QRectF(QtCore.QPointF(0, 0), QtCore.QSizeF(event.size()))
self.setSceneRect(self.sceneRect().united(r))

Related

PyQt5: Why does the default style of a QGroupBox disappear when painting?

My application uses a variety of QGroupBoxes arranged in a QGridLayout to display information. I would like one of them to display custom shapes that I draw. Since I want the shapes to be it's GroupBox and it's difficult and not good to get the widget's position relative to the window, I decided to subclass the GroupBox and add the paint event to the subclass. This works beautifully, however it completely eliminates the default style of the GroupBox, including the title.
The following code creates a simple window with two GroupBoxes, one using the standard class and one with the paint event in a sub class. You should see that the one on the right only has the painted rectangle and none of the GroupBox style.
If you comment out the paint event, then the GroupBox displays as usual. Why is this happening and what should I do to keep the GroupBox style? Is there another way to use the painter within a GroupBox that doesn't use a paint event in a subclass?
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class MainWindow(QWidget):
def __init__(self, *args, **kwargs):
## Set up Window layout
super(MainWindow, self).__init__(*args, **kwargs)
self.layout = QGridLayout()
self.setLayout(self.layout)
self.setGeometry(300, 50, 1000, 700)
leftGroup = QGroupBox('Left Group')
self.layout.addWidget(leftGroup, 0, 0)
rightGroup = RightGroup()
self.layout.addWidget(rightGroup, 0, 1)
class RightGroup(QGroupBox):
def __init__(self):
super(RightGroup, self).__init__('Right Group')
def paintEvent(self, event):
painter = QPainter(self)
# Paint Style
painter.setPen(QPen(Qt.black, .5, Qt.SolidLine))
painter.drawRect(QRectF(0, 0, self.width(), self.height()))
if __name__ == '__main__':
## Creates a QT Application
app = QApplication([])
## Creates a window
window = MainWindow()
## Shows the window
window.show()
app.exec_()
Window with paint event enabled
Window with paint event commented out
You are overriding the whole painting, and "overriding" means that you ignore the default behavior.
The default behavior of paintEvent() of a standard Qt widget results in painting that widget (whether it's a button, a label, a groupbox, etc.). If you just override that, that widget will never be painted, unless you call the default implementation (i.e.: super().paintEvent(event)).
For instance, consider the following:
class RightGroup(QGroupBox):
pass
which will properly show a normal group box.
Now, this:
class RightGroup(QGroupBox):
def paintEvent(self, event):
pass
will show nothing, as you're explicitly ignoring the default painting behavior. To "revert" this and also add custom drawing:
class RightGroup(QGroupBox):
def paintEvent(self, event):
super().paintEvent(event)
qp = QPainter(self)
qp.drawRect(self.rect().adjusted(0, 0, -1, -1)
Note: unless you use antialiasing and the pen width is smaller or equal than 0.5, you should always restrict the right/bottom edges of the object rectangle to an integer smaller or equal to half of the pen width. That's the reason of the adjusted(0, 0, -1, -1) above. For your above code, it would have been QRectF(0, 0, self.width() - 1, self.height() - 1)
That said, you probably want to draw inside that groupbox, so, a better solution would be to create a subclass for the contents of that groupbox, add that to the groupbox and override the paint event of that subclass instead.
class MainWindow(QWidget):
def __init__(self, *args, **kwargs):
# ...
rightGroup = QGroupBox('Right Group')
self.layout.addWidget(rightGroup, 0, 1)
rightLayout = QVBoxLayout(rightGroup)
rightLayout.addWidget(Canvas())
class Canvas(QWidget):
def __init__(self):
super().__init__()
self.path = QPainterPath()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.path.moveTo(event.pos())
def mouseMoveEvent(self, event):
# note: here we use "buttons", not "button".
if event.buttons() == Qt.LeftButton:
self.path.lineTo(event.pos())
self.update()
def mouseReleaseEvent(self, event):
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHints(painter.Antialiasing)
painter.setPen(QPen(Qt.black, .5, Qt.SolidLine))
painter.drawRect(self.rect())
painter.drawPath(self.path)

Scale only one layer of image with QGraphicsView [duplicate]

I've been looking at other StackOverflow questions regarding this error (and elsewhere on the web) but I don't understanding how the answers relate to my code. So, I'm hoping for either a fixed example that makes sense to me, or a better explanation of how and when events occur.
The code below was intended to figure out the dimensions of the screen it's running on, resize to that and draw a circle in the center that occupies most of the available screen real estate. It tried to do a lot more, but I've stripped it down -- enough, I hope. Now it just tries to draw a circle.
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class Viewport(QGraphicsView):
def __init__(self, parent=None):
super(Viewport, self).__init__(parent)
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
def paintEvent(self, event):
super(Viewport, self).paintEvent(event)
qp = QPainter()
qp.begin(self)
square = QRect(10, 10, 30, 30)
qp.drawEllipse(square)
qp.end()
class UI(QDialog):
def __init__(self, parent=None):
super(UI, self).__init__(parent)
self.view = Viewport(self)
gridLayout = QGridLayout()
gridLayout.addWidget(self.view, 0, 0, 1, 1)
self.setLayout(gridLayout)
def resizeEvent(self, event):
super(UI, self).resizeEvent(event)
self.view.setFrameShape(QFrame.NoFrame)
self.view.setSceneRect(0, 0, 400, 400)
self.view.setFixedSize(400, 400)
app = QApplication(sys.argv)
ui = UI()
ui.show()
sys.exit(app.exec_())
The above was stripped out of broken code that had a moving SVG item and the circle originally had a gradient fill. The SVG item was displaying and moving okay but the circle never showed up.
The gradient-filled circle worked fine in another program when it was in a drawn by a paintEvent for a QGroupBox, but I cannot grok how QGraphicsScene and QGraphicsView work.
UPDATED
The error message, exactly as I see it (sadly w/o line numbers):
$ ./StackOverflow.py
QPainter::begin: Widget painting can only begin as a result of a paintEvent
QPainter::end: Painter not active, aborted
You need to paint on the viewport():
def paintEvent(self, event):
super(Viewport, self).paintEvent(event)
qp = QPainter()
qp.begin(self.viewport())
square = QRect(10, 10, 30, 30)
qp.drawEllipse(square)
qp.end()

setting a cursor in PyQt5 label widget

I am making an image acquisition program using a webcam in PyQt. Whenever I click on the image, which is inside a label widget, i need to put an additional fixed cursor at that position (to have a reference point for e.g). I created a cursor object, set a shape and position (obtained from the clicked position). yet I dont see an additional cursor being created at the clicked position i.e. Qpoint
below is the code snippet:
def eventFilter(self, source, event):
if event.type()==QtCore.QEvent.MouseButtonPress:
self.new_cursor=QtGui.QCursor() # the additional cursori want to place
self.new_cursor.setShape(self,Qt.PointingHandCursor) # setting shape
self.cursor_clicked=event.pos() # getting position from the click
self.cursor_clicked=self.label.mapFromParent(event.pos()) #mapping to widget coords.
self.cursor_x=self.cursor_clicked.x()
self.cursor_y=self.cursor_clicked.y()
self.new_cursor.setPos(self.cursor_x,self.cursor_y)
self.setCursor(self.new_cursor)
return QtWidgets.QWidget.eventFilter(self,source,event)
QCursor is not a "static image", but is an "abstract" object related to the mouse cursor, so there's no use of it for your purpose.
What you're looking for is drawing on the existing image or the widget that shows it.
Since you probably want to leave the image unchanged, the second option is what you're looking for.
The idea is that you call the base class implementation of the paintEvent method and then draw over it.
Painting a crosshair by hand is not that hard using simple lines, but you'll need to draw an extra border around the cross using a different color to ensure its visibility even on lighter or darker backgrounds, which makes it unnecessary long; in this example I'm using the cursor image Qt uses in the CursorShape enum documentation, but you can use whatever image you want, as soon as its center is exactly at the center of it (hint: use a square image with odd sized width/height).
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
self.label = QtWidgets.QLabel()
layout.addWidget(self.label)
self.label.setPixmap(QtGui.QPixmap('myimage.png'))
self.label.installEventFilter(self)
self.cursorPos = None
# I'm using the crosshair cursor as shown at
# https://doc.qt.io/qt-5/qt.html#CursorShape-enum
self.cursorPixmap = QtGui.QPixmap('cursor-cross.png')
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
# set the current position and schedule a repaint of the label
self.cursorPos = event.pos()
self.label.update()
elif event.type() == QtCore.QEvent.Paint:
# intercept the paintEvent of the label and call the base
# implementation to actually draw its contents
self.label.paintEvent(event)
if self.cursorPos is not None:
# if the cursor position has been set, draw it
qp = QtGui.QPainter(self.label)
# translate the painter at the cursor position
qp.translate(self.cursorPos)
# paint the pixmap at an offset based on its center
qp.drawPixmap(-self.cursorPixmap.rect().center(), self.cursorPixmap)
return True
return super().eventFilter(source, event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Besides that, another approach is to use the Graphics View Framework, add the pixmap to the scene and add/move another pixmap for the cursor when the user clicks on the image. Dealing with QGraphicsViews, QGraphicsScenes and their items is a bit more complex, but if you're going to need some more advanced level of interaction with an image, it usually is the better path to take.
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
self.view = QtWidgets.QGraphicsView()
layout.addWidget(self.view)
# remove any border around the view
self.view.setFrameShape(0)
self.scene = QtWidgets.QGraphicsScene()
self.view.setScene(self.scene)
pixmap = QtGui.QPixmap('myimage.png')
# adapt the view's size to that of the pixmap
self.view.setFixedSize(pixmap.size())
# add a pixmap to a scene, which returns a QGraphicsPixmapItem
self.pixmapItem = self.scene.addPixmap(pixmap)
self.crossHairItem = None
self.view.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
if not self.crossHairItem:
# as above, get a QGraphicsPixmapItem for the crosshair cursor
pixmap = QtGui.QPixmap('cursor-cross.png')
self.crossHairItem = self.scene.addPixmap(pixmap)
# set an offset of the item, so that its position is always
# based on the center of the pixmap
self.crossHairItem.setOffset(-pixmap.rect().center())
self.crossHairItem.setPos(self.view.mapToScene(event.pos()))
return super().eventFilter(source, event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Both methods behave in the same way for the user, and as you can see they look exactly identical.

Qt.ScrollBarAsNeeded not showing scrollbar when it's actually needed

I'm implementing a python application using PyQt5 and I encountered some problems when making use of a QScrollArea. This is the layout of my application:
It's composed of 2 QScrollArea (left and right pane) and a QMdiArea (center widget) arranged into a QHBoxLayout. When I expand the widgets on the left pane by clicking on the controls, and the height of the QWidget of the QScrollArea is bigger than then height of the QScrollArea itself, the scrollbar appears (as expected), but it's overlapping the content of the QScrollArea. To fix this problem I reimplemented the resizeEvent adding the necessary space for the scrollbar (till this point everything works.
Now, when I manually resize the Main Window, the left Pane gets more space and the scrollbar should disappear (but it doesn't) and it overlaps the widgets of the pane:
I also tried to manually toggle the visibility of the scrollbar (when the resizeEvent is received): when I do this, I can successfully hide the scrollbar but then I can't show it again (not matter if I call setVisible(True) on the scrollbar). This results in the space for the scrollbar being added, but the scrollbar is missing and the content of the pane is not scrollable:
Here is the implementation of the pane widget:
class Pane(QScrollArea):
MinWidth = 186
def __init__(self, alignment=0, parent=None):
super().__init__(parent)
self.mainWidget = QWidget(self)
self.mainLayout = QVBoxLayout(self.mainWidget)
self.mainLayout.setAlignment(alignment)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
self.setContentsMargins(0, 0, 0, 0)
self.setFrameStyle(QFrame.NoFrame)
self.setFixedWidth(Pane.MinWidth)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored)
self.setWidgetResizable(True)
self.setWidget(self.mainWidget)
def resizeEvent(self, resizeEvent):
if self.viewport().height() < self.widget().height():
self.setFixedWidth(Pane.MinWidth + 18)
# THIS DOESN'T WORK
#self.verticalScrollBar().show()
else:
self.setFixedWidth(Pane.MinWidth)
#self.verticalScrollBar().hide()
def addWidget(self, widget):
self.mainLayout.addWidget(widget)
def removeWidget(self, widget):
self.mainLayout.removeWidget(widget)
def update(self, *__args):
for item in itemsInLayout(self.mainLayout):
item.widget().update()
super().update(*__args)
What I want to achieve is pretty simple (but practically it seems not as simple): I would like to dynamically show the vertical scrollbar on my left/right pane widgets only when it's needed, and add the necessary space for the scrollbar so it doesn't overlap the widgets in the QScrollArea.
Before someone asks, I already tried to do something like this:
def resizeEvent(self, resizeEvent):
if self.viewport().height() < self.widget().height():
self.setFixedWidth(Pane.MinWidth + 18)
scrollbar = self.verticalScrollbar()
scrollbar.setVisible(True)
self.setVerticalScrollBar(scrollbar) ## APP CRASH
else:
self.setFixedWidth(Pane.MinWidth)
#self.verticalScrollBar().hide()
which results in my application to crash.
I hope that someone already faced this issue and is able to help me.
EDIT: I'm using PyQt5.5 compiled against Qt5.5 under OSX Yosemite 10.10.4 using clang.
Everything seems to work as expected for me without any need for workarounds. However, I strongly suspect there are additional constraints in your real code that you have not revealed in your question.
UPDATE
Below is a simple example that resizes the scrollareas when the scrollbars are shown/hidden:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QMainWindow):
def __init__(self):
super(Window, self).__init__()
widget = QtWidgets.QWidget(self)
layout = QtWidgets.QHBoxLayout(widget)
self.mdi = QtWidgets.QMdiArea(self)
self.leftScroll = Pane(
QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft, self)
self.rightScroll = Pane(
QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft, self)
layout.addWidget(self.leftScroll)
layout.addWidget(self.mdi)
layout.addWidget(self.rightScroll)
self.setCentralWidget(widget)
for scroll in self.leftScroll, self.rightScroll:
for index in range(4):
widget = QtWidgets.QTextEdit()
widget.setText('one two three four five')
scroll.addWidget(widget)
class Pane(QtWidgets.QScrollArea):
MinWidth = 186
def __init__(self, alignment=0, parent=None):
super().__init__(parent)
self.mainWidget = QtWidgets.QWidget(self)
self.mainLayout = QtWidgets.QVBoxLayout(self.mainWidget)
self.mainLayout.setAlignment(alignment)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
self.setContentsMargins(0, 0, 0, 0)
self.setFrameStyle(QtWidgets.QFrame.NoFrame)
self.setFixedWidth(Pane.MinWidth)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
QtWidgets.QSizePolicy.Ignored)
self.setWidgetResizable(True)
self.setWidget(self.mainWidget)
self.verticalScrollBar().installEventFilter(self)
def addWidget(self, widget):
self.mainLayout.addWidget(widget)
def removeWidget(self, widget):
self.mainLayout.removeWidget(widget)
def eventFilter(self, source, event):
if isinstance(source, QtWidgets.QScrollBar):
if event.type() == QtCore.QEvent.Show:
self.setFixedWidth(Pane.MinWidth + source.width())
elif event.type() == QtCore.QEvent.Hide:
self.setFixedWidth(Pane.MinWidth)
return super(Pane, self).eventFilter(source, event)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 300)
window.show()
sys.exit(app.exec_())

Moving a QGraphicsItem around a central point in PyQt4

I'm using Python 2.7 and PyQt4. I am trying to have a half-circle object that is a QGraphicsItem. I want to be able to move it using the mouse, by clicking and dragging. I can create the object and move it around with the mouse by setting the flag ItemIsMovable. Now the half-circle moves around freely but I want it to move just around the fixed central point. It is difficult to describe, but it should be something similar to a dial. How can I accomplish this?
you can use QGraphicsItem::mouseMoveEvent event to track item's movements within the scene and correct its position once it's moved off the restricted area. Pls, check if an example below would work for you:
import sys
from PyQt4 import QtGui, QtCore
class TestEclipseItem(QtGui.QGraphicsEllipseItem):
def __init__(self, parent=None):
QtGui.QGraphicsPixmapItem.__init__(self, parent)
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
# set move restriction rect for the item
self.move_restrict_rect = QtCore.QRectF(20, 20, 200, 200)
# set item's rectangle
self.setRect(QtCore.QRectF(50, 50, 50, 50))
def mouseMoveEvent(self, event):
# check of mouse moved within the restricted area for the item
if self.move_restrict_rect.contains(event.scenePos()):
QtGui.QGraphicsEllipseItem.mouseMoveEvent(self, event)
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
scene = QtGui.QGraphicsScene(-50, -50, 600, 600)
ellipseItem = TestEclipseItem()
scene.addItem(ellipseItem)
view = QtGui.QGraphicsView()
view.setScene(scene)
view.setGeometry(QtCore.QRect(0, 0, 400, 200))
self.setCentralWidget(view)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
hope this helps, regards

Categories

Resources