Stop QScrollArea items from jumping around when adding new items through textinput - python

I have a simple QScrollarea that has a QWidget with a vertical box layout inside that contains a lot of widgets. I'm adding new widgets to it after mouse clicks and after text inputs in a QLineEdit.
However the scrollarea behaves differently depending on what caused an insert of a new item. If my addTestLabel() gets called after a textChanged() signal the whole contents jump around for a second, however if the same addTestLabel() is called from a mouseclick event it works exactly as it should, which doesn't make sense to me.
Gif example, at first I insert items by clicking and it works perfectly, then I start inserting by typing and it jumps around. Then I go back to clicking and it jumps after the first click but then all further clicks insert without jumping again
Shortest minimal example I could make:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QApplication, QScrollArea, QVBoxLayout, QSizePolicy, QLineEdit
from ui.tabdock.utils import *
class ChatView(QWidget):
def __init__(self):
super().__init__()
#make the main window a vertical box layout
self._boxLayout = QVBoxLayout()
self._boxLayout.setAlignment(Qt.AlignTop)
self.setLayout(self._boxLayout)
# make a scroll area and put it at the top
self._scrollArea = QScrollArea()
self._scrollArea.setWidgetResizable(True)
self._boxLayout.addWidget(self._scrollArea)
# add a text input below the scroll area
self._textInput = QLineEdit()
# make it add a new item every time textChange event fires
#THIS RESULTS IN EVERYTHING JUMPING FOR A SECOND
self._textInput.textChanged.connect(lambda t: self.addTestLabel("text change add"))
self._boxLayout.addWidget(self._textInput)
# make a container view with a vertical box layout that will contain all the actual items
self.listContainerView = QWidget()
self.listContainerView.setLayout(QVBoxLayout())
self.listContainerView.layout().setAlignment(Qt.AlignTop)
# put the container view inside the scroll area
self._scrollArea.setWidget(self.listContainerView)
#some items to get started
for i in range(10):
self.addTestLabel("label %s"%i)
#add new items whenever the mouse is pressed somewhere
# THIS INSERTS CORRECTLY WITHOUT JUMPING
self.mousePressEvent = lambda e:self.addTestLabel()
def addTestLabel(self, text = "complex widget goes here"):
label = QLabel(text)
label.setStyleSheet("border: 1px solid red;")
label.setFixedHeight(50)
self.listContainerView.layout().addWidget(label)
if __name__ == '__main__':
app = QApplication(sys.argv)
cv = ChatView()
cv.show()
cv.setGeometry(400,400,1200,800)
sys.exit(app.exec_())
How do I stop the split second of jumping around if addTestLabel() is called from the textChanged signal and make it behave like when it's called from the mouseclick?

So it turns out the issue was that whenever the scrollarea and its containerview were not in focus, like when I was typing in the textbox and that had focus, it would not instantly update the geometry and layout after adding something to the container widget so it would look weird for a second until it got around to update it.
Just adding self.listContainerView.updateGeometry() after adding the widgets to force a layout refresh fixed it.

Related

Sending a fake mouse click to an unfocused QLineEdit widget with PyQt

I am using PyQt5. I want to simulate a mouse click by a user on a specific QLineEdit widget programmatically, expecting the same behavior as for the real click.
The code below is my attempt, where this action is bound to a QPushButton. It works correctly when the QLineEdit widget target is focused. But if some other widget has focus when the button is pressed, both the focused widget and the target widget get the "focused" frame:
What should I do to avoid this problem and perform a simulated click correctly?
Note: it is not clear to me why calling .setFocus() in on_button_clicked below is necessary. I thought that a mouse click on a widget with a Qt.StrongFocus focus property is sufficient to switch focus, but the simulated click seems to be completely ignored by the target widget if the focus is not manually switched.
import functools
from PyQt5.QtWidgets import *
from PyQt5 import QtGui, QtCore
from PyQt5.QtCore import Qt
app = QApplication([])
win = QMainWindow()
widget = QWidget()
win.setCentralWidget(widget)
layout = QVBoxLayout()
widget.setLayout(layout)
target = QLineEdit('Widget to be controlled')
layout.addWidget(target)
layout.addWidget(QLineEdit('(widget to test focus)'))
button = QPushButton('Click')
layout.addWidget(button)
def on_button_clicked(target):
# This minimal example always uses the same position in the widget
pos = QtCore.QPointF(25, 10)
# Transfer focus, otherwise the click does not seem to be handled
target.setFocus(Qt.OtherFocusReason)
press_event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, pos,
Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
target.mousePressEvent(press_event)
release_event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonRelease, pos,
Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
target.mouseReleaseEvent(release_event)
button.clicked.connect(functools.partial(on_button_clicked, target))
win.show()
app.exec_()
(If it helps or if there are easier ways to do this, the thing I am actually trying to do is to construct a widget which behaves almost like a QLineEdit, but completely ignores single mouse clicks (both press and release events), while the double click works like a single click on a QLineEdit. Transforming the double click event into a single click event on the underlying QLineEdit widget is the part I struggle with.)

