Passing a python list of objects to qml - python

I am trying to pass a list of objects from python to qml. on the qml side, I will interpret this information and using repeater and listmodel element, display these information in a table like manner.
if i simply pass an object or a list of integers, i could read the information on the qml side. but otherwise when trying to pass a list of objects. how can i read a list of objects on the qml side? do i have to use different properties?
below is what i have so far:
class File(QObject):
def __init__(self, fileName, commentsStatus, diagnosisStatus, parent=None):
QObject.__init__(self, parent)
self.fileName = fileName
self.commentsStatus = commentsStatus
self.diagnosisStatus = diagnosisStatus
class DisplayComponent(QObject):
def __init__(self, parent = None):
QObject.__init__(self, parent)
self.list = [File("file 1", True, False), File("file 2", False, True)]
#pyqtProperty(QQmlListProperty)
def getDicomFilesList(self):
return QQmlListProperty(File, self, self.list)
exposing to the qml side the following way:
context.setContextProperty("dicomFiles", displayComponent)
and this is how i am reading the list on the qml side:
HanaContainer {
Text {
id: display
text: "no signal detected yet"
}
Component.onCompleted: {
console.log(dicomFiles.getDicomFilesList[1]) // prints File(0x7f8a6d454c70)
console.log(dicomFiles.getDicomFilesList[1].fileName) // prints undefined
}
}
ps: am completely new to Qml and Qt5. if i am making any fundamental errors in my concepts, please do let me know

For an attribute to be visible in qml this must be a property, for this you should use pyqtProperty as shown below:
class File(QObject):
def __init__(self, fileName, commentsStatus, diagnosisStatus, parent=None):
QObject.__init__(self, parent)
self._fileName = fileName
self._commentsStatus = commentsStatus
self._diagnosisStatus = diagnosisStatus
#pyqtProperty(str)
def fileName(self):
return self._fileName
#pyqtProperty(bool)
def commentsStatus(self):
return self._commentsStatus
#pyqtProperty(bool)
def diagnosisStatus(self):
return self._diagnosisStatus
The above will make the attribute only readable, if we want to be editable implementetar setters, eg:
#fileName.setter
def fileName(self, value):
self._fileName = value

Related

Cant access custom widget instance when iterating through layout

I have a layout that I add a widget, class called ColList. When I run widget.children() I get an expected result, a list of widget. however when I try to run a method of ColList or access a variable pyqt crashes. For now I am trying to run a my sanity check method each ColList instance
I am using two .ui files. if you want me to post them I can. this is the (reduced) code I am trying to get to work
class ColList(QWidget):
def __init__(self, col, parent=None):
super().__init__(parent)
# Load the GUI
uic.loadUi("Listwidget.ui", self)
self.title = col
self.groupBox.setTitle(col)
print(self.parent())
def SC(self):
print("hi")
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
uic.loadUi("MainWindow.ui", self)
self.columns = ['a', 'b']
self.loadTags
def loadTags(self):
for i in self.columns:
# TagsH is the name of a Horizontal layout that is itself a child of a QScrollarea named scrollFilters
self.TagsH.insertWidget(self.TagsH.count() - 1, ColList(f"{i}", parent=self))
def applyFilters(self):
# the print works it outputs: [<PyQt6.QtWidgets.QWidget object at 0x00000221161E2830>, <PyQt6.QtWidgets.QWidget object at 0x00000221161E28C0>]
print(self.scrollFilters.children()[1:])
# this print outputs an empty list
print(self.TagsH.children())
for i in self.scrollFilters.children()[1:]:
# this part seems to crash the program
i.widget().SC()

QCheckBox to check/uncheck all QCheckboxes from another class in PyQt5

