Unsolved Need some assistance with QStandardItemModel and multiple Views
-
I have a QStandardItemModel that represents website, account, and cookie information for that particular website. I need to use 2 separate views to display the data. In the left view is a simple QTreeView that shows 2 columns (website and account name). In the other view I need to show cookie information for the particular website/account that is selected in the left view. Both views don't require expanding of any items, so they are all top level items. Here is a diagram of what I'm trying to achieve:
I'm a bit confused on how this is supposed to work with a single model. My gut says I need to make my model 3 columns (Website, Account, Cookies), where Cookies is a QStandardItem with children (being the individual cookies). For the left view do I have to hide all those other columns with leftView.setColumnHidden()? How is setHorizontalHeaderLabels() supposed to work when using multiple views like this?
I have a pretty good understanding of the Model View Framework in Qt when using a single view. But when mixing views like this, it's got me a bit confused.
Any pointers in the right direction would be appreciated.
Thanks.
-
Both views don't require expanding of any items
Use QTableView Then
My gut says I need to make my model 3 columns
You'll need 7 columns (all those shown in your picture)
the top level items will have just the first 2 columns populated, then eachindex(i,0,QModelIndex())
will have children with the last 7 columns populated.something like this:
#include <QWidget> #include <QDate> #include <QStandardItemModel> #include <QTableView> #include <QHBoxLayout> #include <QHeaderView> #include <QTimer> class TestWidget : public QWidget { Q_OBJECT Q_DISABLE_COPY(TestWidget) public: enum ModelColumns{ mcWebsite ,mcAccount ,mcCookieName ,mcCookieValue ,mcCookieDomain ,mcCookiePath ,mcCookieExpire ,ModelColCount }; explicit TestWidget(QWidget *parent = Q_NULLPTR) :QWidget(parent) { model=new QStandardItemModel(this); model->insertColumns(0,ModelColCount); model->setHeaderData(mcWebsite,Qt::Horizontal,tr("Website")); model->setHeaderData(mcAccount,Qt::Horizontal,tr("Account")); model->setHeaderData(mcCookieName,Qt::Horizontal,tr("Cookie Name")); model->setHeaderData(mcCookieValue,Qt::Horizontal,tr("Cookie Value")); model->setHeaderData(mcCookieDomain,Qt::Horizontal,tr("Cookie Domain")); model->setHeaderData(mcCookiePath,Qt::Horizontal,tr("Cookie Path")); model->setHeaderData(mcCookieExpire,Qt::Horizontal,tr("Cookie Expiration")); leftView=new QTableView(this); leftView->horizontalHeader()->setStretchLastSection(true); leftView->setModel(model); leftView->setSelectionBehavior(QAbstractItemView::SelectRows); leftView->setSelectionMode(QAbstractItemView::SingleSelection); for(int i=0;i<ModelColCount;++i) leftView->setColumnHidden(i,i!=mcWebsite && i!=mcAccount); rightView=new QTableView(this); rightView->setModel(model); rightView->horizontalHeader()->setStretchLastSection(true); for(int i=0;i<ModelColCount;++i) rightView->setColumnHidden(i,i==mcWebsite || i==mcAccount); connect(leftView->selectionModel(),&QItemSelectionModel::selectionChanged,this,&TestWidget::webSiteSelected); QHBoxLayout *mainLay=new QHBoxLayout(this); mainLay->addWidget(leftView); mainLay->addWidget(rightView); /////////////////////////////////////////////////////////////////////////////// //insert example data model->insertRows(0,2); model->setData(model->index(0,mcWebsite),QStringLiteral("Website1")); model->setData(model->index(0,mcAccount),QStringLiteral("TestAccount1")); model->setData(model->index(1,mcWebsite),QStringLiteral("Website2")); model->setData(model->index(1,mcAccount),QStringLiteral("TestAccount2")); const QModelIndex exParent=model->index(0,0); model->insertRows(0,2,exParent); model->insertColumns(0,ModelColCount,exParent); model->setData(model->index(0,mcCookieName,exParent),QStringLiteral("some_cookie")); model->setData(model->index(0,mcCookieValue,exParent),QStringLiteral("some_value")); model->setData(model->index(0,mcCookieDomain,exParent),QStringLiteral("website.com")); model->setData(model->index(0,mcCookiePath,exParent),QStringLiteral("/")); model->setData(model->index(0,mcCookieExpire,exParent),QDate(2017,7,25)); model->setData(model->index(1,mcCookieName,exParent),QStringLiteral("some_cookie2")); model->setData(model->index(1,mcCookieValue,exParent),QStringLiteral("different_value")); model->setData(model->index(1,mcCookieDomain,exParent),QStringLiteral("sub.website.com")); model->setData(model->index(1,mcCookiePath,exParent),QStringLiteral("/blog")); model->setData(model->index(1,mcCookieExpire,exParent),QStringLiteral("Session")); model->insertColumns(0,ModelColCount,model->index(1,0)); /////////////////////////////////////////////////////////////////////////////// webSiteSelected(); } private slots: void webSiteSelected(const QItemSelection &selected = QItemSelection()){ QModelIndexList selectedIdx= selected.indexes(); std::sort(selectedIdx.begin(),selectedIdx.end(),[](const QModelIndex& a, const QModelIndex& b)->bool{return a.column()<b.column();}); const QModelIndex parent = selectedIdx.value(0,QModelIndex()); rightView->setRootIndex(parent); const int rowCnt=rightView->model()->rowCount(); for(int i=0;i<rowCnt;++i) rightView->setRowHidden(i,!parent.isValid()); } private: QAbstractItemModel* model; QTableView* leftView; QTableView* rightView; };
-
Thanks for the quick reply. I should have mentioned I'm using PyQt. I do have some experience with C++ in the past, so I can mostly read what is going on. I'm a bit confused why you are using setData() and index(). Isn't the point of using a QStandardItemModel so you don't have to calling these lower level QAbstractItemModel methods? Can I not just use QStandardItem's and appendRow()?
Also in the slot what is setRowHidden supposed to be doing? What are we wanting to hide in the right view? The function also seems to be missing a QModelIndex as the second parameter
In the example setData, Why do we need to insertColumns for parent of the second row?
"will have children with the last 7 columns populated"
Did you mean last 5?
model->insertColumns(0,ModelColCount,model->index(1,0));
Also I like the look of QTreeView over QTableView. With QTableView I get a "spreadsheet" looking view which I don't want. Unless there is a way to get the table view to look like a 2D tree view. I haven't dug that deep into the view functions yet.
-
@Wallboy said in Need some assistance with QStandardItemModel and multiple Views:
I should have mentioned I'm using PyQt. I do have some experience with C++ in the past, so I can mostly read what is going on.
I'm afraid I'm a total ignorant in python (and I'm ashamed of it) so I'll have to stick with C++
I'm a bit confused why you are using setData() and index(). Isn't the point of using a QStandardItemModel so you don't have to calling these lower level QAbstractItemModel methods?
No, QStandardItemModel gives you another interface to access the data (the so called QStandardItem interface) but nothing prevents you from using the QAbstractItemModel interface. The difference is that, if in the future you decide to change model (go for a custom one?) all you'll need to do to my code is change
model=new QStandardItemModel(this);
tomodel=new MyCustomModel(this);
if you had used the QStandardItem interface you'd have to rewrite the code from scratch.Can I not just use QStandardItem's and appendRow()?
Yes, of course you can
Also in the slot what is setRowHidden supposed to be doing? What are we wanting to hide in the right view?
It's just a dirty trick to handle the case when no website is selected. I basically hide all rows in the right view if nothing is selected and show them all if one website is selected
The function also seems to be missing a QModelIndex as the second parameter
the signature of the signal is
QItemSelectionModel::selectionChanged(const QItemSelection &, const QItemSelection &)
, since I don't need to know what indexes were deselected I just ignore the second parameter (just to be clear, the code above is tested and works)In the example setData, Why do we need to insertColumns for parent of the second row?
It's just to display the headers, really. if you did not insert the columns
model->columnCount(model->index(1,0))
would return 0 so the right view would not display the headers if you select the second row. this basically means every time you add a top level row, insert the columns for the child at the same time."will have children with the last 7 columns populated"
Did you mean last 5?Yes, my bad
Also I like the look of QTreeView over QTableView. With QTableView I get a "spreadsheet" looking view which I don't want. Unless there is a way to get the table view to look like a 2D tree view. I haven't dug that deep into the view functions yet.
Up to you really, you can try
rightView->showGrid(false); leftView->showGrid(false);
or change the views to QTreeViews and callleftView->setItemsExpandable(false);
it's just a matter of style, not of functionality -
"The function also seems to be missing a QModelIndex as the second parameter"
I was speaking of setRowHidden() function, but then realized your version was for QTableView, and mine is QTreeView, which needed the second parent QModelIndex parameter.
And if we only want to display the last 5 columns, why do you do this:
model->insertColumns(0,ModelColCount,exParent);
Should it not be:
model->insertColumns(0,5,exParent);
"It's just to display the headers, really. if you did not insert the columns model->columnCount(model->index(1,0)) would return 0 so the right view would not display the headers if you select the second row. this basically means every time you add a top level row, insert the columns for the child at the same time."
But what this before is doing already?
model->insertColumns(0,ModelColCount,exParent);
I have the following snippet of code which is not working and making a lot of columns in the right tree view. and only showing text for one row in the left view. Basically I have "Website" objects that hold "Account" objects. I loop through all websites and accounts extracting the information I need:
for site in list(registeredSites.values()): for account in list(enumerate(list(site.accounts.values()))): self.insertRow(0) self.setData(self.index(account[0], self.mWebsite), site.__class__.__name__) self.setData(self.index(account[0], self.mAccount), account[1].login) parent = self.index(account[0], 0) self.insertColumns(0, 5, parent) numCookies = randint(1, 15) # Just for testing for cookieNum in range(0, numCookies): self.insertRow(0, parent) cName = 'Cookie Name {}'.format(cookieNum) cValue = 'Cookie Value {}'.format(cookieNum) cDomain = 'Cookie Domain {}'.format(cookieNum) cPath = 'Cookie Path {}'.format(cookieNum) cExpires = 'Cookie Expires {}'.format(cookieNum) self.setData(self.index(cookieNum, 0, parent), cName) self.setData(self.index(cookieNum, 1, parent), cValue) self.setData(self.index(cookieNum, 2, parent), cDomain) self.setData(self.index(cookieNum, 3, parent), cPath) self.setData(self.index(cookieNum, 4, parent), cExpires)
I know you said you don't know much Python, but maybe you can see where I'm logically messing up in this code?
EDIT: Ok I got it working correctly,
but I don't understand why I had to "append" the row by switching insertRow(0) to insertRow(self.rowCount())(The enumerate value is wrong for subsequent loops. I seen what I did wrong there now) and also why I had to move the insertRow outside of the cookie loop. And I'm also a bit lost why I have to add all 7 columns as a child and not just the 5 cookie values. And just to clear about the sort lambda, we do that to make sure index(x, 0) is the first in the list? Are they not in order to begin with by column? Here is the now working code:for site in list(registeredSites.values()): for account in list(enumerate(list(site.accounts.values()))): self.insertRow(self.rowCount()) self.setData(self.index(account[0], self.mWebsite), site.__class__.__name__) self.setData(self.index(account[0], self.mAccount), account[1].login) parent = self.index(account[0], 0) self.insertColumns(0, self.mColumnCount, parent) numCookies = randint(1, 15) self.insertRows(0, numCookies, parent) for cookieNum in range(0, numCookies): # self.insertRow(0, parent) cName = 'Cookie Name {}'.format(cookieNum) cValue = 'Cookie Value {}'.format(cookieNum) cDomain = 'Cookie Domain {}'.format(cookieNum) cPath = 'Cookie Path {}'.format(cookieNum) cExpires = 'Cookie Expires {}'.format(cookieNum) self.setData(self.index(cookieNum, self.mName, parent), cName) self.setData(self.index(cookieNum, self.mValue, parent), cValue) self.setData(self.index(cookieNum, self.mDomain, parent), cDomain) self.setData(self.index(cookieNum, self.mPath, parent), cPath) self.setData(self.index(cookieNum, self.mExpires, parent), cExpires)
-
Sorry to bump my old thread, but I'm now in the situation where I need to switch the model from QStandardItemModel to a QAbstractItemModel and I can't figure out how to implement parent() and index() correctly. I still have Account objects in the following form:
class Account: def __init__(website, name, cookies): self.website = website # string self.name = name # string # list of lists: # [ ['CookieName1', 'CookieValue1', 'Domain1', 'Path1', 'Expiration1'], # ['CookieName2', 'CookieValue2', 'Domain2', 'Path2', 'Expiration2'] ] self.cookies = cookies
In my mind the model is supposed to look something like this (in this example pretend I have two Accounts in the model. First Account has 3 cookies, and the second Account only has 1 cookie):
[Website] [Account] [CName] [CValue] [CDomain] [CPath] [CExpires] - Website1 Account1 (empty) (empty) (empty) (empty) (empty) (empty) (empty) CName1 CVal1 Dom1 Path1 Exp1 (empty) (empty) CName2 CVal2 Dom2 Path2 Exp2 (empty) (empty) CName3 CVal3 Dom3 Path3 Exp3 - Website2 Account2 (empty) (empty) (empty) (empty) (empty) (empty) (empty) CName1 CVal1 Dom1 Path1 Exp1
for rowCount() I've done the following:
def rowCount(self, parentIdx): # If no parent index I assume top-level row, so just return the total amount of Accounts we have if not parentIdx.isValid(): return len(self.accounts) # Otherwise get the Account object and return how many cookies we have account = parentIdx.internalPointer() return len(account.cookies)
But I'm completely lost on how to implement index() and parent(). For index() I was thinking it would be just as simple as:
def index(self, row, column, parentIdx=QModelIndex()): return self.createIndex(row, column, self.accounts[row])
Where we need access to the Account object for every row, so we just create a pointer to the Account, but then how do I implement parent()? How do I know when I'm on a "child" (cookie) row and need to get a parent index?
I've been working with Qt for over a year and Tree Models still confuse the hell out of me. I do have a few models that follow the Qt Simple Tree Model Example, but those are designs that allow for arbitrary tree structure depth, whereas the one I'm trying to design is fixed to only 1 level max (the cookies if there is any).
I would appreciate any help.
Thanks