PyQt5 resize tabs to its content

Consider this example. I want to make a program where the main window is divided into three parts which can be resized. In the middle I want to have two widgets placed vertially, the bottom one is QTabWidget, where users can change certain properties. Currently I have only one tab and one property there can be more.
I saw similar questions (here and here) but I can't seem to fathom how all the different parts related to size and layout even work together in the first place + they were C++ questions.
Please help me resize QTabWidget to its minimum necessary size to show the contents of the current tab.
As side note you can point me to some understandable docs for a beginner in GUI and PyQt5.
import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QLineEdit, QLabel, QSplitter, QWidget, QListWidget, QApplication, QTabWidget, QGroupBox, \
QFormLayout, QSizePolicy, QLayout
from PyQt5.QtCore import Qt
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.init_tabs()
self.main_splitter = QSplitter(Qt.Horizontal)
some_left_widget = QWidget()
some_right_widget = QWidget()
mid = QSplitter(Qt.Vertical)
mid.addWidget(QListWidget())
mid.addWidget(self.tabs)
self.main_splitter.addWidget(some_left_widget)
self.main_splitter.addWidget(mid)
self.main_splitter.addWidget(some_right_widget)
self.setCentralWidget(self.main_splitter)
self.showMaximized()
def init_tabs(self):
self.properties_dict = {}
self.properties_dict['Text'] = QLineEdit()
self.tabs = QTabWidget()
self.properties_groupbox = QGroupBox("Overview")
layout = QFormLayout()
for k, v in self.properties_dict.items():
layout.addRow(QLabel(k + ':'), v)
self.properties_groupbox.setLayout(layout)
self.tabs.addTab(self.properties_groupbox, 'Properties')
# I have no idea how these work
self.properties_groupbox.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.properties_groupbox.resize(self.properties_groupbox.minimumSizeHint())
self.properties_groupbox.adjustSize()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Left one is now, right one is desired
A QSplitter uses complex computation to evaluate the sizes it assigns to each of its child widgets, especially when resizing (which is something that also happens as soon as it's first shown, like any other widget).
The most important aspects it takes into account are the widgets size hints (what the widget suggests it would be the preferable size) and size policy (how the widget can be resized and how it will behave if there's more or less available space).
To achieve what you want, you'll need to set the size policy stretch (which is the proportion of available space in the layout the widget will try to use).
Just add the following lines after adding the widgets to the splitter:
mid.setStretchFactor(0, 1)
mid.setStretchFactor(1, 0)
The first line indicates that the first widget (the list) will use a stretch factor of 1, while the second (the tab widget) will be 0. The stretch factor is computed based on the sum of all the stretch factors of the widgets.
In this way the list will try to uccupy the maximum available space (since 1 is the maximum of 1 + 0), while the tab the least.
Remember that stretch factor also consider the size hints of the widget, so if you set 2 to the list and 1 to the tab, you will not get a list with a height twice than that of the tab.
Also, as soon as the splitter is resized, the new proportions will be used when the splitter is resized, ignoring the previously set stretch factors.

Move pyqt button out of list of buttons

I have a list of pyqt4 push button and want to move the position. Since it is troublesome it make lots of buttons variable I create them through a list. The code below
import sys
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QVBoxLayout
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout(self)
self.buttons = []
for i in range(3):
self.buttons.append(QPushButton('',self))
self.buttons[-1].setFixedWidth(50)
self.buttons[-1].setFixedHeight(50)
self.buttons[-1].move(70*i+50,300)
layout.addWidget(self.buttons[-1])
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
window.resize(500,500)
window.show()
sys.exit(app.exec_())
don't work for specify the position
but
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout(self)
self.button1 = QPushButton('',self)
self.button1.setFixedWidth(50)
self.button1.setFixedHeight(50)
self.button1.move(50,300)
self.button2 = QPushButton('',self)
self.button2.setFixedWidth(50)
self.button2.setFixedHeight(50)
self.button2.move(120,300)
self.button3 = QPushButton('',self)
self.button3.setFixedWidth(50)
self.button3.setFixedHeight(50)
self.button3.move(190,300)
layout = QVBoxLayout(self)
layout.addWidget(self.button1)
layout.addWidget(self.button2)
layout.addWidget(self.button3)
works fine.
What's the reason behind?
If you want to manually specify the geometry (position and size) of widgets, you should not add them to a layout.
Your second example "works" just because you already created and set a layout to the top-level widget (the Window class), and since a layout already exists the second one is not "installed". Running it from the console will shows this error:
StdErr: QLayout: Attempting to add QLayout "" to Window "", which already has a layout
When a widget is added to a layout, the ownership of the "layout item" (an abstract item used by layouts to manage its widgets) is transferred to the layout, and the widget is reparented to the widget that uses that layout.
Since the second layout cannot be set, it will not manage the geometries of the items you tried to add, and the result is that they will keep the geometries you set before.
The same result can be obtained if you remove all the last lines referencing the other layout, which is exactly what you need.
Also, note that in order to add a widget that is not managed by a layout, a parent is required. Your last example also works because you specified the window as the parent while instantiating them; if you don't do that the buttons will not be shown, but if you do show them (with show() or setVisible(True)) they will each appear in separate windows, as they will become their own top level windows.
If you don't have other widgets that should be managed by a layout, you can also avoid creating the first layout at all (but, still, the parent is required).
Finally, let me tell you that using manual geometries is generally discouraged, and there are very few and specific cases for which it's a good idea to go with.
The main reason behind this is that widgets tend to show very differently from device to device, and that depends on various aspects:
different OS and OS versions draw widgets differently (sometimes dramatically), and this involves varying the widget size and behavior; while this might not be a major issue for simple widgets, it can be a problem for things like item views, spinboxes, etc;
different OS and systems use different styles, which also includes things like internal widget content margins and minimum size;
specific system settings (most importantly, the default font) could make widgets mostly unreadable, specifically with text being clipped within the widget margins if the font size is too big;
depending on the OS, you might face issues if the system uses High DPI displays, and you could end up with very tiny widgets that are almost impossible to interact with;
fixed geometries force you (and the user) to have a fixed widget/window size, and (along with the DPI issue above) this can be a problem: the same widget could look too big or too small;

PyQt5: Progamically adding QWidget to layout, doesn't show Qwidget when a spacer is added

Edit: I have tried a few more things. If I move the spacer to a layout below the spacer I am adding to it doesn't exibit the same exact behavior. Its still not optimal, and isn't going to work because my end goal is to have a scrollArea with a spacer inside that i add my widgets to, but this would not look right. I think the problem is the widgets are getting to a size zero I just do not know why or how to fix it.
I have two ui files made in QtDesigner. The first file is my main window at the start of the program I load the second ui file and place it into a vertical spacer in the middle of the first one. Additional copies are placed each time a button is clicked.
This all works great until I add a vertical spacer to push the items to the top. I have tired adding it from Designer and in the code. Both have the same result.
I have looked on google quite a bit and tried a lot of suggestions.
I tried setting the second ui files parent as a Qwidget I added on the first that contained the vertical layout.
I tried setting the minimum sizes and sizing polices to various things.
Below is my current code, any ideas or suggestions would be appreciated!
#!python3
import sys
from PyQt5 import QtWidgets, QtCore, uic
class TimesheetWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(TimesheetWidget, self).__init__(parent)
self.parent = parent
self.tableRows = dict()
def setup(self):
self.labelSaved.hide()
self.addTableRow()
def addTableRow(self):
thisRow = len(self.tableRows)
self.tableRows[thisRow] = uic.loadUi("gui/tableRow.ui")
self.tableRows[thisRow].addButton.clicked.connect(self.addTableRow)
self.spacer.addWidget(self.tableRows[thisRow])
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
timesheet = TimesheetWidget()
Mytimesheet = uic.loadUi("gui/timesheet.ui", baseinstance=timesheet)
Mytimesheet.setup()
Mytimesheet.show()
sys.exit(app.exec_())
here is a link to the ui files (they are to long to post):
gist link for ui files
I finally fixed this by trying random things over and over until something worked.
It turned out that on my second ui file I did not have a top level layout. (I am not sure if that's what its called.)
To fix this I right clicked on the top level widget and choose layout and selected horizontal layout, although I think any would have worked.
Here is a picture that shows the top level widget with the cancel symbol on it. Once I added the layout that went away and everything worked!

PyQt: getting widgets to resize automatically in a QDialog

I'm having difficulty getting widgets in a QDialog resized automatically when the dialog itself is resized.
In the following program, the textarea resizes automatically if you resize the main window. However, the textarea within the dialog stays the same size when the dialog is resized.
Is there any way of making the textarea in the dialog resize automatically? I've tried using setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) on the dialog itself and the two widgets within, but that seems to have no effect.
I'm using Qt version 3.3.7 and PyQt version 3.5.5-29 on openSuSE 10.2, if that's relevant.
import sys
from qt import *
# The numbers 1 to 1000 as a string.
NUMBERS = ("%d " * 1000) % (tuple(range(1,1001)))
# Add a textarea containing the numbers 1 to 1000 to the given
# QWidget.
def addTextArea(parent, size):
textbox = QTextEdit(parent)
textbox.setReadOnly(True)
textbox.setMinimumSize(QSize(size, size*0.75))
textbox.setText(NUMBERS)
class TestDialog(QDialog):
def __init__(self,parent=None):
QDialog.__init__(self,parent)
self.setCaption("Dialog")
everything = QVBox(self)
addTextArea(everything, 400)
everything.resize(everything.sizeHint())
class TestMainWindow(QMainWindow):
def __init__(self,parent=None):
QMainWindow.__init__(self,parent)
self.setCaption("Main Window")
everything = QVBox(self)
addTextArea(everything, 800)
button = QPushButton("Open dialog", everything)
self.connect(button, SIGNAL('clicked()'), self.openDialog)
self.setCentralWidget(everything)
self.resize(self.sizeHint())
self.dialog = TestDialog(self)
def openDialog(self):
self.dialog.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
mainwin = TestMainWindow(None)
app.setMainWidget(mainwin)
mainwin.show()
app.exec_loop()
QMainWindow has special behavior for the central widget that a QDialog does not. To achieve the desired behavior you need to create a layout, add the text area to the layout and assign the layout to the dialog.
Just to add a little note about this - I was trying to have a child window spawned from an application, which is a QDialog, containing a single QTextEdit as a child/content - and I wanted the QTextEdit to resize automatically whenever the QDialog window size changes. This seems to have done the trick for me with PyQt4:
def showTextWindow(self):
#QVBox, QHBox # don't exist in Qt4
dialog = QDialog(self)
#dialog.setGeometry(QRect(100, 100, 400, 200))
dialog.setWindowTitle("Title")
dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose)
textbox = QTextEdit(dialog)
textbox.setReadOnly(True)
textbox.setMinimumSize(QSize(400, 400*0.75))
textbox.setText("AHAAA!")
# this seems enough to have the QTextEdit
# autoresize to window size changes of dialog!
layout = QHBoxLayout(dialog)
layout.addWidget(textbox)
dialog.setLayout(layout)
dialog.exec_()
I had looked at using a QLayout before but had no luck. I was trying to do something like
dialog.setLayout(some_layout)
but I couldn't get that approach to work so I gave up.
My mistake was that I was trying to pass the layout to the dialog when I should have been passing the dialog to the layout.
Adding the lines
layout = QVBoxLayout(self)
layout.add(everything)
to the end of TestDialog.__init__ fixes the problem.
Thanks to Monjardin for prompting me to reconsider layouts.
Check out Python QT Automatic Widget Resizer It's suppose to work well.

Categories