I have a collection widgets in a mainwindow that all work with a single bit of data on a model, I've attached a drawing of the structure below. Currently only the tree widget has access to the model and also has a treeview. Communication with the other widgets is done through signals and slots. When a new selection is made in the tree, every other widget on the screen changes to represent the new selected tree node. This works, however sending signals through one widget to all others is extremely cumbersome.
I'd like to pass the model that the tree uses to all the other widgets and make it work off of the signals that the model sends upon any changes to it. So instead of having all the classes sending signals to each other communication can be done through the model itself.
I've created and passed a model to the tree and the other widgets in the main window class's constructor but I'm not sure how I should proceed from this point as I am relatively unfamiliar with MVC. The tree class is the only class with a view at this point, do I need to add views to the other classes as well to be able to get access to signals that are emitted when a certain field is selected or would the model be enough?.
I'd appreciate any input, thanks.
Yes, notify model if something changed and make it emit signal, subscribe to that signal in relevant widgets. In other words decouple widgets from each other removing widget-widget subscriptions, subscribe only to model.
Selection is not a property of the model, but of the view. Multiple views can be attached to a single model, each displaying a different "item". Thus, there is no out of the box method to signal a change in selection from the model itself.
There is a dedicated QItemSelectionModel class, that deals with exactely what you need: keep track of selection on a single model across multiple views.
You would wrap your data model in a selection model and base your communication upon it, e.g.:
MyMainWindow::MyMainWindow() {
myModel = new MyModel;
treeView = new MyTreeView(myModel);
QItemSelectionModel *selectionModel = new QItemSelectionModel(myModel);
treeView->setSelectionModel(selectionModel);
detailWidget = new MyDetailWidget(this, selectionModel);
}
////////////
MyDetailWidget::MyDetailWidget(QWidget *parent, QItemSelectionModel *selectionModel) {
// React to changed selection
connect(selectionModel, &QItemSelectionModel::currentChanged, this,
[=](auto current, auto /*previous*/) {
updateView(current);
}
);
// React to change in selected item
connect(selectionModel->model(), &QAbstractItemModel::dataChanged, this,
[=](auto topLeft, auto bottomRight, auto /*roles*/) {
QItemSelection sel(topLeft, bottomRight);
if (sel.contains(selectionModel->currentIndex()) {
updateView(selectionModel->currentIndex());
}
}
);
}
void MyDetailWidget::updateView(const QModelIndex &idx) {
// Assume some label on this widget
m_name->setText(idx.data(Qt::DisplayRole).toString());
}
Related
I have subclassed QAbstractItemModel and trying to retrieve a widget in slot of dataChanged signal.
connect(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(slotDataChanged(const QModelIndex&, const QModelIndex&)));
void MyEditor::slotDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight)
{
QComboBox* widget = dynamic_cast<QComboBox*>(sender());
if (widget)
{
// do something
}
}
Here I am getting a null widget everytime, same result with qobject_cast.
I am setting combobox widget in my tableview a delegate class which derives QStyledItemDelegate.
MyDelegate* myDelegate;
myDelegate = new MyDelegate();
tableView->setItemDelegate(myDelegate);
tableView->setModel(model);
QWidget* MyDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QComboBox* cb = new QComboBox(parent);
cb->addItem(QString("All"));
cb->setCurrentIndex(0);
return cb;
}
How can I obtain sender object in this case? Thanks.
Not sure of what are your intentions. Acquiring the editor widget when the data is already updated in the model is unnecessary in general.
Seems to me that a brief introduction to Model-View-Delegate concept is needed to solve your problem.
In short, the view, which in your case is the QTableView, has no data by itself. View acquires data from the attached model by calling data method. When user tries to edit some data, delegate createEditor and setEditorData methods are called. The latter has model pointer as one of the arguments so it can access actual data which needs to be represented.
When user finishes editing setModelData is called which has the editor widget available to acquire the updated value. It also has the model available to change the proper data entry normally done via setData method. At this point the dataChanged signal is emitted which notifies the view that corresponding data was updated so it can refresh the displayed value(s).
Hence, try rethinking your design. Maybe what you want to achieve can be implemented differently or your implementation can be slightly modified to conform the described flow.
You can also check the Qt site for Star Delegate Example to see some sample implementations or Model View Tutorial for a broader description of the Model-View topic.
My Model/View design was fine. I just needed to obtain a widget when user double clicks on a cell in my editor.
QComboBox* widget = dynamic_cast<QComboBox*>(tableView->indexWidget(topLeft));
if (widget)
{
// Do something
}
Here in slotDataChanged I obtained the required widget using QModelIndex.
Thanks for helping me out.
There is a container Widget instance A and there are contained Widget instances C1, C2, C3, ... etc within A.
There is a slot that handles Widget A's action_triggreed() signal.
Is there a way to determine which of the target Widgets C1, C2, C3, ... was clicked?
The reason for this necessity is that there are numerous contained widgets, and it doesn't make sense to use connect() method on each, which would require 50+ lines of extra connect statements, one for each!
For example: Consider a QToolBox with lots of Buttons. How would you determine which Button is pressed by using a QToolBox action_triggered or similar signals, without using signals and slots for individual Buttons separately?
You can use the QObject::sender() function.
void A::triggeredSlot() {
QObject* obj = sender();
QButton* but = qobject_cast<QButton*>(obj);
}
Just use the correct types etc.
There are a few general approaches.
The first one is straightforward - you should connect each widget's signal to the slot and check what sender() is. Yes, lines of code for connecting.
The second one is to use a kind of mapper. Every widget is connected to the mapper and only mapper is connected to the target slot. Lines of code again, at least in the part where you connect widgets to the mapper. There is a generic QSignalMapper or you can use more specific one suited for your widgets. For example, if they are buttons then you can use QButtonGroup class. Every button is registered in the group and only one signal/slot connection is required.
QButtonGroup group;
group->addButton(buttonC1,C1_ID);
...
group->addButton(buttonC1,Cn_ID);
connect(&group,SIGNAL(buttonClicked(QAbstractButton*),this,SLOT(buttonClicked(QAbstractButton*));
The third approach is to detect mouse event only on the mother's widget A and then iterate over all its children and find which one is under mouse. Less code, you can easily add new widgets, but the cost is iterating over all widgets in runtime. Below is an example. Note, that you can add specific QObject names or properties to the widgets C1... so that you could filter them if you are interested only in a part of children widgets of the given type.
void mousePressEvent(QMouseEvent* event)
{
if (event->button()==Qt::LeftButton)
{
QList<QToolButton*> buttons=findChildren<QToolButton*>(); // you can also use specific object names on the widgets under your interest
foreach (QToolButton* button, buttons)
{
if (button->underMouse()) // you could try isDown() for button, but I'm not sure if that will work here
{
emit buttonClicked(button);
break;
}
}
}
}
Well, you could try another approach also. Detect the mouse event, get cursor position and find the child widget on that position. Not costly in runtime.
void mousePressEvent(QMouseEvent* event)
{
if (event->button()==Qt::LeftButton)
{
QPoint pt=mapFromGlobal(QCursor::pos());
QWidget* child=childAt(pt);
if (child)
{
emit childClicked(child);
}
}
}
I have a QListView that has its model (derived from QAbstractItemModel) regularly changed based on a some criteria in the UI. I would like to be notified when the view itself believes a new row has been added/removed either when the current model updates or when the model is changed. I need this notification so that I can call setIndexWidget and add a custom control under a particular column. I would prefer not to call setIndexWidget repeatedly because the population of the widget is expensive. So once per row would be ideal.
I've tried rowsInserted/rowsAboutToBeRemoved and dataChanged but those don't get called if the model being set into the view already has items in it.
Any thoughts would be greatly appreciated.
You're wrong about dataChanged. If the contents of any data item change, then dataChanged is signaled. The following invariant holds, assuming that the dataChanged slot is connected to the same signal on the model.
class ... : public QObject {
QModelIndex m_index;
bool m_changed;
Q_SLOT void dataChanged(const QModelIndex & tl) {
m_changed = m_changed || tl == m_index;
}
...
};
m_index = QModelIndex(...);
QVariant before, after;
m_changed = false;
before = model->data(index);
after = model->data(index);
Q_ASSERT(before == after || m_changed);
What you're describing is most likely caused by incorrect behavior of your model. There is a model test suite you could use to verify compliance of your model with required invariants.
Addressing your question points specifically:
I would like to be notified when the view itself believes a new row has been added/removed either when the current model updates...
The view doesn't need to believe anything. Your model must be emitting relevant signals to that effect. Simply connect to those signals from some QObject. That's all the view is doing. If the signals don't fire, the view won't be notified. End of story.
... or when the model is changed.
There's no signal for that since the entire model is replaced. You're the one who calls setModel on the view, so that shouldn't be a problem. You better know when the call is made :)
The source code relating to this question is available on my public Git repository on BitBucket.
I'm trying to dynamically add some items to a QTreeView model using the following code in mainwindow.cpp:
if(dlg->exec() == QDialog::Accepted) {
QList<QVariant> qList;
qList << item.name << "1111 0000" << "0x00";
HidDescriptorTreeItem *item1 = new HidDescriptorTreeItem(qList, hidDescriptorTreeModel->root());
hidDescriptorTreeModel->root()->appendChild(item1);
}
This works when run from within my MainWindow constructor, just after ui->setupUi(this), but I need this to run from within an event filter, but the same code doesn't get the QTreeView updating. When I set a breakpoint at mainwindow.cpp:70 and step through the next few lines, I can see the data is being added to the Model, but I need the QTreeView to refresh.
I understand this is done by emitting dataChanged(), but not really sure how to do this. The signal signature for the dataChanged signal looks as follows:
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>());
so I need to come up with topLeft and bottomRight QModelIndex instances. How do I build/obtain these from item1 in the above snippet?
Also, where does beginInsertRows() and endInsertRows() come into view with this, should I be calling these functions?
From QAbstractItemModel documentation:
void QAbstractItemModel::beginInsertRows ( const QModelIndex & parent, int first, int last ) [protected]
Begins a row insertion operation.
When reimplementing insertRows() in a subclass, you must call this function before inserting data into the model's underlying data store.
The parent index corresponds to the parent into which the new rows are inserted; first and last are the row numbers that the new rows will have after they have been inserted.
The other protected functions say similar things.
And insertRows() says:
If you implement your own model, you can reimplement this function if
you want to support insertions. Alternatively, you can provide your
own API for altering the data. In either case, you will need to call
beginInsertRows() and endInsertRows() to notify other components that
the model has changed.
Take a look to QAbstractItemModel protected functions and signals
Views connect to those signals to know when model data changes and rearrange data inside. The functions emit the signals internally to make it easy for you to warn the view when it has happenned. But signals can only be emitted by abstract class.
Components connected to this signal use it to adapt to changes in the
model's dimensions. It can only be emitted by the QAbstractItemModel
implementation, and cannot be explicitly emitted in subclass code.
So you will have to stick to the methods.
Edit in answer to your comment:
Indeed, Items should have a reference to model and tell it about changes, check theses lines from QStandardItem:
void QStandardItem::emitDataChanged()
void QStandardItem::removeRows(int row, int count)
( Note, how, in second, it calls model's rowsAboutToBeRemoved() and rowsRemoved() )
Maybe you should try to use QStandardItem and QStandardItemModel.
Either direct or subclassing. It will hide a lot of ugly stuff.
There's also a less proper but much easier way to achieve this - emit layoutChanged() instead of dataChanged(). More info - https://stackoverflow.com/a/41536459/635693
Is it possible to add QPushButtons for every item in a QTreeView? For instance, when you click on a TreeItem (that is a button), it's children get displayed as buttons as well? I just have a standard QTreeView.
_layout = new QVBoxLayout(this);
treeView = new QTreeView(this);
QStandardItemModel* standardModel = new QStandardItemModel();
QStandardItem* rootMenu = standardModel->invisibleRootItem();
//populate TreeView
treeView->setModel(standardModel);
treeView->setWordWrap(true);
treeView->setHeaderHidden(true);
//treeView->expandAll();
_layout->addWidget(treeView);
this->setLayout(_layout);
I have not personally done this (yet), but you could try using QAbstractItemView::setIndexWidget(). The widgets won't aren't connected in any way to the data model, so it is up to your code to update them if necessary. Also, you need to call it for each QModelIndex separately.
Here is the answer. You must create your own delegate and applay it for your QTreeView.
To create delegate you must subclass QStyledItemDelegate and re-implement its QStyledItemDelegate::paint(...) method in that way what you want, also, don't forget about re-implementing QStyledItemDelegate::sizeHint(...) method if needed, of course.
Also, you may need to re-implement QStyledItemDelegate::createEditor(...) method.
To apply created delegate to your view (QTreeView) you must create delegate and call QTreeView's method setItemDelegate (or setItemDelegateForColumn, or setItemDelegateForRow).
Good luck!