alphanumeric
alphanumeric

Reputation: 19329

How to share same model with QTableView and QComboBox

The list:

items = [['Pet', 'Dog'],['Pet', 'Cat'],['Bird','Eagle'],['Bird','Jay'],['Bird','Falcon']]

is used by model that is assigned to QTableView and QComboBox.

http://i.imgur.com/Us551Nh.png

I want Combobox to only display "Pet" and "Bird" while QTableView to display: "Dog", "Eagle" and "Jay". How to achieve this?

from PySide import QtGui, QtCore

class Model(QAbstractTableModel):
    def __init__(self, parent=None, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.items = [['Pet', 'Dog'],['Pet', 'Cat'],['Bird','Eagle'],['Bird','Jay'],['Bird','Falcon']]

    def rowCount(self, parent=QModelIndex()):
        return len(self.items)      
    def columnCount(self, parent=QModelIndex()):
        return 2
    def data(self, index, role):
        if not index.isValid(): return 
        row=index.row()
        column=index.column()
        return self.items[row][column]

class Proxy(QSortFilterProxyModel):
    def __init__(self):
        super(Proxy, self).__init__()

    def filterAcceptsRow(self, rowProc, parentProc):  
        modelIndex=self.sourceModel().index(rowProc, 0, parentProc)
        item=self.sourceModel().data(modelIndex, Qt.DisplayRole)
        return True

class MyWindow(QWidget):
    def __init__(self, *args):
        QWidget.__init__(self, *args)
        vLayout=QVBoxLayout(self)
        self.setLayout(vLayout)

        model=Model(self)  
        proxy=Proxy()
        proxy.setSourceModel(model)

        self.combo=QtGui.QComboBox()
        self.combo.activated.connect(self.comboActivated)
        vLayout.addWidget(self.combo)

        self.combo.setModel(proxy)

        self.ViewA=QTableView(self)
        self.ViewA.setModel(model)
        self.ViewA.clicked.connect(self.viewClicked)
        vLayout.addWidget(self.ViewA)

    def viewClicked(self, indexClicked):
        print 'indexClicked() row: %s  column: %s'%(indexClicked.row(), indexClicked.column() )
        proxy=indexClicked.model()

    def comboActivated(self, arg):
        print 'comboClicked() arg:', arg

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())

Upvotes: 0

Views: 1755

Answers (2)

strubbly
strubbly

Reputation: 3477

It is possible to do what you want. You will need to implement a separate QAbstractProxyModel for each view. The one for the combo box should filter and leave only the non-duplicated elements of the first column. The other should filter the second row down to only the rows relevant to the current state of the combo box view.

If you did that, in this example, you would end up with four model-related objects: your data (in Model.items), your Model, and the two proxies.

But I don't think this is a good approach at all. Both of these proxies are complex and completely change the row,column structure of the original model. Imagine the complexity that will be needed in the handling of the dataChanged signal in each proxy when data in the first column of the model changes.

In practice I think you would be much better off with a three object version: a proper (slightly elaborated) data class and then a pair of QAbstractItemModels sharing that data and each specialised to the View they support. Think of it like a pair of QFileSystemModels on the same file system.

Changes to the data will have to be signaled to the two QAbstractItemModels and you won't have the benefit of having that automatically implemented by the Qt framework. But, as I pointed out above, in practice that won't work easily for the proxies in any case.

So here's an example based on your code with as few changes as possible. Note that I had to fix quite a few things - your imports didn't work and your combo model was returning incorrect data for some roles.

from PySide import QtGui, QtCore
import sys

class Data(object):

    def __init__(self):
        self.items = [['Pet', 'Dog'],['Pet', 'Cat'],['Bird','Eagle'],['Bird','Jay'],['Bird','Falcon']]
        self.selectors = list({ k[0] for k in self.items})

    def currentItems(self,select_on):
        return [k[1] for k in self.items if k[0] == select_on]

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data, parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.data_obj = data
        self.currentSelection = None
    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.data_obj.currentItems(self.currentSelection))      
    def columnCount(self, parent=QtCore.QModelIndex()):
        return 1
    def data(self, index, role):
        if not index.isValid() or role != QtCore.Qt.DisplayRole: return 
        row=index.row()
        return self.data_obj.currentItems(self.currentSelection)[row]
    def setSelection(self,combo_row):
        self.currentSelection = self.data_obj.selectors[combo_row]
        self.layoutChanged.emit()

class ComboModel(QtCore.QAbstractListModel):
    def __init__(self, data, parent=None):
        QtCore.QAbstractListModel.__init__(self, parent)
        self.data_obj = data
    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.data_obj.selectors)      
    def data(self, index, role):
        if not index.isValid() or role != QtCore.Qt.DisplayRole: return 
        row=index.row()
        return self.data_obj.selectors[row]

class MyWindow(QtGui.QWidget):
    def __init__(self, *args):
        QtGui.QWidget.__init__(self, *args)
        vLayout=QtGui.QVBoxLayout(self)
        self.setLayout(vLayout)

        self.data=Data()
        self.tableModel = TableModel(self.data)
        self.comboModel = ComboModel(self.data)

        self.combo=QtGui.QComboBox()
        self.combo.setModel(self.comboModel)
        self.combo.activated.connect(self.tableModel.setSelection)
        vLayout.addWidget(self.combo)

        self.ViewA=QtGui.QTableView(self)
        self.ViewA.setModel(self.tableModel)
        self.ViewA.clicked.connect(self.viewClicked)
        vLayout.addWidget(self.ViewA)

        self.tableModel.setSelection(0)

    def viewClicked(self, indexClicked):
        print('indexClicked() row: %s  column: %s'%(indexClicked.row(), indexClicked.column() ))

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())