I have two QWidget classes: Widget1 and Widget2, and I would like to have a QCheckBox in the first class (Widget1) that can check/uncheck all the QcheckBoxes generated by the second class (Widget2). Is there a way to do this?
Many thanks in advance for your help.
class Widget1(QWidget):
def __init__(self, ids):
super().__init__()
self.ids = ids # ids is a list of list [[1, 1], [2, 2], [3, 3], ..., [n, n] generated elsewhere in the code
self.initUI()
def initUI(self):
self.widget1_layout = QVBoxLayout()
self.setLayout(self.Widget1_Layout)
self.master_checkbox = QCheckBox("Select all")
self.master_checkbox.stateChanged.connect(self.selectAll)
self.widget1_layout.addWidget(self.master_checkbox)
for i, id in enumerate(self.ids):
self.singleID_checkbox = Widget2(self, i, id)
self.widget1_layout.addWidget(self.singleID_checkbox)
def selectAll(self):
if self.master_checkbox.isChecked():
function_that_check_all_Widget2_checkboxes()
else:
function_that_UNcheck_all_Widget2_checkboxes()
class Widget2(QWidget):
def __init__(self, parent, i, id, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.i = i
self.id = id
self.initUI()
def initUI(self):
self.singleIDcheckbox_layout = QGridLayout(self)
self.singleIDcheckbox = QCheckBox(str(self.id))
self.singleIDcheckbox_layout.addWidget(self.singleIDcheckbox, self.i, 0)
The two functions
function_that_check_all_Widget2_checkboxes()
and
function_that_UNcheck_all_Widget2_checkboxes()
do not exist.
They are are here as examples to better present my problem, as I guess that this is where I should put some code to do what I'd like to achieve.
The simplest solution is to add each widget to a list whenever you're creating them, then check/uncheck them according to the "master" state.
Note that:
when creating multiple objects there's no use in setting them as instance members: the purpose of an instance attribute is to have a persistent reference to an object, if you continously overwrite that reference you lose that benefit;
most signals provide arguments, especially those relating "changes"; calling isChecked() is unnecessary, as stateChanged already returns the current state, you just need to add the argument to the function;
unless you're requiring a tristate checkbox, the correct signal is toggled (which returns a bool state), not the stateChanged (which returns a Qt.CheckState flag that has 2 for the checked state);
class Widget1(QWidget):
# ...
def initUI(self):
self.widget1_layout = QVBoxLayout(self)
self.master_checkbox = QCheckBox("Select all")
self.master_checkbox.toggled.connect(self.selectAll)
self.widget1_layout.addWidget(self.master_checkbox)
self.checkboxes = []
for i, id in enumerate(self.ids):
singleID_checkbox = Widget2(self, i, id)
self.widget1_layout.addWidget(singleID_checkbox)
self.checkboxes.append(singleID_checkbox)
def selectAll(self, state):
for check in self.checkboxes:
check.setChecked(state)
class Widget2(QWidget):
# ...
def setChecked(self, state):
self.singleIDcheckbox.setChecked(state)
Note that, since you're using a specialized class, you could also use findChildren, which returns all child objects that are instances of the specified class:
def selectAll(self, state):
for check in self.findChildren(Widget2):
check.setChecked(state)
Use the above with care, though, as by default it looks recursively through the whole object tree, so using a list is still a better solution.

QTreeView not displaying items

I've created a simple QTreeView widget and I'm subclassing QStandardItemModel but for some reason none of my rows appear to display in the view or print when trying to access it's Data. This seems like a very simple thing. I must be overlooking something in my Model that is making it not work at all. The columns display properly though. I put print statements in the def data(...) but they never trigger, why?
import os, sys, datetime
from Qt import QtCore, QtGui, QtWidgets
class ValidationItem(object):
def __init__(self, message, **kwargs):
super(ValidationItem, self).__init__()
self.status = kwargs.get('status', 'info')
self.message = message
class ValidationItemModel(QtGui.QStandardItemModel):
# Constructor
def __init__(self):
super(ValidationItemModel, self).__init__()
self._items = []
self.setHorizontalHeaderLabels(['Status', 'Message'])
# Overrides
def clear(self):
self.beginResetModel()
self._items = []
self.endResetModel()
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return len(self._items)
def index(self, row, column, parent=QtCore.QModelIndex()):
return self.createIndex(row, column, parent)
def data(self, index, role=QtCore.Qt.DisplayRole):
print 'GETTING DATA'
if not index.isValid():
return None
row = index.row()
col = index.column()
print row, col
item = self.itemByIndex(index.row())
if not item:
return None
print item
if role == QtCore.Qt.UserRole:
return item
return None
# Methods
def itemByIndex(self, index):
print index
if (index < 0 or index >= len(self._items)):
return None
return self._items[index]
def appendItem(self, item):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self._items.append(item)
self.endInsertRows()
print self.rowCount()
class SimpleDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(SimpleDialog, self).__init__(parent=parent)
self.resize(500,300)
self.itemModel = ValidationItemModel()
self.itemModel.appendItem(ValidationItem('Hello world! 1', status='warning'))
self.itemModel.appendItem(ValidationItem('Hello world! 2', status='info'))
self.itemModel.appendItem(ValidationItem('Hello world! 3', status='error'))
self.itemModel.appendItem(ValidationItem('Hello world! 4', status='valid'))
self.proxyModel = QtCore.QSortFilterProxyModel()
self.proxyModel.setSourceModel(self.itemModel)
self.treeView = QtWidgets.QTreeView()
self.treeView.setModel(self.proxyModel)
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.treeView)
self.setLayout(self.layout)
def main():
app = QtWidgets.QApplication(sys.argv)
ex = SimpleDialog()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
UPDATED
Below is my updated code which I got working. The reason for posting this code, if because a user suggested below that i should be customizable QStandardItem instead of the model. I'm hoping he can demonstrate this with my code so i can better understand on how to do this. The main thing to notice is users can double click an item and it will execute the run function of each item accessing it through the UserRole.
import os, sys, datetime
from Qt import QtCore, QtGui, QtWidgets
class ValidationItem(object):
def __init__(self, message, **kwargs):
super(ValidationItem, self).__init__()
self.status = kwargs.get('status', 'info')
self.message = message
def run(self):
print 'STUBBED IN METHOD', self.status, self.message
class ValidationItemURL(ValidationItem):
def __init__(self, message, **kwargs):
super(ValidationItem, self).__init__()
self.status = kwargs.get('status', 'info')
self.message = message
def run(self):
os.startfile('https://stackoverflow.com/')
class ValidationItemModel(QtCore.QAbstractTableModel):
VALIDATION_STATUSES = {
'error': QtGui.QColor(220,70,55),
'warning': QtGui.QColor(240,180,10),
'valid': QtGui.QColor(10,170,70)
}
# Constructor
def __init__(self):
super(ValidationItemModel, self).__init__()
self._items = []
self._headers = ['Status', 'Message']
# Overrides
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._headers[section]
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def clear(self):
self.beginResetModel()
self._items = []
self.endResetModel()
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return len(self._items)
def columnCount(self, parent):
return len(self._headers)
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
item = self.itemByIndex(index.row())
if not item:
return None
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
if column == 0:
return item.status.title()
elif column == 1:
return item.message
elif role == QtCore.Qt.ToolTipRole:
if column == 0:
return item.status.title()
elif column == 1:
return item.message
elif role == QtCore.Qt.ForegroundRole:
if column == 0:
if item.status in self.VALIDATION_STATUSES:
return self.VALIDATION_STATUSES[item.status]
if role == QtCore.Qt.UserRole:
return item
return None
# Methods
def itemByIndex(self, index):
if (index < 0 or index >= len(self._items)):
return None
return self._items[index]
def appendItem(self, item):
if isinstance(item, ValidationItem):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self._items.append(item)
self.endInsertRows()
class SimpleDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(SimpleDialog, self).__init__(parent=parent)
self.resize(500,300)
self.itemModel = ValidationItemModel()
self.itemModel.appendItem(ValidationItem('Hello world! 1', status='warning'))
self.itemModel.appendItem(ValidationItem('Hello world! 2', status='info'))
self.itemModel.appendItem(ValidationItem('Hello world! 3', status='error'))
self.itemModel.appendItem(ValidationItemURL('stackoverflow (double-click to visit)', status='valid'))
self.proxyModel = QtCore.QSortFilterProxyModel()
self.proxyModel.setSourceModel(self.itemModel)
self.treeView = QtWidgets.QTreeView()
self.treeView.setModel(self.proxyModel)
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.treeView)
self.setLayout(self.layout)
# Signals
self.treeView.doubleClicked.connect(self.slotDoubleClickedItem)
# Slots
def slotDoubleClickedItem(self, index):
if index.isValid():
item = index.data(role=QtCore.Qt.UserRole)
if item:
item.run()
def main():
app = QtWidgets.QApplication(sys.argv)
ex = SimpleDialog()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Answer
The reason your UPDATED code works is because you have implemented headerData(). Per the Model Subclassing Reference docs (added emphasis is mine):
To provide read-only access to data provided by a model, the following functions must be implemented in the model’s subclass
flags()
data()
headerData()
rowCount()
These four functions must be implemented in all types of model, including list models (QAbstractListModel subclasses) and table models (QAbstractTableModel) subclasses.
This is the case even if you do not want headers in your view. To hide them, call setHeaderHidden(True) on your QTreeView instance.
For doing more than read-only access, the docs go on to state which additional methods must be implemented.
NOTE: My answer references PySide6 , including its documentation and source code. I do not know which python binding or version of Qt you are using, but my answer should be generally correct.
Aside
#eyllanesc alluded to the fact that your model must notify the view that it has been updated. The best way to know how and where to do this is by reading the Qt C++ source code, but there are also 2 good examples provided by PySide:
Simple (Ready-Only) Tree Model Example
Editable Tree Model Example
QTreeView is a subclass of QAbstractItemView. If you look at the source code for QTreeView.setModel(), you will see it calls QAbstractItemView.setModel(). Both connect up several sets of signals, but the relevant ones are in QAbstractItemView.
QAbstractItemView connects the model signals
dataChanged()
rowsInserted()
rowsAboutToBeRemoved()
modelReset()
to view slots of the same name (except for modelReset(), the view slot for which is simply reset()).
Each of these slots calls viewport.update(), which draws the view.
If you look at the source code for QStandardItemModel, you will see
insertRows() calls rowsInserted
removeRows() calls rowsAboutToBeRemoved
setChild() (and some others) call dataChanged
NOTE: Any functions used in qstandarditemmodel_p.h, qstandarditemmodel.h, or qstandarditemmodel.cpp , but not defined there, are defined in QAbstractItemModel, (either in one of its header files (as an inline function) or its .cpp file), since QStandardItemModel inherits from QAbstractItemModel.
NOTE: C++ allows overloaded functions, whereas Python does not. Instead, in Python, you can specify optional keyword arguments (i.e. those given a default value) to mimic C++ overloading. C++ does not (natively) support keyword arguments.
The consequence of this is, when reading the Qt source, you need to be sure to look at all overloaded methods and choose whether to combine their logic into one method using optional keyword arguments, or use only one of the overloaded methods.
NOTE: Qt practices the Pointer to an Implementation, or pIMPL idiom. This is a kind of language-specific pattern/technique that removes implementation details of a class from its object representation. You can (loosely) think of it as the C++ version of Python modules. In C++, whenever a translation unit ("file") is modified, it and every other translation unit that uses it must be recompiled. This is an expensive and tedious process, esp. for large code bases, and can cause existing user code linked against it to fail. Using a pointer adds a level of indirection that prevents this from happening. (In Python, every .py file is a module and is compiled into byte code (a .pyc file) when executed (the bytecode is run by the interpreter). Changing that file causes only that file to need to be recompiled; the rest of your code will still work with it.) In fact C++20 finally introduced modules into the language.
Qt does this by using what it calls D-Pointers and Q-Pointers. The source for these implementation details are placed in private headers suffixed with _p.h. The names of these classes are suffixed Private as well.
Wherever you see d->..., it is using a private class method. You can think of "d" as standing for detail in "implementation detail."
WARNING: Every method of qstandarditemmodel.cpp calls d->itemFromIndex(), which is inlined in qstandarditemmodel_p.h, but qstandarditemmodel.cpp also defines its own itemFromIndex() to be used by model instances. If you do not use the inlined method, your standard item model will not work.
Note also that the inlined method returns root.data(), which is not the same as the model data method. root is a QScopedPointer of QStandardItems. The QScopedPointer.data() method simply returns the value of the pointer it references, which is the root item. When you see this, simply replace it with your python self.root_item.
When translating to Python, I recommend creating a pseudo-private (i.e. name-mangled) method __itemFromIndex() to be used internally in your method implementations, and "public" one itemFromIndex() to be used externally by instances of your model.
Since dataChanged appears to be the only signal exposed in PySide (rowsInserted and rowsAboutToBeRemoved are methods on the private QStandardItemPrivate class in C++), the most straightforward solution is to emit dataChanged at the end of your clear() and appendRow() methods (immediately before endResetModel() and endInsertRows(), resp.).
In general,
beginInsertRows() emits the rowsAboutToBeInserted() signal (see the docs and the source),
endInsertRows() emits the rowsInserted() signal (see the source)
and similarly for the corresponding Columns() methods.
These should typically be sufficient to update the view, but it's also a good idea to explicitly emit dataChanged() when you do so. In fact, it should be emitted if setData() is successful, but the subclassing docs also state:
The dataChanged() and headerDataChanged() signals must be emitted explicitly when reimplementing the setData() and setHeaderData() functions, respectively.

