QListWidget crash when reusing the widget associated with item - c++

I have a custom simple widget inheriting from QWidget and I add it to a QListWidget like this :
void MainWindow::AddToWidgetList(const QString &tag, const QString &html)
{
HtmlItem *html_item = new HtmlItem();
html_item->set_tag(tag);
html_item->set_html(html);
connect(html_item, SIGNAL(RemoveIt(uintptr_t)), this, SLOT(on_RmBtn_clicked(uintptr_t)));
QListWidgetItem *list_item = new QListWidgetItem();
html_item->set_list_item(list_item);
list_item->setSizeHint(html_item->sizeHint());
ui->CodeBlocks->addItem(list_item);
ui->CodeBlocks->setItemWidget(list_item, html_item);
}
I then want to move the selected element up when a button is pressed
void MainWindow::on_UpArrowBtn_clicked()
{
if (ui->CodeBlocks->count() < 2)
return;
int current_row = ui->CodeBlocks->currentRow();
if (current_row == 0)
return;
HtmlItem *item_widget = (HtmlItem*)ui->CodeBlocks->itemWidget(ui->CodeBlocks->item(current_row));
QListWidgetItem *item = ui->CodeBlocks->takeItem(current_row);
ui->CodeBlocks->insertItem(current_row - 1, item);
ui->CodeBlocks->setItemWidget(item, item_widget);
}
but I get crash in this line :
ui->CodeBlocks->setItemWidget(item, item_widget);

The following example shows what is happening. Basically, the rules are like this:
Calling setItemWidget transfers the ownership of the Item-Widget to the QListWidget instance. Hence, it is QListWidget's responsibility to destroy the set Item-Widget.
Now, QListWidget has no member, which allows to withdraw the ownership of a set Item-Widget. The only option one has is to create a new Item-Widget with the same properties like Item-Widget about to removed.
Note, that the Item-Widget ist deleted later after returning to the event loop, which happens by calling deleteLater() inside of takeItem. Hence, it is valid to access label till the end of the slot.
If you are not happy with this behavior, you are still able to switch to a QListView class with your own delegate. Albeit this seems to be more work, it is the more extensible approach.
#include <QApplication>
#include <QHBoxLayout>
#include <QPushButton>
#include <QListWidget>
#include <QLabel>
#include <QDebug>
int main(int argc, char** args) {
QApplication app(argc, args);
auto frame = new QFrame;
auto listWidget = new QListWidget;
for (auto iter=0; iter<10; iter++)
{
auto label = new QLabel(QString("Item-%1").arg(iter));
auto item = new QListWidgetItem();
listWidget->addItem(item);
listWidget->setItemWidget(item, label); // listWidget becomes the owner of label
}
auto moveUp = new QPushButton("Move Up");
frame->setLayout(new QHBoxLayout);
frame->layout()->addWidget(listWidget);
frame->layout()->addWidget(moveUp);
frame->show();
QObject::connect(moveUp, &QPushButton::clicked, [&]()
{
auto row = listWidget->currentRow();
auto item=listWidget->currentItem();
if (!item) return;
if (row == 0) return;
auto label = qobject_cast<QLabel*>(listWidget->itemWidget(item));
if (!label) return;
QObject::connect(label, &QLabel::destroyed, []()
{
qDebug() << "Destroyed"; // takeItem calls deleteLater on itemWidget
});
auto myItem=listWidget->takeItem(row);
listWidget->insertItem(row-1,myItem);
listWidget->setItemWidget(item, new QLabel(label->text())); // copy content of itemWidget and create new widget
listWidget->setCurrentRow(row-1);
});
app.exec();
}

Related

Qt add many QGraphicsPixmapItem to a QGraphicsScene

