PDA

View Full Version : Using model with different views



Wallabee19
24th April 2017, 04:06
I've been working with Qt for a few months now and am still having some trouble with understanding the model view architecture. I have some data structure of the following form (I use PyQt5):


class MyData:
def __init__(self):
self.name = "Item 1"
self.value = "Value 1"

# List of lists
self.attributes = [
["Attr 1", "100", "560", "300", "1200"]
,["Attr 2", "50", "40", "470", "20"]
,["Attr 3", "75", "30", "30", "100"]
,["Attr 4", "300", "300", "280", "400"]
]

I want to display this data in two different views. (read only) The 'name' and 'value' attributes in one view ( 2 columns ). And the list of attributes in another view ( 5 Columns ) when whichever item in the first view is selected. I threw together a quick representation of what I want in Qt Designer:

12443

Ideally I would like to use a single model (QAbstractItemModel, or maybe even just QAbstractTableModel?) I believe a main model and two Proxy models are probably the way to do this? With the proxy models having their own reimplementation of headerData(). But how does that work then with columnCount()? Do I have to reimplement that as well in each proxy? Here is somewhat pseudocode of what my current main model looks like:


class MyModel(QAbstractItemModel):
def __init__(self, parent=None)
super(MyModel, self).__init__(parent)
self.myData = [] # List of MyData() instances

def columnCount(self, parent):
# Do I have to check which view is requesting the column count?

if firstView:
return 2
else:
return 5

def rowCount(self, parent):
if firstView:
return len(self.myData)
else:
return len(self.myData[parent.row()].attributes) # ??

def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable

def headerData(self, section, orientation, role):
pass # Reimplemented in each proxy model?

def parent(index):
# I have no idea how this needs to be implemented since the data in each view is essentially a table, but I still probably need some sort of parent index
return QModelIndex()

def index(row, column, parent):
return createIndex(row, column, self.myData[row]) # Probably just use internalPointer() in data()?

def data(self, index, role):
# Again, I'm not sure if I implement this in the main model or each of the proxy models?

if not index.isValid():
return None

data = index.internalPointer()
if role == Qt.DisplayRole:
if index.column() == 0:
return ...

def insertRow(MyData)
# I would implement some insert method to insert into self.myData

I would then connect the selectionChanged signal for the first view to a slot that would reset the proxy model for the second view? And probably have to do setRootIndex()? Again I'm a bit lost on how to go about doing this and would love to see a working example with the above idea. The actual modeling of the structure when there is a list that needs to be displayed in a different view is what's really confusing me.

Feel free to respond in C++ or PyQt. I can understand both. Any advice would be appreciated. Thanks.

high_flyer
24th April 2017, 16:04
I believe a main model and two Proxy models are probably the way to do this?
Depends.
Proxy models are used when you want to show the data in some "digested" form, sorted or filtered mostly.
If you want to show the data as is (you could show just part of the data in each view) then you can use only one model.
From your post I could not understand if you are doing anything to the data that you show in both views or not.

Are the "Item" and "Value" pair part of the attributes data that is shown in the lower table?
Or is it more like a parent child relationship? (where Item+value are parent to a an attributes set?
This would then be probably best done as a QAbstractItemModel, or depending on your constraints with a QStandardItemModel (would make it easier for you if you could use the later).
If that is the case you could indeed use only one model and show the "parents" in the upper view and the "children" of the selected part in the lower view.
But these were guesses on my part, not sure this is what you actually are after here.

Wallabee19
25th April 2017, 01:54
If you look at the data structure I have at the top I've used the same data that is displayed for the first row in the picture example. I will have instances of that and each instance represents a row in the top view (In the picture example, 3 instances that would be stored in the self.myData list within the model. (First row is selected in the view). When you select a row from the top view, the bottom view should be filled with that particular selected row's/instance self.attributes list.

Wallabee19
25th April 2017, 10:44
I've come up with a solution using a QIdentityProxyModel for the bottom view and just using the main model for the top view. I connect the currentRowChanged() signal from the top views selection model to a slot that simply does the following:


def updateProxyModel(current, previous)
ProxyModel.beginResetModel()
ProxyModel.selectedRow = current.row()
ProxyModel.endResetModel()

I did have to reimplement index() as the base implementation of QAbstractTableModel calls hasIndex() which checks the rowCount() and columnCount() of only the main model, and would never generate indexes above 2 columns for the proxy model. But this was ok, because I like to use internalPointer() anyway to save on a little bit of typing in the data() methods

Main model:


class MainModel(QAbstractTableModel):
def __init__(self, data, parent=None):
super(MainModel, self).__init__(parent)
self.headers = ('Name', 'Value')
self.data = data

def columnCount(self, parent=QModelIndex()):
return len(self.headers)

def rowCount(self, parent=QModelIndex()):
return len(self.data)

def index(self, row, col, parent=None):
return self.createIndex(row, col, self.data[row])

def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemNeverHasChildren

def headerData(self, column, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.headers[column]

def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None

rowData = index.internalPointer()
col = index.column()

if role == Qt.DisplayRole:
if col == 0:
return rowData.name
if col == 1:
return rowData.value

return None

Proxy Model:


class ProxyModel(QIdentityProxyModel):
def __init__(self, data, parent=None):
super(ProxyModel, self).__init__(parent)
self.data = data
self.headers = ('Attribute', 'Width', 'Height', 'Pos X', 'Pos Y')
self.selectedRow = None

def headerData(self, column, orientation, role=None):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.headers[column]

def columnCount(self, parent=QModelIndex()):
if parent.isValid():
return 0

return len(self.headers)

def rowCount(self, parent=QModelIndex()):
if parent.isValid() or not self.selectedRow:
return 0

return len(self.data[self.selectedRow].attributes)

def data(self, index, role=Qt.DisplayRole):
if not index.isValid() or not self.selectedRow:
return None

col = index.column()
row = index.row()

if role == Qt.DisplayRole:
return self.data[self.selectedRow].attributes[row][col]

return None

Decent solution? What could be done better or differently?

high_flyer
25th April 2017, 15:46
If you look at the data structure I have at the top I've used the same data that is displayed for the first row in the picture example
I personally would have used a tree for that (parent child relationship).
This would allow you to use a tree view as well, or some other hierarchical view.
Or use a combox and a table.
When you do that you can give the index of the item selected in the upper view as the root index for the lower view freeing you from the need to use proxies for separating the data.
But at the end there is no right or wrong, but what best fits your needs.