Can't write to QTextBrowser when calling function from another file

I have a python plugin where the main.py file displays a QTextBrowser and writes some text. This works fine.
I wrote a second file anotherFile.py which identifies the same QTextBrowser but cannot write any text to it. Perhaps it needs to take ownership, I'm not sure?
Here is the code used:
# main.py #
from Example_dockwidget import ExampleDockWidget
from anotherFile import anotherClass
class Example:
def __init__(self, iface):
self.iface = iface
def function(self):
self.dockwidget = ExampleDockWidget()
self.dockwidget.show()
textBrowser = self.dockwidget.textBrowser
#textBrowser.setText('This works!')
a = anotherClass(self)
a.anotherFunction()
# anotherFile.py #
from Example_dockwidget import ExampleDockWidget
class anotherClass:
def __init__(self, iface):
self.iface = iface
self.dockwidget = ExampleDockWidget()
def anotherFunction(self):
textBrowser = self.dockwidget.textBrowser
textBrowser.setText('This does not work!')
print 'Why?'
# Example_dockwidget.py #
FORM_CLASS, _ = uic.loadUiType(os.path.join(
os.path.dirname(__file__), 'Example_dockwidget_base.ui'))
class ExampleDockWidget(QtGui.QDockWidget, FORM_CLASS):
def __init__(self, parent=None):
super(ExampleDockWidget, self).__init__(parent)
self.setupUi(self)
Your two classes both create their own ExampleDockWidget. Only one of them is shown (has its show method called) but there are two.
So it's not surprising that text sent to one does not appear on the other. You need to arrange for your anotherClass object to obtain a reference to the other ExampleDockWidget so it can share the same one.
As strubbly mentioned, I need to reference the same ExampleDockWidget instead of creating separate versions. In the anotherFile.py file, I added an extra parameter to take in ExampleDockWidget:
class anotherClass:
def __init__(self, iface, dockwidget):
self.iface = iface
self.dockwidget = dockwidget
def anotherFunction(self):
textBrowser = self.dockwidget.textBrowser
textBrowser.setText('This does not work!')
print 'Why?'
And then inserted the reference in the main.py file:
def function(self):
self.dockwidget = ExampleDockWidget()
self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dockwidget)
self.dockwidget.show()
textBrowser = self.dockwidget.textBrowser
#textBrowser.setText('This works!')
a = anotherClass(self.iface, self.dockwidget)
a.anotherFunction()

