Scroll to the bottom when adding an element to a QScrollArea - python

I have a QScrollArea and I would like when a I push my "Add" button that adds Widgets to the widgets contained in the QScrollArea for the scroll to scroll all the way to the bottom
I made several attempts to Scroll to the bottomw ith code like
scrollWidget.update()
bar = scrollWidget.verticalScrollBar()
bar.setValue(bar.maximum())
or even using ensureWidgetVisible But what appears to be happening is it scrolls to the bottom of the scroll "Before" the resize occurs, then it resizes so I am not quite at the bottom.
I verfied this by writing code that checks the bar size, and the maximum bar size and the child count
This shows there are new children but the bar size has not yet been updated.
I then tried to give Qt time to "recaluclate" sizes first by calling:
QApplication.processEvents()
scrollWidget.update()
I do not want the scroll area to ALWAYS be on the bottom but only after pushing my button

I just had to deal with the same issue, I figured out a solution that I think is good, although I'm a Qt newbie so take it with a grain of salt:
class MyMainWindow(QWidget):
def __init__(self, parent=None):
super(MyMainWindow, self).__init__(parent)
self.scrollarea = QScrollArea()
# [...]
self.vscrollbar = self.scrollarea.verticalScrollBar()
self.vscrollbar.rangeChanged.connect(self.scrollToBottom)
#Slot(int, int)
def scrollToBottom(self, minimum, maximum):
self.vscrollbar.setValue(maximum)
During construction we connect the rangeChanged Signal to our custom Slot scrollToBottom that sets the scrolling value to the maximum value, thereby scrolling down to the bottom every time the contents grow vertically.
I went a step further and made it only scroll to the bottom if the view was scrolled all the way to the bottom before the contents grew:
class MyMainWindow(QWidget):
def __init__(self, parent=None):
super(MyMainWindow, self).__init__(parent)
self.scrollarea = QScrollArea()
# [...]
self.vscrollbar = self.scrollarea.verticalScrollBar()
self.vscrollbar.rangeChanged.connect(self.scrollToBottomIfNeeded)
self.vscrollbar.valueChanged.connect(self.storeAtBottomState)
self.atbottom = True
#Slot(int)
def storeAtBottomState(self, value):
self.atbottom = value == self.vscrollbar.maximum()
#Slot(int, int)
def scrollToBottomIfNeeded(self, minimum, maximum):
if self.atbottom:
self.vscrollbar.setValue(maximum)
In the context of my application, this is the preferred behaviour, as the contents can grow while the user is looking at something in the ScrollArea, so autoscroll would prevent them from staying where they are. If your application only grows the contents after a user action, use the approach in the first snippet.
In response to your comment, this is how to only scroll down when adding an element:
class MyMainWindow(QWidget):
def __init__(self, parent=None):
super(MyMainWindow, self).__init__(parent)
self.scrollarea = QScrollArea()
self.addbutton = QPushButton()
self.addbutton.clicked.connect(self.addElement)
# [...]
self.vscrollbar = self.scrollarea.verticalScrollBar()
self.vscrollbar.rangeChanged.connect(self.scrollToBottomIfNeeded)
self.adding = False
#Slot()
def addElement(self):
self.adding = true
# ... actually add an element ...
#Slot(int, int)
def scrollToBottomIfNeeded(self, minimum, maximum):
if self.adding:
self.vscrollbar.setValue(maximum)
self.adding = False

Related

QMenu (contextMenuEvent) not closing on click on action nor parent QWidget