But here is another version which is more based on your idea, and is a pattern I have used in the past. Here we have a single model as you requested, but providing two interfaces both actually QAbstractItemModels. It's not that different in this example but makes the single model idea more clear.

The first version has a data object which is driven by the underlying data representation and consequently can be quite clean and clear. But it needs a custom signalling method to signal changes to the two models.

The second version has a single integrated model object which encapsulates that signalling. But in a real system it will probably still need to connect to a separate data object - so in practice there are probably more objects in this version.

from PySide import QtGui, QtCore
import sys

class CombinedModel(object):

    def __init__(self):
        self.items = [['Pet', 'Dog'],['Pet', 'Cat'],['Bird','Eagle'],['Bird','Jay'],['Bird','Falcon']]
        self.selectors = list({ k[0] for k in self.items})
        self.table_if = TableModel(self)
        self.combo_if = ComboModel(self)
        self.currentSelection = None

    def currentItems(self):
        return [k[1] for k in self.items if k[0] == self.currentSelection]

    def setSelection(self,combo_row):
        self.currentSelection = self.selectors[combo_row]
        self.table_if.layoutChanged.emit()

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, model, parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.model = model

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.model.currentItems())      
    def columnCount(self, parent=QtCore.QModelIndex()):
        return 1
    def data(self, index, role):
        if not index.isValid() or role != QtCore.Qt.DisplayRole: return 
        row=index.row()
        return self.model.currentItems()[row]

class ComboModel(QtCore.QAbstractListModel):
    def __init__(self, model, parent=None):
        QtCore.QAbstractListModel.__init__(self, parent)
        self.model = model

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.model.selectors)      
    def data(self, index, role):
        if not index.isValid() or role != QtCore.Qt.DisplayRole: return 
        row=index.row()
        return self.model.selectors[row]

class MyWindow(QtGui.QWidget):
    def __init__(self, *args):
        QtGui.QWidget.__init__(self, *args)
        vLayout=QtGui.QVBoxLayout(self)
        self.setLayout(vLayout)

        self.model=CombinedModel()

        self.combo=QtGui.QComboBox()
        self.combo.setModel(self.model.combo_if)
        self.combo.activated.connect(self.model.setSelection)
        vLayout.addWidget(self.combo)

        self.ViewA=QtGui.QTableView(self)
        self.ViewA.setModel(self.model.table_if)
        self.ViewA.clicked.connect(self.viewClicked)
        vLayout.addWidget(self.ViewA)

        self.model.setSelection(0)

    def viewClicked(self, indexClicked):
        print('indexClicked() row: %s  column: %s'%(indexClicked.row(), indexClicked.column() ))

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())

Upvotes: 3

Mel
Mel

Reputation: 6065

I want ComboBox to display two generic categories (Pet and Bird). After the user selects the category QTableView would display a list of every specie associtated under the selected category.

To do this, you don't need to subclass any model. You can populate a QComboBox directly with a list of strings, and you can use a QListWidget to display the animal names.

To make things easier, I transformed your list into a dictionary:

self.items = [['Pet', 'Dog'],['Pet', 'Cat'],['Bird','Eagle'],['Bird','Jay'],['Bird','Falcon']]

becomes

self.myDict={"Pet":['Dog','Cat'],"Bird":['Eagle','Jay','Falcon']}

The keys of the dictionary (Pet, Bird) can be use for the QComboBox. When the user select one specie (signal: QComboBox.currentIndexChanged), you display the list associated to the key in a QListWidget.

Here's the full working example:

import sys, signal
from PyQt4 import QtCore, QtGui

class myWidget(QtGui.QWidget):
    def __init__( self, parent=None):
        super(myWidget, self ).__init__( parent )

        self.myDict={"Pet":['Dog','Cat'],"Bird":['Eagle','Jay','Falcon']}

        self.listWidget=QtGui.QListWidget()

        self.comboBox=QtGui.QComboBox()
        self.comboBox.addItems(list(self.myDict.keys()))
        #use overload signal: emits the text associated to the index 
        self.comboBox.currentIndexChanged[str].connect(self.on_change)
        #set initial index so the listWidget is not empty
        self.comboBox.setCurrentIndex(1)

        layout=QtGui.QVBoxLayout()
        layout.addWidget(self.comboBox)
        layout.addWidget(self.listWidget)
        self.setLayout(layout)

    def on_change(self,key):
        #clear everything
        self.listWidget.clear()
        #fill with list of corresponding key
        for name in self.myDict[key]:
            item=QtGui.QListWidgetItem(name)
            item.setFlags(item.flags()|QtCore.Qt.ItemIsUserCheckable)
            item.setCheckState(QtCore.Qt.Unchecked)
            self.listWidget.addItem(item)

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    win= myWidget()
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    win.show() 
    sys.exit(app.exec_())

Upvotes: 0

Related Questions