I'm trying to create a layer system like most photo editor programs (Photoshop) and I'm basically drawing a single QGraphicsPixmapItem using QGraphicsPixmapItem::setPixmap(QPixmap *image); on QGraphicsScene. How could I do this but instead I can add many QPixmaps and remove them at will. I tried creating a list of QPixmaps and one of QGraphicsPixmapItems but it gets messy if I remove or rearrange the order of my QPixmaps is there a better way to do this?
QTimer *timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(draw())); //calls the function below to redraw sc
timer->start(30);
This updates the GraphicsScene every 30ms so any drawing I do on the pixmap *image gets drawn but now I want to get a list a QPixmap and add them to the scene everytime draw() is called but the problem is I need a list of QGraphicsPixmapItems and if I delete a layer or move the order of them I want the associated QGraphicsPixmapItem to also be removed/moved. I guess I can do this but it seems very complicated so any advice?
void PaintArea::draw()
{
m_item->setPixmap(*image); //Do this but call layerManager.getListOfLayers() and add each to the scene
}
The following small example app shows how you might proceed. Basically, there are two models and two views. The models are the QGraphicsScene and a standard QStandardItemModel, whereas the views are a QListView and a QGraphicsView.
The main task is to keep both models in sync, by using signal and slots.
The model can be modified with the Add button and the context menu. For this small app one can only add, remove and change the picture of your pixmap. It is really simple to add other actions like moving the items with drag and drop and also to hide/visible them using a checkable action and other custom user role.
#include <QApplication>
#include <QFileDialog>
#include <QGraphicsPixmapItem>
#include <QGraphicsView>
#include <QHBoxLayout>
#include <QListView>
#include <QMenu>
#include <QPushButton>
#include <QStandardItemModel>
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
auto frame = new QFrame;
frame->setLayout(new QHBoxLayout);
auto listView = new QListView;
frame->layout()->addWidget(listView);
auto graphicsView = new QGraphicsView;
frame->layout()->addWidget(graphicsView);
auto graphicsScene = new QGraphicsScene;
graphicsView->setScene(graphicsScene);
auto myModel = new QStandardItemModel;
auto btnAdd = new QPushButton("Add");
frame->layout()->addWidget(btnAdd);
QObject::connect(btnAdd, &QPushButton::clicked, [&]() {
auto item = new QStandardItem("Pixmap");
item->setData(QString("./data/test.png"), Qt::ItemDataRole::UserRole + 1);
myModel->appendRow(item);
});
listView->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
QObject::connect(listView, &QListView::customContextMenuRequested, [&](const QPoint& pos) {
auto index = listView->indexAt(pos);
QMenu menu;
auto remove = menu.addAction("Remove", [&]() {
myModel->removeRow(index.row(), index.parent());
});
if (!index.isValid()) remove->setEnabled(false);
auto changeImage = menu.addAction("Change...", [&]() {
auto file=QFileDialog::getOpenFileName(frame, "Select PNG file", "./data/", "(*.png)");
if (file.isEmpty()) return;
myModel->setData(index, file, Qt::ItemDataRole::UserRole + 1);
});
if (!index.isValid()) changeImage->setEnabled(false);
menu.exec(listView->mapToGlobal(pos));
});
QObject::connect(myModel, &QStandardItemModel::dataChanged, [&](const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles = QVector<int>()) {
if (auto item = myModel->itemFromIndex(topLeft)) {
if (auto pixItem = dynamic_cast<QGraphicsPixmapItem*>(graphicsScene->items()[topLeft.row()])) {
pixItem->setPixmap(QPixmap(item->data(Qt::ItemDataRole::UserRole + 1).toString()));
}
}
});
QObject::connect(myModel, &QStandardItemModel::rowsInserted, [&](const QModelIndex& parent, int first, int last) {
for (auto iter = first; iter <= last; iter++) {
auto index=myModel->index(iter, 0, parent);
auto pixmap=myModel->data(index, Qt::ItemDataRole::UserRole + 1).toString();;
auto item=graphicsScene->addPixmap(QPixmap(pixmap));
}
});
QObject::connect(myModel, &QStandardItemModel::rowsRemoved, [&](const QModelIndex& parent, int first, int last) {
auto items = graphicsScene->items();
for (auto iter = first; iter <= last; iter++) {
graphicsScene->removeItem(items[iter]);
}
});
listView->setModel(myModel);
frame->show();
return app.exec();
}
Header file:
...
QGraphicsScene *scene; QGraphicsItemGroup *itemGroup;
...
.cpp file:
void PaintArea::draw()
{
m_item->setPixmap(*image); m_item->setGroup(itemGroup); // Layers-related code
}
void PaintArea::deleteGroup(QGraphicsItemGroup *group)
{
scene->destroyItemGroup(group); // Layers-related code
}

How to disable the default copy behavior in QTreeView?

I have a QTreeView with a QStandardItemModel and I would like to be able to prevent the user from copying the text of the items.
#include <QMainWindow>
#include <QStandardItemModel>
#include <QTreeView>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr) :
QMainWindow(parent)
{
auto *treeView = new QTreeView(this);
auto *model = new QStandardItemModel(this);
for (int n = 0; n < 5; n++)
model->appendRow(createItem(QString::number(n)));
treeView->setModel(model);
treeView->setContextMenuPolicy(Qt::NoContextMenu);
setCentralWidget(treeView);
}
private:
QStandardItem *createItem(const QString &name)
{
auto *item = new QStandardItem(name);
item->setFlags(Qt::ItemIsEnabled);
return item;
}
};
I have already made the items not editable and disabled the context menu. However, it is still possible for the user to click on an item and copy the text by pressing Ctrl+C. I can use Qt::NoItemFlags, but I want the items to be enabled.
How to accomplish that?
To disable the default copy behavior of QTreeView reimplement QTreeView::keyPressEvent in a subclass, e.g. TreeView, like that:
void TreeView::keyPressEvent(QKeyEvent *event)
{
if (!(event == QKeySequence::Copy))
QTreeView::keyPressEvent(event);
}
Then in your code instead of QTreeView:
auto *treeView = new QTreeView(this);
instantiate TreeView:
auto *treeView = new TreeView(this);
Alternatively, you can use installEventFilter to trap the keystroke events with having to subclass.

