Deleting QTreeView item using custom context menu crashes - c++

I have my QTreeView with custom model and items. Each item can have context menu triggered by right click. My problem is that when I am trying to refresh the View after deleting an item using its context menu, the application crashes because it get segmentation fault. The context menu is run in other thread by exec() so it doesn't know when the item (and its context menu QMenu) is deleted and crashes.
void ProjectExplorerDock::onCustomContextMenu(const QPoint& point)
{
QModelIndex index = this->pProjectExplorerView->indexAt(point);
auto item = static_cast<TreeItem*>(index.internalPointer());
if(item)
{
if(item->getMenu())
{
item->getMenu()->exec(this->pProjectExplorerView->viewport()->mapToGlobal(point));
}
}
}
The triggered action emits a signal to delete the item. I've tried to do this after the exec() like this:
void ProjectExplorerDock::onCustomContextMenu(const QPoint& point)
{
QModelIndex index = this->pProjectExplorerView->indexAt(point);
auto item = static_cast<TreeItem*>(index.internalPointer());
if(item)
{
if(item->getMenu())
{
item->getMenu()->exec(this->pProjectExplorerView->viewport()->mapToGlobal(point));
emit refreshProject();
}
}
}
but it's much more work to distinguish if it is necessary to refresh or not.
Is there a way to stop the exec() when the QMenu is deleted?
Example function to show how it is done. It is called every time I want to refresh the view. Each TreeItem has QMenu.
void ProjectExplorerDock::fillModel(Project& project)
{
delete pTreeModel;
pTreeModel = new TreeModel(mHeaders);
if(!project.getName().empty())
{
QList<QVariant> projectName {QString::fromStdString(project.getName()), ""};
TreeItem* projectNameItem = new TreeItem(projectName);
pTreeModel->addNewTreeItem(projectNameItem, pTreeModel->getRootItem());
pProjectExplorerView->setModel(pTreeModel);
void TreeModel::addNewTreeItem(TreeItem* item, TreeItem* parentItem)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
item->setParent(parentItem);
parentItem->appendChild(item);
endInsertRows();
}

Okay, I found the answer.
Before deleting the QMenu make sure to call QMenu::hide() to stop the use of it.

Related

slow response on right click contex menu in graphic scene Qt

I have set a large menu in event filter on right click with 45-50 actions
inside and I find that when I right click the response to show the menu is slow
I did try the same code with 5 actions in the menu and the response was fine.
Is there something wrong with this way of coding on a contex menu ?
eventFilter
bool Editor::eventFilter(QObject *o, QEvent *e)
{
Q_UNUSED (o);
QGraphicsSceneMouseEvent *me = (QGraphicsSceneMouseEvent*) e;
switch ((int) e->type()){
case QEvent::GraphicsSceneMousePress:{
switch ((int) me->button()){
case Qt::RightButton:{
QGraphicsItem *item = itemAt(me->scenePos());
showContextMenu(item->scenePos().toPoint());
return true;
}
//more cases here//
}
break;
}
}
return QObject::eventFilter(o, e);
}
showContextMenu
void Editor::showContextMenu(const QPoint &pos)
{
QGraphicsItem *item =itemAt(pos);
// Create main effe menu
effeMenu= new QMenu("Menu");
QString menuStyle(
"QMenu {"
"border:10px };"
//more code here
);
effeMenu->setStyleSheet(menuStyle);
AmpMenu=effeMenu->addMenu(QIcon(":/effectImg/img/effePng/amp.png"),"Amp");
Amp1 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 1");
Amp2 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 2");
CabMenu=effeMenu->addMenu(QIcon(":/effectImg/img/effePng/cab.png"),"Cab");
Cab1 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 1");
Cab2 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 2");
.
.
.
.
//45 actions more
connect(effeMenu, &QMenu::triggered,this,[this,&item](QAction * k){
menuSelection(k,item);
});
Instead of creating a new QMenu each time you call showContextMenu you could make it a member of the class and build it once. On the other hand it is not necessary to use a signal, you could simply use the exec() method of QMenu:
*.h
class Editor: ...{
...
private:
QMenu effeMenu;
}
*.cpp
Editor::Editor(...){
effeMenu.setTitle("Menu");
QString menuStyle(
"QMenu {"
"border:10px };"
//more code here
);
effeMenu.setStyleSheet(menuStyle);
AmpMenu=effeMenu.addMenu(QIcon(":/effectImg/img/effePng/amp.png"),"Amp");
Amp1 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 1");
Amp2 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 2");
CabMenu=effeMenu.addMenu(QIcon(":/effectImg/img/effePng/cab.png"),"Cab");
Cab1 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 1");
Cab2 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 2");
...
}
void Editor::showContextMenu(const QPoint &pos){
QGraphicsItem *item =itemAt(pos);
QAction *action = menu.exec(pos);
menuSelection(action, item);
}
There are two things you can do to improve speed:
1 - itemAt(pos) is costly, and you are doing it twice, one in the event, and one in the showContextMenu. From what I could understand from your code you don't need the item in the event, just in the showMenu.
2 - The menu creation that you are doing is expensive: all the actions have pixmaps. this allocs memory for the QPixmap, loads, execute, dumps. Because you told us that you use around 40 actions (and really, that's too much for a menu), this can get costly.
My advice:
Create a class for your menu, create one instance of it, add a setter for the current QGraphicsObject that your menu will work on, and always use that one instance.

Qt5 : Get value of item clicked in a listview

I'm making a Qt5.7 application where I am populating a QListView after reading stuff from a file. Here's the exact code of it.
QStringListModel *model;
model = new QStringListModel(this);
model->setStringList(stringList); //stringList has a list of strings
ui->listView->setModel(model);
ui->listView->setEditTriggers(QAbstractItemView::NoEditTriggers); //To disable editing
Now this displays the list just fine in a QListView that I have set up. What I need to do now is to get the string that has been double clicked and use that value elsewhere. How do I achieve that?
What I tried doing was to attach a listener to the QListView this way
... // the rest of the code
connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(fetch()));
...
And then I have the function fetch
void Window::fetch () {
qDebug() << "Something was clicked!";
QObject *s = sender();
qDebug() << s->objectName();
}
However the objectName() function returns "listView" and not the listView item or the index.
The signal already provides you with a QModelIndex which was clicked.
So you should change your slot to this:
void Window::fetch (QModelIndex index)
{
....
QModelIndex has now a column and a row property. Because a list has no columns you are interessted in the row. This is the index of the item clicked.
//get model and cast to QStringListModel
QStringListModel* listModel= qobject_cast<QStringListModel*>(ui->listView->model());
//get value at row()
QString value = listModel->stringList().at(index.row());
You should add the index as parameter of your slot. You can use that index to access the list
Your code should be some thing like this.
void Window::fetch (QModelIndex index) {
/* Do some thing you want to do*/
}

Removing item from QListWidget from inside a Widget

I have a QListWidget in my MainWindow that displays a list of VideoWidgets (a custom QWidget).
VideoWidget has a clickable label where on clicking the label it should delete a file and then remove the QListItem which holds the VideoWidget from the QListWidget. Here is my VideoWidget class:
VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent)
{
ClickableLabel *smallRed = new ClickableLabel(this)
//...
QObject::connect(smallRed,SIGNAL(clicked()),this,SLOT(removeVideo()));
}
void VideoWidget::removeVideo(){
//...code to remove a file
QListWidget* list = myParent->getList();
QListWidgetItem* item = list->takeItem(list->currentIndex().row());
myList->removeItemWidget(item);
}
The problem is that clicking the smallRed label will not select its item in the QListWidget which means that list->currentIndex().row() will return -1. Clicking anywhere else in the Widget does select the current item. For the code to work I currently have to first click anywhere in the VideoWidget and then click its ClickableLabel. Is there any way I can achieve the same effect with one single click on my ClickableLabel?
From your previous qestion, we suggested use signal and slots. For example:
for(int r=0;r<3;r++)
{
QListWidgetItem* lwi = new QListWidgetItem;
ui->listWidget->addItem(lwi);
QCheckBox *check = new QCheckBox(QString("checkBox%1").arg(r));
check->setObjectName("filepath");
connect(check,SIGNAL(clicked()),this,SLOT(echo()));
ui->listWidget->setItemWidget(lwi,check);
}
Slot:
void MainWindow::echo()
{
qDebug() << sender()->objectName() << "should be remmoved";
}
It is not unique way to solve this problem, but it shows all main things, with signals and slots mechanism, objectName and sender() you can achieve all what you need.
sender() return object which send signal, you can cast it, but if you need only objectName you should not cast.

Hide the checkbox from a QListView item

I wave a QListView that is backed by a QStandardItemModel. Under certain circonstances, the QStandardItem are made checkable. A checkbox gets displayed besides the item's display. At some point, I want to remove hide the QStandardItem checkbox. I set its checkable state to false but it doesn't hide the checkbox (though it cannot be checked anymore).
The only way I have found of hiding the checkbox is to replace the item with a new one. This doesn't seem the proper way to preceed.
This is the code:
MyModel::MyModel(QObject *parent):QStandardItemModel(parent){}
void MyModel::createItem(int row, const QString &text)
{
setItem(row, new QStandardItem(text));
}
void MyModel::setCheckable(int row)
{
item(row)->setCheckState(Qt::Unchecked);
item(row)->setCheckable(true); // A checkbox appears besides the text
}
void MyModel::hideCheckBox(int row)
{
item(row)->setCheckState(Qt::Unchecked);
item(row)->setCheckable(false); // does not work
// I need to completely replace the item for the checkbox to disapear.
// This doesn't seem the proper way to proceed
setItem(row, new QStandardItem(item(row)->data(Qt::DisplayRole).toString()));
}
Is there better way to proceed?
When you call setCheckState or setCheckable, the qt will update the data of list item by adding or setting a Qt::CheckStateRole data. If the Qt::CheckStateRole data is existed, the check icon will be shown. So you need remove it from the data map of the list item.
Finally, the code of hideCheckBox should be:
void MyModel::hideCheckBox(int row)
{
// check the item pointer
QStandardItem* pitem = item(row);
if (pitem == NULL) return;
// find and delete the Qt::CheckStateRole data
QMap<int, QVariant> mdata = itemData(pitem->index());
if (mdata.remove(Qt::CheckStateRole))
{
setItemData(pitem->index(), mdata);
}
}
Hope it useful. :)
I think the presence of the check boxes in items defined by item flags, so that I would write the function in the following way:
void MyModel::hideCheckBox(int row)
{
// Does not set the Qt::ItemIsUserCheckable flag.
item(row)->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
}

Is it possible to create a signal for when a QTreeWidgetItem checkbox is toggled?

I've created a checkbox that's also a QTreeWidgetItem using the code below.
//Populate list
QTreeWidgetItem *program = createCheckedTreeItem(QString::fromStdString(itr->first), true);
treePrograms->addTopLevelItem(program);
QTreeWidgetItem* ConfigDialog::createCheckedTreeItem(QString name,bool checkBoxState)
{
QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name));
item->setFlags(item->flags()|Qt::ItemIsUserCheckable);
if (checkBoxState)
{
item->setCheckState(0,Qt::Unchecked);
}
else
{
item->setCheckState(0,Qt::Checked);
}
return item;
}
I need a way of connecting a signal and slot for when the state of this checkbox is changed. The current way I've implemented this is below but unfortunately doesn't work. Can someone explain what I'm doing wrong and what I need to do in order to get it to connect?
connect(program, SIGNAL(toggled(bool)), this, SLOT(programChecked(bool)));
You have to grab the signal itemChanged ( QTreeWidgetItem * item, int column ) coming from QTreeWidget.
Connect to the signal itemClicked(QTreeWidgetItem* item, int column) of the tree. When handling the signal, just verify item->checkState(column).
Your QTreeWidgetItem is directly linked to your model data, so you should connect to your QTreeWidget's model's QAbstractItemModel::dataChanged signal to be notified of the change.
The best solution we found was to reimplement setData in an item subclass:
void MyItem::setData(int column, int role, const QVariant& value)
{
const bool isCheckChange = column == 0
&& role == Qt::CheckStateRole
&& data(column, role).isValid() // Don't "change" during initialization
&& checkState(0) != value;
QTreeWidgetItem::setData(column, role, value);
if (isCheckChange) {
MyTree *tree = static_cast<MyTree *>(treeWidget);
emit tree->itemCheckStateChanged(this, checkState(0) == Qt::Checked);
}
}
It would really be convenient to have this in Qt indeed, it makes me think about contributing a itemCheckStateChanged signal there directly :)
PS: setting Qt::ItemIsUserCheckable is unnecessary, this flag is on by default for all QTreeWidgetItems.
Add this signal to your QTreeWidget object:
connect(ui->treeWidget, &QTreeWidget::itemChanged, this, &YourDialog::treeWidgetItemChanged);
so you can use this slot to receive the signal:
void YourDialog::treeWidgetItemChanged(QTreeWidgetItem *TWI, int column)
{
//Do some staff
}