How to use update and paintEvent in a QWidget in pyside? - python

Being new to pyside I am still having trouble understanding some GUI concepts, even aware of decent documentation.
I have a widget, dervied from QWidget, which I want to draw inside a function paintEvent, which is the function called to paint the widget (as far as I understood the documentation). Also, the method update should be used to update the widget, which calls the method paintEvent (as far as I understood the documentation).
In the following code skeleton I give a short overview of my code which should do two things:
- when initialized the widget should be drawn
- if required, the widget should be redrawn by a call to update inside the derived class
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.update() # update the widget for the first time
def paintEvent(self, x):
self.setGeometry(300, 200, 970, 450)
self.setWindowTitle("My Window")
...
self.table_model = MyTableModel(...)
self.view = QTableView()
...
self.setLayout(layout)
def do_something(self):
....
self.update()
When running the code the widget is drawn as expected. But once the call to update is made inside do_something nothing happens, the widget is NOT redrawn! I also tried to use the method repaint instead, but the widget is still not redrawn.
How to fix the code to ensure the widget is redrawn from scratch by calling paintEvent?

Related

Qt - update view size on delegate sizeHint change

I have a QTreeView with a QStyledItemDelegate inside of it. When a certain action occurs to the delegate, its size is supposed to change. However I haven't figured out how to get the QTreeView's rows to resize in response to the delegate's editor size changing. I tried QTreeView.updateGeometry and QTreeView.repaint and a couple other things but it doesn't seem to work. Could someone point me in the right direction?
Here's a minimal reproduction (note: The code is hacky in a few places, it's just meant to be a demonstration of the problem, not a demonstration of good MVC).
Steps:
Run the code below
Press either "Add a label" button
Note that the height of the row in the QTreeView does not change no matter how many times either button is clicked.
from PySide2 import QtCore, QtWidgets
_VALUE = 100
class _Clicker(QtWidgets.QWidget):
clicked = QtCore.Signal()
def __init__(self, parent=None):
super(_Clicker, self).__init__(parent=parent)
self.setLayout(QtWidgets.QVBoxLayout())
self._button = QtWidgets.QPushButton("Add a label")
self.layout().addWidget(self._button)
self._button.clicked.connect(self._add_label)
self._button.clicked.connect(self.clicked.emit)
def _add_label(self):
global _VALUE
_VALUE += 10
self.layout().addWidget(QtWidgets.QLabel("Add a label"))
self.updateGeometry() # Note: I didn't expect this to work but added it regardless
class _Delegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
widget = _Clicker(parent=parent)
viewer = self.parent()
widget.clicked.connect(viewer.updateGeometries) # Note: I expected this to work
return widget
def paint(self, painter, option, index):
super(_Delegate, self).paint(painter, option, index)
viewer = self.parent()
if not viewer.isPersistentEditorOpen(index):
viewer.openPersistentEditor(index)
def setEditorData(self, editor, index):
pass
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def sizeHint(self, option, index):
hint = index.data(QtCore.Qt.SizeHintRole)
if hint:
return hint
return super(_Delegate, self).sizeHint(option, index)
class _Model(QtCore.QAbstractItemModel):
def __init__(self, parent=None):
super(_Model, self).__init__(parent=parent)
self._labels = ["foo", "bar"]
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if role == QtCore.Qt.SizeHintRole:
return QtCore.QSize(200, _VALUE)
if role != QtCore.Qt.DisplayRole:
return None
return self._labels[index.row()]
def index(self, row, column, parent=QtCore.QModelIndex()):
child = self._labels[row]
return self.createIndex(row, column, child)
def parent(self, index):
return QtCore.QModelIndex()
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return len(self._labels)
application = QtWidgets.QApplication([])
view = QtWidgets.QTreeView()
view.setModel(_Model())
view.setItemDelegate(_Delegate(parent=view))
view.show()
application.exec_()
How do I get a single row in a QTreeView, which has a persistent editor applied already to it, to tell Qt to resize in response to some change in the editor?
Note: One possible solution would be to close the persistent editor and re-open it to force Qt to redraw the editor widget. This would be generally very slow and not work in my specific situation. Keeping the same persistent editor is important.
As the documentation about updateGeometries() explains, it:
Updates the geometry of the child widgets of the view.
This is used to update the widgets (editors, scroll bars, headers, etc) based on the current view state. It doesn't consider the editor size hints, so that call or the attempt to update the size hint is useless (and, it should go without saying, using global for this is wrong).
In order to properly notify the view that a specific index has updated its size hint, you must use the delegate's sizeHintChanged signal, which should also be emitted when the editor is created in order to ensure that the view makes enough room for it; note that this is normally not required for standard editors (as, being they temporary, they should not try to change the layout of the view), but for persistent editors that are potentially big, it may be necessary.
Other notes:
calling updateGeometry() on the widget is pointless in this case, as adding a widget to a layout automatically results in a LayoutRequest event (which is what updateGeometry() does, among other things);
as explained in createEditor(), "the view's background will shine through unless the editor paints its own background (e.g., with setAutoFillBackground())";
the SizeHintRole of the model should always return a size important for the model (if any), not based on the editor; it's the delegate responsibility to do that, and the model should never be influenced by any of its views;
opening a persistent editor in a paint event is wrong; only drawing related aspects should ever happen in a paint function, most importantly because they are called very often (even hundreds of times per second for item views) so they should be as fast as possible, but also because doing anything that might affect a change in geometry will cause (at least) a recursive call;
signals can be "chained" without using emit: self._button.clicked.connect(self.clicked) would have sufficed;
Considering all the above, there are two possibilities. The problem is that there is no direct correlation between the editor widget and the index it's referred to, so we need to find a way to emit sizeHintChanged with its correct index when the editor is updated.
This can only be done by creating a reference of the index for the editor, but it's important that we use a QPersistentModelIndex for that, as the indexes might change while a persistent editor is opened (for example, when sorting or filtering), and the index provided in the arguments of delegate functions is not able to track these changes.
Emit a custom signal
In this case, we only use a custom signal that is emitted whenever we know that the layout is changed, and we create a local function in createEditor that will eventually emit the sizeHintChanged signal by "reconstructing" the valid index:
class _Clicker(QtWidgets.QWidget):
sizeHintChanged = QtCore.Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.setAutoFillBackground(True)
layout = QtWidgets.QVBoxLayout(self)
self._button = QtWidgets.QPushButton("Add a label")
layout.addWidget(self._button)
self._button.clicked.connect(self._add_label)
def _add_label(self):
self.layout().addWidget(QtWidgets.QLabel("Add a label"))
self.sizeHintChanged.emit()
class _Delegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
widget = _Clicker(parent)
persistent = QtCore.QPersistentModelIndex(index)
def emitSizeHintChanged():
index = persistent.model().index(
persistent.row(), persistent.column(),
persistent.parent())
self.sizeHintChanged.emit(index)
widget.sizeHintChanged.connect(emitSizeHintChanged)
self.sizeHintChanged.emit(index)
return widget
# no other functions implemented here
Use the delegate's event filter
We can create a reference for the persistent index in the editor, and then emit the sizeHintChanged signal in the event filter of the delegate whenever a LayoutRequest event is received from the editor:
class _Clicker(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setAutoFillBackground(True)
layout = QtWidgets.QVBoxLayout(self)
self._button = QtWidgets.QPushButton("Add a label")
layout.addWidget(self._button)
self._button.clicked.connect(self._add_label)
def _add_label(self):
self.layout().addWidget(QtWidgets.QLabel("Add a label"))
class _Delegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
widget = _Clicker(parent)
widget.index = QtCore.QPersistentModelIndex(index)
return widget
def eventFilter(self, editor, event):
if event.type() == event.LayoutRequest:
persistent = editor.index
index = persistent.model().index(
persistent.row(), persistent.column(),
persistent.parent())
self.sizeHintChanged.emit(index)
return super().eventFilter(editor, event)
Finally, you should obviously remove the SizeHintRole return in data(), and in order to open all persistent editors you could do something like this:
def openEditors(view, parent=None):
model = view.model()
if parent is None:
parent = QtCore.QModelIndex()
for row in range(model.rowCount(parent)):
for column in range(model.columnCount(parent)):
index = model.index(row, column, parent)
view.openPersistentEditor(index)
if model.rowCount(index):
openEditors(view, index)
# ...
openEditors(view)
I had a similar problem when I was adding new widgets to a QFrame, because the dumb thing was not updating the value of its sizeHint( ) after adding each new widget. It seems that QWidgets (including QFrames) only update their sizeHint( ) when the children widgets are "visible". Somehow, in some occassions Qt sets new children to "not visible" when they are added, don't ask me why. You can see if a widget is visible by calling isVisible( ), and change its visibility status with setVisible(...). I solved my problem by telling to the QFrame that they new child widgets were intended to be visible, by calling setVisible( True ) with each of each child after adding them to the QFrame. Some Qt fundamentalists may say that this is a blasphemous hack that breaks the fabric of space time or something and that I should be burnt at the stake, but I don't care, it works, and it works very well in a quite complex GUI that I have built.

What affect does inheriting from Kivy Widget class have on the child?

First, a heads up - I am not too familiar with OOP concepts, so this may just be some form of python functionality that I am not aware of.
In Kivy we can modify the behaviour and appearance of widgets by creating classes child to the widgets whose functions we want to alter, for instance:
class MyWidget(Button):
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
self.size_hint = None
self.size = 200, 100
Then use MyWidget: or <MyWidget> to instantiate the widget in kv. This is good if I wish for my widgets to be parent to the root widget, but there are times when this is not wanted for instance when requiring a temporary Popup.
In which case I would just create a typical class which is instantiated when an event is triggered. Like so:
class Interface():
def __init__(self):
btn = Button()
btn.bind(on_press=self.some_callback)
self.popup = Popup(content=btn)
self.popup.open()
def some_callback(self, instance):
print('woo')
And this, to all appearances, looks fine, but the widget events (on_press here) don't trigger callback functions??? The bind() function will call the callback on the event, since I will be notified if the callback function definition has incorrect syntax, but for some reason the contents of the callback are not executed - only when Interface() inherits from the Widget() class (Interface(Widget): ...).
From the docs:
Widget interaction is built on top of events that occur. If a property changes, the widget can respond to the change in the ‘on_’ callback. If nothing changes, nothing will be done.
btn is the widget, it is instance of Button() which itself is a child of Widget, its method has no connection at all to what may be the parent class of Interface so why then is the callback only fully executed when Widget is the parent class of Interface? What am I missing?

In pyqt, how to get access the dockwidget on a QDockWidgetArea QMainWindow

If I've created a QMainWindow instance, and assign the central widget and dock widgets for it:
class MyWindow(QtGui.QMainWindow):
def __init__(self, MyCentralWidget, MyDockWidget):
super(MyWindow,self).__init__()
self.setCentralWidget(MyCentralWidget)
self.addDockWidget(Qt.LeftDockWidgetArea,MyDockWidget)
self.show()
Instance=MyWindow(A,B)
Then I think I can get access to the center widget A by Instance.centralWidget(), but how can I get access to the dockwidget in the LeftDockWidgetArea similarly? (for example, I want to modify the properties of the Widget in that dockwidget after instance is created)
try:
self.dockWidgetContents.your_widget...

PyQt Subclass from QTableWidgetItem and QWidget

Im making a QTableWidget in Pyqt and ran into a bit of an annoying hiccup.
I need to use widgets in my table for its functionality, so im using setCellWidget to add them to the table. However, widgets dont have the same methods available as QTableWidgetItem's do (especially regarding selection in the table).
Im wondering if its possible to do something subclassing both items, so i can have the methods of both, and how i woulda dd that to the table.
Something like:
class TableItem(QtGui.QTableWidgetItem, QtGui.QWidget):
def __init__(self, parent=None):
super(TableItem, self).__init__(parent)
self.check = QtGui.QCheckBox()
self.label = QtGui.QLabel('Some Text')
self.h_box = QtGui.QHBoxLayout()
self.h_box.addWidget(self.check)
self.h_box.addWidget(self.label)
and then somehow add that to my table as a TableWidgetItem so it displays widgets and also has selection methods available.
Any ideas here?
For reference:
setCellWidget: http://pyqt.sourceforge.net/Docs/PyQt4/qtablewidget.html#setCellWidget
QWidget: (easy to find, i cant post more than 2 links)
-Which doesnt have the nice methods for a table
QTableWidgetItem: http://pyqt.sourceforge.net/Docs/PyQt4/qtablewidgetitem.html#type
with isSelected and setSelected (Methods not avialble from a widget used in setCellWidget.
To return the widget in a cell you can use table.cellWidget(row, column) and then use your widgets methods on that. But beacuse setSelected and isSelected arent methods of a widget, you cant check for selection. I was hoping to subclass the two together to allow for both
--Basically I need to know how to get my class to 'return' the proper type when i call it to add to the table with setItem
I am not sure what you want to do but you could "inject" a method like:
class TableWidgetItem(QtGui.QTableWidgetItem):
def __init__(self, parent=None):
QtGui.QTableWidgetItem.__init__(self)
def doSomething(self):
print "doing something in TableWidgetItem"
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self)
tableWidgetItem = TableWidgetItem()
widget = Widget()
def widgetFunction(self):
tableWidgetItem.doSomething()
# as an instance method
settatr(widget, "widgetFunction", MethodType(widgetFunction, widget, type(widget)))
# or as a class method
settatr(widget, "widgetFunction", widgetFunction)
Then you can:
>>>widget.widgetFunction()
doing something in TableWidgetItem
(not tested)

Relaying signals to contained widgets

I'm new to PySide, and trying to figure out how to elegantly create what is effectively a signal fanout; i.e. a doodad that looks like a Slot to container classes and can thus be connect()ed to, and simply forwards that signal on to contained classes, preferably without adding too much overhead in either syntax or performance.
Let's take an entirely contrived example:
class TripleCheckBox(QWidget):
setCheckState = Signal(int)
def __init__(self, parent):
super().__init__(parent)
self.checks = [QCheckBox(x, self) for x in ['One', 'Two', 'Three']]
[self.setCheckState.connect(x) for x in self.checks]
class MainWindow(QWidget):
def __init__(self):
self.chk = QCheckBox('Alpha', self)
self.btn = QPushButton('Push', self)
self.tri = TripleCheckBox(self)
self.chk.stateChanged.connect(self.tri.setCheckState)
self.btn.clicked.connect(self.clearChecks)
def clearChecks(self):
self.tri.setCheckState(0)
So, this does most of what I'm looking for. The stateChanged signal from the Alpha checkbox hits the signal port on the TripleCheckBox, which then rebroadcasts to One, Two, and Three, and (I believe) does so entirely inside the Qt library without having to bounce back and forth between library code and Python.
But MainWindow.ClearChecks() doesn't work. For a real QCheckBox, setCheckState is a Slot, and therefore a function, and can be called with conventional function syntax. But the setCheckState on the TripleCheckBox is a Signal, and so has to be "called" as self.tri.setCheckState.emit(0)
This is syntactically ugly, but also has some ugly mantainability implications. If I have a QCheckBox, I can treat setCheckState as a function. If I have a TripleCheckBox, I have to treat it as a signal instead, even though all it's doing is wrapping up 3 QCheckBoxes.
Ideally, TripleCheckBox.setCheckState would be a thing like a Signal that simply has a __call__ method that calls/is emit. But you can't inherit from Signal.
I can think of several inelegant ways to do this, all of which involving lots of replication of code and adhocery. But this is some pretty basic level stuff, surely there's an elegant answer. Right?
The use of a custom signal in your example is redundant, and makes the code unnecessarily complicated.
Given its name and intended usage, TripleCheckBox.setCheckState, should be a slot rather than a signal. A signal should not, in itself, do anything: it is simply a notification that some event has happened (or is about to happen). A signal should have no direct side-effects, and any object broadcasting it should not care about what consequences (if any) it may have once it's been emitted.
For this reason, the line self.tri.setCheckState(0), makes no sense as a signal (although it does as a slot). On the other hand, something like self.tri.stateChanged.emit(0) would make sense (although not necessarily in that particular context).
Given the above points, here is one way to re-write your example:
from PySide import QtCore, QtGui
class CheckBoxSet(QtGui.QWidget):
def __init__(self, labels, parent=None):
super(CheckBoxSet, self).__init__(parent)
layout = QtGui.QVBoxLayout(self)
self.checkboxes = []
for label in labels:
checkbox = QtGui.QCheckBox(label, self)
layout.addWidget(checkbox)
self.checkboxes.append(checkbox)
def setCheckState(self, state=0):
state = QtCore.Qt.CheckState(state)
for checkbox in self.checkboxes:
checkbox.setCheckState(state)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QtGui.QVBoxLayout(self)
self.chk = QtGui.QCheckBox('Alpha', self)
self.btn = QtGui.QPushButton('Push', self)
self.tri = CheckBoxSet('One Two Three'.split(), self)
layout.addWidget(self.chk)
layout.addWidget(self.btn)
layout.addWidget(self.tri)
self.chk.stateChanged.connect(self.tri.setCheckState)
self.btn.clicked.connect(self.clearChecks)
def clearChecks(self):
self.tri.setCheckState(0)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 300, 200)
window.show()
sys.exit(app.exec_())

Categories

Resources