How can I transform QTabWidget coordinates to its child's coordinates?

I'm trying to generate a right-click menu on a QTabWidget (lists) containing only QListWidgets. I get a menu below where I click the distance of the tab bar's height, which is expected because the context menu is applied to the QTabWidget.
void onCustomContextMenuRequested(const QPoint& pos) {
QListWidgetItem * item = ((QListWidget*)(lists->currentWidget()))->itemAt(pos);
if (item) showContextMenu(item, QListWidget(lists->currentWidget()).viewport()->mapToGlobal(pos));
}
void showContextMenu(QListWidgetItem* item, const QPoint& globalPos) {
QMenu menu;
menu.addAction(item->text());
menu.exec(globalPos);
}
I can get the menu to appear at the mouse, while still referring to an item about 100px beneath it, by changing
QListWidget(lists->currentWidget()).viewport()->mapToGlobal(pos));
to
QListWidget(lists->currentWidget()).viewport()->mapToParent(mapToGlobal(pos)));
But I can't get the menu to refer to the item I am clicking on. I have tried transforming to and from parent coordinates to no effect.
QPoint pos_temp = ((QListWidget*)(lists->currentWidget()))->viewport()->mapFromParent(pos);
if (item) showContextMenu(item, QListWidget(lists->currentWidget()).viewport()->mapToGlobal(pos_temp));
I have also tried to and from global coordinate, and combinations of global and parent, to undesirable effect.
So how can I get the right click menu to refer to the item I am clicking on?
The position sent by the customContextMenuRequested signal is with respect to the widget where the connection is established, and in this case I am assuming that it is the main widget so when using itemAt() of QListWidget it throws inadequate values since this method waits for the position with respect to the viewport(). The approach in these cases is to convert that local position to a global one and then map that global to a local position of the final widget.
In the next part I show an example.
#include <QApplication>
#include <QTabWidget>
#include <QListWidget>
#include <QVBoxLayout>
#include <QMenu>
class Widget: public QWidget{
Q_OBJECT
QTabWidget *lists;
public:
Widget(QWidget *parent=Q_NULLPTR):QWidget(parent){
lists = new QTabWidget;
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &Widget::customContextMenuRequested, this, &Widget::onCustomContextMenuRequested);
auto layout = new QVBoxLayout(this);
layout->addWidget(lists);
for(int i=0; i<4; i++){
auto list = new QListWidget;
lists->addTab(list, QString("tab-%1").arg(i));
for(int j=0; j<10; j++){
list->addItem(QString("item %1-%2").arg(i).arg(j));
}
}
}
private slots:
void onCustomContextMenuRequested(const QPoint& pos){
QPoint globalPos = mapToGlobal(pos);
QListWidget *list = static_cast<QListWidget *>(lists->currentWidget());
if(list){
QPoint p = list->viewport()->mapFromGlobal(globalPos);
QListWidgetItem *item = list->itemAt(p);
if(item)
showContextMenu(item, globalPos);
}
}
void showContextMenu(QListWidgetItem* item, const QPoint& globalPos) {
QMenu menu;
menu.addAction(item->text());
menu.exec(globalPos);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
#include "main.moc"
In the following link is the complete example.

QMainWindow does not show Qwidgets background

I have a problem regarding a QWidget (custom made) inside a QMainWindow (custom made). My problem is that when I add my widget to my window as its central widget using setCentralWidget() method, it won't show the widgets background. It's important to show the background correctly.
Here is my MyWindow.cpp code:
#include "MyMainWindow.h"
MyMainWindow::MyMainWindow(QWidget * parent, Qt::WindowFlags flag) :
QMainWindow(parent, flag)
{
this->setFixedSize(1120, 630);
menu = new MyMenu(this);
// setting = new MySetting();
// tutorial = new MyTutorial();
// game = new MyGame();
this->setCentralWidget(menu);
this->show();
}
MyMainWindow::~MyMainWindow()
{
}
My MyMenu.cpp code:
#include "MyMenu.h"
MyMenu::MyMenu(QWidget *parent, Qt::WindowFlags f) :
QWidget(parent, f)
{
this->resize(1120, 630);
this->set_background();
this->construct_buttons();
this->construct_menu();
}
MyMenu::~MyMenu()
{
delete start;
delete setting;
delete tutorial;
delete exit;
delete buttons;
delete logo;
delete menu;
}
void MyMenu::construct_menu()
{
menu = new QVBoxLayout(this);
logo = new QLabel(this);
QPixmap *pixmap = new QPixmap("/home/kahrabian/ClionProjects/Shooter-AP93UT/Contents/logo.png");
logo->setPixmap(*pixmap);
logo->setAlignment(Qt::AlignHCenter);
menu->addWidget(logo);
menu->addLayout(buttons);
delete pixmap;
}
void MyMenu::construct_buttons()
{
buttons = new QHBoxLayout();
start = new QPushButton("Start", this);
buttons->addWidget(start);
setting = new QPushButton("Setting", this);
buttons->addWidget(setting);
tutorial = new QPushButton("Tutorial", this);
buttons->addWidget(tutorial);
exit = new QPushButton("Exit", this);
buttons->addWidget(exit);
}
void MyMenu::set_background()
{
QPalette *palette = new QPalette();
palette->setBrush(this->backgroundRole(),QBrush(QImage("/home/kahrabian/ClionProjects/Shooter-AP93UT/Contents/background_menu.jpg")));
this->setPalette(*palette);
delete palette;
}
My main.cpp code:
#include <QApplication>
#include "MyMenu.h"
#include "MyMainWindow.h"
int main(int argc, char **argv)
{
QApplication app (argc, argv);
MyMainWindow *mainwin = new MyMainWindow();
// MyMenu *MyMenu = new MyMenu();
// MyMenu->show();
return app.exec();
}
Can anyone help me with this problem??
Check this answer.
I would recommend you to use Qt style sheets.
You would need to call something like this:
setStyleSheet("image: url(path/to/background/image.png);");
on your widget.
Also, you might need to implement paintEvent() for widget to accept style sheets.
I'm usually doing it like this:
void MyWidget::paintEvent(QPaintEvent *pe)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
QWidget::paintEvent(pe);
}