Creating subclass for wx.TextCtrl

I'm creating a subclass for the wx.TextCtrl in wxpython.
I want this class to add extra data to the wx.TextCtrl widgets similar as to the way extra data can be added to a ComboBox or ListBox.
Here's my code:
import wx
class ExtraDataForTxtCtrl(wx.TextCtrl):
def __init(self, ExtraTextData):
self.ExtraTextData=ExtraTextData
def getExtraTCData(self):
return self.ExtraTextData
def setExtraTCData(self, ExtraTextData):
self.ExtraTextData=ExtraTextData
My problem is that I'm new to python and have no idea how to implement this and if it is correct or not.
import wx
class ExtraDataForTxtCtrl(wx.TextCtrl):
def __init__(self,*args,**kwargs):
self.ExtraTextData=kwargs.pop("ExtraTextData")
wx.TextCtrl.__init__(self,*args,**kwargs)
def getExtraTCData(self):
return self.ExtraTextData
def setExtraTCData(self, ExtraTextData):
self.ExtraTextData=ExtraTextData
possibly a better solution would be to use set/getattr
class DataTxtCtrl(wx.TextCtrl):
def __init__(self,*args,**kwargs):
self.datadict = {}
self.ExtraTextData=kwargs.pop("ExtraTextData")
wx.TextCtrl.__init__(self,*args,**kwargs)
def __getattr__(self,attr):
return self.datadict[attr]
def __setattr__(self,attr,val):
self.datadict[attr]=val
then you can set many variables and use it like normal
a = wx.App(redirect=False)
f = wx.Dialog(None,-1,"Example")
te = DataTxtCtrl(f,-1,"some_default")
te.somevar = "hello"
te.someother = "world"
print te.somevar+" "+te.someothervar
f.ShowModal()
Instead of creating a subclass I just decided to create my own class which links an extra string value to wx.textCtrl widgets.
Thanks to all who contributed! :)
Heres my code:
class TextDataHolder:
def __init__(self, wxTextControl, data):
self.wxTextControl=wxTextControl
self.data=data
def setDataTxt(self,data):
self.wxTextControl=wxTextControl
self.data=data
def getDataTxt(self):
return self.data
Heres how I implemented it:
import wx, TextDataHolder
exampleCtrl=wx.TextCtrl(self, -1, "Hello")
exampleData=TextDataHolder.TextDataHolder(exampleCtrl,"Sup?")
print exampleData.getDataTxt() #prints 'Sup?'

Categories