I have a QlistWidget and I need to implement on this an Infinite Scroll, something like this HTML example:
https://scrollmagic.io/examples/advanced/infinite_scrolling.html
Basically, when the user scrolls to the last item of the list, I need to load more items and dynamically append it in the QlistWidget.
Is it possible? I didn't find any example yet.
There are likely many ways to achieve this task, but the easiest I found is to watch for changes in the scroll bar, and detect if we're at the bottom before adding more items to the list widget.
import sys, random
from PyQt5.QtWidgets import QApplication, QListWidget
class infinite_scroll_area(QListWidget): #https://doc.qt.io/qt-5/qlistwidget.html
def __init__(self):
super().__init__() #call the parent constructor if you're overriding it.
#connect our own function the valueChanged event
self.verticalScrollBar().valueChanged.connect(self.valueChanged)
self.add_lines(15)
self.show()
def valueChanged(self, value): #https://doc.qt.io/qt-5/qabstractslider.html#valueChanged
if value == self.verticalScrollBar().maximum(): #if we're at the end
self.add_lines(5)
def add_lines(self, n):
for _ in range(n): #add random lines
line_text = str(random.randint(0,100)) + ' some data'
self.addItem(line_text)
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = infinite_scroll_area()
sys.exit(app.exec_())
You can directly grab scroll wheel events by overriding the wheelEvent method of QListWidget, then do the logic there which solves the potential problem of not starting out with enough list items for the scrollbar to appear. If it's not there, it can't change value, and the event can't fire. It introduces a new problem however as scrolling with the mouse wheel is not the only way to scroll the view (arrow keys, page up/down keys, etc). With the number of classes and subclasses in any gui library, it becomes imperative to get really familiar with the documentation. It's a little inconvenient that it isn't as comprehensive for python specifically, but I think the c++ docs are second to none as far as gui library documentation goes.
Related
I'm using the Qt framework to build my graphical user interface. I use a QGridLayout to position my QWidgets neatly.
The GUI looks like this:
My application regularly adds new widgets to the GUI at runtime. These new widgets are usually not added at the end of the QLayout, but somewhere in the middle.
The procedure to do this is a bit cumbersome. Applied on the figure above, I would need to take out widg_C, widg_D, ... from the QGridLayout. Next, I add widg_xand widg_y, and finally I put the other widgets back again.
This is how I remove widgets from the QGridLayout:
for i in reversed(range(myGridLayout.count())):
self.itemAt(i).widget().setParent(None)
###
As long as you're dealing with a small amount of widgets, this procedure is not a disaster. But in my application I display a lot of small widgets - perhaps 50 or more! The application freezes a second while this procedure is ongoing, which is very annoying to the user.
Is there a way to insert widgets somewhere in a QLayout, without the need to take out other widgets?
EDIT: Apparently the solution for the QVBoxLayout is very simple. Just use the function insertWidget(..) instead of addWidget(..). The docs can be found a this link: http://doc.qt.io/qt-5/qboxlayout.html#insertWidget
Unfortunately, I couldn't find a similar function for the QGridLayout.
EDIT: Many people rightly mentioned that putting back a lot of widgets shouldn't cause a performance issue - it is very fast indeed (thank you #ekhumoro to point that out). Apparently, the performance issue I faced had to do with the algorithm putting the widgets back. It is a fairly complicated recursive algorithm that puts every widget on the right coordinates in the QGridLayout. This resulted in a "flicker" on my display. The widgets are taken out, and put back inside with some delay (due to the algorithm) - causing the flicker.
EDIT: I found a solution such that I can easily insert new rows into the QGridLayout. Inserting new rows means that I don't need to take out and replace all the widgets from scratch - hence I avoid the expensive recursive algorithm to run.
The solution can be found in my answer below.
Thank you #ekhumoro, #Stuart Fisher, #vahancho and #mbjoe for your help. I eventually found a way to solve the issue. I no longer use the QGridLayout(). Instead, I built a wrapper around the QVBoxLayout to behave as if it was a GridLayout, with an extra function to insert new rows:
class CustomGridLayout(QVBoxLayout):
def __init__(self):
super(CustomGridLayout, self).__init__()
self.setAlignment(Qt.AlignTop) # !!!
self.setSpacing(20)
def addWidget(self, widget, row, col):
# 1. How many horizontal layouts (rows) are present?
horLaysNr = self.count()
# 2. Add rows if necessary
if row < horLaysNr:
pass
else:
while row >= horLaysNr:
lyt = QHBoxLayout()
lyt.setAlignment(Qt.AlignLeft)
self.addLayout(lyt)
horLaysNr = self.count()
###
###
# 3. Insert the widget at specified column
self.itemAt(row).insertWidget(col, widget)
''''''
def insertRow(self, row):
lyt = QHBoxLayout()
lyt.setAlignment(Qt.AlignLeft)
self.insertLayout(row, lyt)
''''''
def deleteRow(self, row):
for j in reversed(range(self.itemAt(row).count())):
self.itemAt(row).itemAt(j).widget().setParent(None)
###
self.itemAt(row).setParent(None)
def clear(self):
for i in reversed(range(self.count())):
for j in reversed(range(self.itemAt(i).count())):
self.itemAt(i).itemAt(j).widget().setParent(None)
###
###
for i in reversed(range(self.count())):
self.itemAt(i).setParent(None)
###
''''''
i use python 2.7 + qt4.8
how to dynamically change the number of widgets in the window? I need to remove all the widgets and create new ones in the right quantity. testarovaniya made for a simple script:
import sys
from PyQt4 import QtCore, QtGui, uic
class MainForm(QtGui.QDialog):
def __init__(self):
super(MainForm, self).__init__()
uic.loadUi("testQTwindow.ui", self)
self.connect(self.addBtn, QtCore.SIGNAL("clicked()"), self.addBtnClick)
self.connect(self.delBtn, QtCore.SIGNAL("clicked()"), self.delBtnClick)
def addBtnClick(self):
self.tempBtn = QtGui.QPushButton('button')
self.gridLayout_2.addWidget(self.tempBtn)
def delBtnClick(self):
while True:
item = self.gridLayout_2.takeAt(0)
if not item:
break
self.gridLayout_2.removeWidget(item.widget())
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
sys.exit(app.exec_())
and load this UI: https://yadi.sk/d/jBOmSubYhqbjm
I have two buttons. One for adding buttons to QScrollArea with gridLayout. And the second to remove all the widgets in the QScrollArea. Adding works. I can see how there are new buttons. But when you press the cleaning button does not disappear, and new ones continue to appear over the old ones. The old button can also be pressed, which suggests that they work, and not just the ghosts that are cleaned redrawing window.
I try repaint() and update() functions - but it has no effect...
This is a simple example, but even he is not working. And I do not need to add a button in the future, and whole blocks with a bunch of elements.
How to add and remove widgets dynamically?
This part of the loop should be enough:
while True:
item = self.gridLayout_2.takeAt(0)
I suspect you are attempting to delete widgets you already removed, and so prematurely ending your loop. There may have been an error message written somewhere.
I have a grid of items in PyQt, and when the user modifies the window size I need to increase/decrease the number of columns accordingly. The number of rows are handled by a scrollarea, so I don't need to worry about changes in the y direction (if that matters).
Inside my implementation of QMainWindow, I know it's possible to override the resizeEvent() function, which will be triggered for any and all window adjustments. However, using that to rebuild the grid everytime is horribly inefficient. Just to test the function to see how it worked, I had resizeEvent merely print a string, and that caused my window adjustments to be slightly laggy and visually imperfect (jittery rather than smooth). I'll probably run a simple division operation on the window size to see if it has gotten larger or smaller enough to change the number of columns, but even that, when run a hundred times per adjustment, might cause lag issues. Rebuilding the entire grid might even take a second to do, so it would be preferable not to need to do it as the user is manipulating the window.
Is there a more efficient way to do it, or is resizeEvent my only option? Ideally, I'd like an event that triggered only once the user finished adjusting the window and not an event that triggers for practically every pixel movement as they happen (which can be hundreds or thousands of times per adjustment in the span of 1 second).
I'm using PyQt5, but if you're more familiar with PyQt4, I can figure out your PyQt4 solution in the context of PyQt5. Same for a C++ Qt4/5 solution.
It looks like the only real problem is detecting when resizing has completed. So long as this is carefully controlled, the actual laying out can be done in any way you like.
Below is a solution that uses a timer to control when a resize-completed signal is emitted. It doesn't appear to be laggy, but I haven't tested it with any complex layouts (should be okay, though).
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
resizeCompleted = QtCore.pyqtSignal()
def __init__(self):
QtGui.QWidget.__init__(self)
self._resize_timer = None
self.resizeCompleted.connect(self.handleResizeCompleted)
def updateResizeTimer(self, interval=None):
if self._resize_timer is not None:
self.killTimer(self._resize_timer)
if interval is not None:
self._resize_timer = self.startTimer(interval)
else:
self._resize_timer = None
def resizeEvent(self, event):
self.updateResizeTimer(300)
def timerEvent(self, event):
if event.timerId() == self._resize_timer:
self.updateResizeTimer()
self.resizeCompleted.emit()
def handleResizeCompleted(self):
print('resize complete')
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 300, 300)
window.show()
sys.exit(app.exec_())
I think you need a FlowLayoutfot this purpose, which automatically adjusts the number of columns on resizing the widget containing it. Here is the documentation for FlowLayout and here is the PyQt version of the same layout.
I'm trying to remove a Qt widget from a layout in a PySide application.
Here is a minimal example. It is a widget with 5 buttons in it, and the middle one is supposed to remove itself when clicked:
import sys
from PySide import QtGui
app = QtGui.QApplication(sys.argv)
widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
buttons = [QtGui.QPushButton(str(x)) for x in xrange(5)]
def deleteButton():
b = layout.takeAt(2)
buttons.pop(2)
del b
buttons[2].clicked.connect(deleteButton)
map(layout.addWidget, buttons)
widget.setLayout(layout)
widget.show()
app.exec_()
What actually happens is this:
The button is unclickable and clearly isn't taken into consideration for the layout computations, but its image stays in place.
According to the Qt documentation, the correct way of deleting all objects from a layout is:
while ((child = layout->takeAt(0)) != 0) {
delete child;
}
Here I just want to delete the third button, so I just call takeAt(2), and then del b to call the destructor on that item. The button object is also .pop'd from the buttons list to make sure there is no leftover reference to the object. How does my code differ from the one in the Qt docs that would cause such a behavior?
Super simple fix:
def deleteButton():
b = layout.takeAt(2)
buttons.pop(2)
b.widget().deleteLater()
You first have to make sure you are addressing the actual button and not the QWidgetItem that is returned from the layout, and then call deleteLater() which will tell Qt to destroy the widget after this slot ends and control returns to the event loop.
Another example illustrates why the problem is occurring. Even though you take the layout item, the underlying widget is still parented to the original layouts widget.
def deleteButton():
b = layout.takeAt(2)
buttons.pop(2)
w = b.widget()
w.setParent(None)
This is not the preferred way, as it still leaves the cleanup of the object ambiguous. But it shows that clearing the parent allows it to leave the visual display. Use deleteLater() though. It properly cleans everything up.
The answer that 'jdi' provided is valid, although If anyone is interested, I tried implementing what is suggested in the Qt Documentation with the loop of every child Widget, and I got the following code working in Python PySide6:
def delete():
while ((child := layout.takeAt(0)) != None):
child.widget().deleteLater()
im just a beginner in PyQT.
and im not sure if my thread title is the correct thing to put for my problem.
im having a problem creating a popmenu on a Qpushbutton.
based on the doc of QT docs
i need to make a QPushButton.setMenu (self, QMenu menu)
but i really dont know where to start.. i cant find a sample on how to use this.
please help me making one.
The basic idea is that you first have to create a QMenu, then use the setMenu method to attach it to your push button. If you look at the QMenu documentation, you'll see that there is a method called addAction that will add menu items to your newly created QMenu. addAction is overloaded, so there are a lot of different ways to call it. You can use icons in your menu, specify keyboard shortcuts and other things. To keep things simple though, let's just add a menu item and give it a method to call if that item is selected.
from PyQt4 import QtGui, QtCore
import sys
class Main(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
pushbutton = QtGui.QPushButton('Popup Button')
menu = QtGui.QMenu()
menu.addAction('This is Action 1', self.Action1)
menu.addAction('This is Action 2', self.Action2)
pushbutton.setMenu(menu)
self.setCentralWidget(pushbutton)
def Action1(self):
print 'You selected Action 1'
def Action2(self):
print 'You selected Action 2'
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Main()
main.show()
app.exec_()
Here we've created a push button (creatively named pushbutton). We then create a menu (again creatively named menu) using QtGui.QMenu(). The actions are created by calling addAction and giving it a string that will be used as the menu item text and a method (self.Action1 or self.Action2) that will be called if that menu item is selected. Then we call the setMenu method of pushbutton to assign our menu to it. When you run it and select an item, you should see text printed corresponding to the selected item.
That's the basic idea. You can look through the QMenu docs to get a better idea of the functionality of QMenu.