Creating and laying out widgets using a for loop in Qt

So i wanted to create 5 buttons in Qt but instead, create just one button and put it in a for loop so i don't have to create each of the 5 buttons manually. I tried different ways but all proved futile. I'm new to C++ and Qt.
Here are the codes;
show.h
#ifndef SHOW_H
#define SHOW_H
#include <QDialog>
#include <QPushButton>
#include <QVBoxLayout>
class Show : public QDialog {
Q_OBJECT
public:
explicit Show(QWidget *parent = 0);
~Show();
private:
QPushButton *button;
};
#endif // SHOW_H
show.cpp
#include "show.h"
#include "ui_show.h"
Show::Show(QWidget *parent) : QDialog(parent) {
int a = 5;
button = new QPushButton[a];
button->setText("Ok");
QVBoxLayout *layout = new QVBoxLayout[a];
for (int i = 0; i < sizeof(button)/4; i++) {
/*here, i wanted to do something like this;
'layout[i].addWidget(button[i]);' but didn't work*/
layout[i].addWidget(button);
}
setLayout(layout);
}
Show::~Show() {
}
main.cpp
#include "show.h"
#include <QApplication>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
Show *dialog = new Show;
dialog->show();
return a.exec();
}
After i run the code, i only see one button.
Your help is deeply appreciated. Thank you!!
QPushButton* pButton = new QPushButton("Ok");
This creates a single instance of a QPushButton.
You can add the button to a layout with a call to Layout::addWidget, which internally calls addItem.
As the documentation states for addItem: -
Note: The ownership of item is transferred to the layout, and it's the layout's responsibility to delete it
So your current code creates a single button and as it gets added to each successive layout, it is removed from the layout in which it was previously added.
You're creating one button and adding the same button 5 times. If you want 5 buttons in 5 layouts using a loop, then you need 5 separate instances of the button: -
for (int i=0; i<5; ++i)
{
QPushButton* pButton = new QPushButton("Ok");
layout[i].addWidget(pButton);
}
In your show.cpp change the lines
Show::Show(QWidget *parent) : QDialog(parent) {
int a = 5;
// You only need one layout for all buttons, not one per button.
QVBoxLayout *layout = new QVBoxLayout( this );
for (int i = 0; i < a; i++) {
QPushButton * newButton = new QPushButton( this );
newButton ->setText( "Ok" );
layout->addWidget( newButton );
}
setLayout(layout);
}
Show::~Show() {
}
The layout expects a pointer and you only need one layout instead one per button.