I am trying to create a context menu (QMenu) for a right-click inside of PatternWidget(QWidget) with PySide6.
For this purpose I have overwritten contextMenuEvent as seen below.
For whatever reason, the context menu will NOT exit, when any of the actions in it or any space around it within PatternWidget is clicked, whether by left nor by right click.
The only way I can make the QMenu close is to click inside another widget or outside of the whole Application window.
I spent a long time on Google already and it seems that people usually have the opposite problem (keeping it open when clicking somewhere). I therefore think that something must be wrong in my code that prevents the default behavior to close the QMenu rather than me having to connect to some Signal and close it manually
I was also wondering if the Mouse Click event is not bubbling up until the QMenu but,the signals of the actions in the QMenu (Copy Row Up and Down) are firing as expected when clicked.
EDIT:
Removed QScrollArea
Added Code for PatternOverlay
class MainWindow(QMainWindow):
...
def __init_layout(self):
self.layout = QHBoxLayout()
self.layout.setContentsMargins(10, 10, 10, 10)
self.layout.setSpacing(1)
self.mainWidget = QWidget()
self.mainWidget.setLayout(self.layout)
self._scene = QtWidgets.QGraphicsScene(self)
self._view = QtWidgets.QGraphicsView(self._scene)
self._view.setStyleSheet("border: 0px; background-color: Gainsboro")
self.pattern_widget = PatternWidget()
self._scene.addWidget(self.pattern_widget)
self.layout.addWidget(self._view)
self.setCentralWidget(self.mainWidget)
class PatternOverlay(QWidget):
def __init__(self, parent):
super().__init__(parent=parent)
self.setAttribute(Qt.WA_NoSystemBackground)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
self.rects = []
def paintEvent(self, _):
painter = QPainter(self)
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
for rect in self.rects:
painter.drawRect(rect)
painter.end()
self.rects.clear()
def addRect(self, rect):
self.rects.append(rect)
class PatternWidget(QWidget):
def __init__(self, cell_width, cell_height, stitches, rows):
super().__init__()
self.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
self.setMouseTracking(True)
# initialize overlay
self.__overlay = PatternOverlay(self)
self.__overlay.resize(self.size())
self.__overlay.show()
...
def contextMenuEvent(self, e):
copy_row_up_action = QAction("Copy Row Up", self)
copy_row_up_action.setStatusTip("Copy Row Up")
copy_row_up_action.triggered.connect(self.__handle_copy_row_up)
copy_row_down_action = QAction("Copy Row Down", self)
copy_row_down_action.setStatusTip("Copy Row Down")
copy_row_down_action.triggered.connect(self.__handle_copy_row_down)
context = QMenu(self)
context.addActions([copy_row_up_action, copy_row_down_action])
context.exec_(e.globalPos())

how to make an overriden QGraphicsTextItem editable & movable?

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

PyQt - Toggle between two widgets withtout resizing qsplitter

I try to toggle a splitter container between two widgets keeping the actual size of the splitter.
For this I use QSplitter.sizes() to read the actual size and QSplitter.setSizes() after I toggle my widgets.
The problem is that I have a QToolButton which I resize with setFixedSize() in a resizeEvent(), and because of this when I set the new size, it often doesn't work.
I write a little script to reproduce this :
In the left part of the splitter, I have a button to toggle the right part of the splitter between two classes (which are QWidgets).
A little precision : I want to keep my QToolbutton in a 1:1 aspect ratio.
Here a demo :
https://webmshare.com/play/5Bmvn
So here the script :
from PyQt4 import QtGui, QtCore
minSize = 50
maxSize = 350
class mainWindow(QtGui.QWidget):
def __init__(self):
super(mainWindow, self).__init__()
self.layout = QtGui.QVBoxLayout(self)
self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
self.splitter.setHandleWidth(20)
self.layout.addWidget(self.splitter)
wgt_left = QtGui.QWidget()
lyt_left = QtGui.QVBoxLayout(wgt_left)
self.btn_toggleSplitter = QtGui.QPushButton('Toggle Button')
self.btn_toggleSplitter.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
self.btn_toggleSplitter.setCheckable(True)
lyt_left.addWidget(self.btn_toggleSplitter)
self.splitter.addWidget(wgt_left)
self.first = panel('1')
self.second = panel('2')
self.splitter.addWidget(self.first)
self.width = self.first.size()
self.btn_toggleSplitter.clicked.connect(self.ToggleParent)
def ToggleParent(self):
self.sizes = self.splitter.sizes()
if self.btn_toggleSplitter.isChecked() == True:
self.first.setParent(None)
self.splitter.addWidget(self.second)
else :
self.second.setParent(None)
self.splitter.addWidget(self.first)
self.splitter.setSizes(self.sizes)
class panel(QtGui.QWidget):
def __init__(self, text):
super(panel, self).__init__()
lyt_main = QtGui.QVBoxLayout(self)
lyt_icon = QtGui.QHBoxLayout()
self.tbtn_icon = QtGui.QToolButton()
self.tbtn_icon.setText(text)
self.tbtn_icon.setMinimumSize(QtCore.QSize(minSize,minSize))
self.tbtn_icon.setMaximumSize(QtCore.QSize(maxSize,maxSize))
lyt_icon.addWidget(self.tbtn_icon)
lyt_horizontal = QtGui.QHBoxLayout()
lyt_horizontal.addWidget(QtGui.QPushButton('3'))
lyt_horizontal.addWidget(QtGui.QPushButton('4'))
lyt_main.addWidget(QtGui.QLabel('Below me is the QToolButton'))
lyt_main.addLayout(lyt_icon)
lyt_main.addLayout(lyt_horizontal)
lyt_main.addWidget(QtGui.QPlainTextEdit())
def resizeEvent(self, event):
w = panel.size(self).width()
h = panel.size(self).height()
size = min(h, w)-22
if size >= maxSize:
size = maxSize
elif size <= minSize:
size = minSize
self.tbtn_icon.setFixedSize(size, size)
app = QtGui.QApplication([])
window = mainWindow()
window.resize(600,300)
window.show()
app.exec_()
Thanks
You are looking for QtGui.QStackedWidget. Adding the widgets to this on the right side of your splitter will change the code around self.first and self.second's construction to this:
self.stack_right = QtGui.QStackedWidget()
self.splitter.addWidget(self.stack_right)
self.first = panel('1')
self.second = panel('2')
self.stack_right.addWidet(self.first)
self.stack_right.addWidget(self.second)
Then your ToggleParent method:
def ToggleParent(self):
if self.btn_toggleSplitter.isChecked() == True:
self.stack_right.setCurrentWidget(self.second)
else:
self.stack_right.setCurrentWidget(self.first)
This will avoid the awkwardness of caching and manually resizing your widgets.
Addendum:
The tool button scaling is really a separate question, but here's a tip:
Have a look at the heightForWidth layout setting for lyt_left. This will help you keep a 1:1 ratio for the QToolButton. You currently have a size policy of Preferred/Expanding, which doesn't make sense if you need a 1:1 aspect ratio. I highly recommend this over manually resizing the tool button while handling an event. Generally, calling setFixedSize more than once on a widget should be considered a last resort. Let the layouts do the work.
Addendum to addendum: doing a little poking (it's been awhile), you may need to inherit from QToolButton and reimplement the hasHeightForWidth() and heightForWidth() methods. There are a plethora of questions addressing the subject here. Just search for heightForWidth.

Have 2 pyqt buttons move synchronized when mouse moves

I am currently making a program where a user selects an image qpushbutton. I already have superseded mouseMoveEvent, mousePressEvent, and mouseReleaseEvent in the button class to get a movable button. The buttons are currently moving independently, but I would like the buttons to move so that the horizontal distance between them stays the same.
So currently in pseudo code I have:
import stuff
import mvbutton as mv
class MasterClass(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
#more setup stuff, layout, etc
self.addbutton(image,name,size)
def addbutton(#args):
self.button=mv.dragbutton(#args)
#some more setup
#now rename so that each button has its own name
if name== "name1":
self.name1=self.button
else:
self.name2=self.button
self.button=""
#more code to set up
I supersede the mouse motion/press/release functions in the dragbutton class. I cannot, therefore reference the new self.name# there. So the self.move(pos) in my dragbutton class cannot get the self.name# because it is a different self. Any ideas on how I could get this to work? Thanks.
Done something very rough after trying to understand your requirement.
Hope this helps.
EDIT
tried to add more accuracy in moving. Won't do real time moving cause it has problems with lag and update. I guess the moving won't be jittery any more.
from PyQt4 import QtGui
import sys
class MultiButton(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
self._b1 = QtGui.QPushButton("B1")
self._b2 = QtGui.QPushButton("B2")
self._arrangeWidgets()
self.setStyleSheet("background-color: rgb(0, 0, 0);\n"+\
"color: rgb(255, 255, 255);\n"+\
"border:1px solid #7F462C ;\n")
self._moveStart = False
self._startX = 0
self._startY = 0
def _arrangeWidgets(self):
layout = QtGui.QHBoxLayout()
layout.addWidget(self._b1)
#horizontal spacing remains constant now
layout.addSpacing(90)
layout.addWidget(self._b2)
self.setLayout(layout)
def mousePressEvent(self,event):
self._moveStart = True
self._startX = event.pos().x() - self.pos().x()
self._startY = event.pos().y() - self.pos().y()
return QtGui.QWidget.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
if self._moveStart:
self.setGeometry(event.pos().x() - self._startX,event.pos().y() - self._startY,self.width(),self.height())
self._moveStart = False
self._startX = 0
self._startY = 0
return QtGui.QWidget.mouseReleaseEvent(self, event)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
wd = QtGui.QMainWindow()
wd.resize(500,500)
mb = MultiButton()
mb.setFixedSize(200,50)
wd.setCentralWidget(mb)
wd.show()
sys.exit(app.exec_())
here the MultiButton widget moves the two buttons keeping the horizontal space between the two always constant.

PyQT custom widget fixed as square

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

Categories