Andre
Andre

Reputation: 715

QTableWidget: signal itemChanged fires too late (after signals for other actions, close, etc.)

I am using a QTableWidget to represent some user editable data. Upon editing, the internal representation of the data is updated by connecting to the table's itemChanged(QTableWidgetItem*) signal. I have a menu action "save" which saves the data and also if the user tries to close the window, I check if there current document is modified in the closeEvent. If the user tries to close while having unsaved modifications, they get asked if they want to save.

Now, the problem is, that the itemChanged signal seems to only be sent when the table loses input focus. Consider this scenario: the user double clicks a cell in the table, changes the text, then immediately clicks save. The save action gets triggered before the itemChanged signal gets sent and thus before the user input actually gets synchronized. Likewise if the user closes the window.

I tried actively reading the data from the table in the save function instead of waiting for the signal to trigger, but that also doesn't work as the corresponding item still contains the old data while its editor is open. This is obviously a big problem because either the wrong data gets saved or no save happens at all, i.e. data loss.

How can I correctly deal with this?


Attached is a minimal working example demonstrating the problem. For demonstration purposes, the closeEvent just unconditionally executes the "save" (which just prints here). In a real application, it would check if data is modified first, which is not executed correctly.

mainwindow.h:

#include <QMainWindow>
#include <QMenu>
#include <QMenuBar>
#include <QTableWidget>

#include <iostream>
#include <vector>

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    std::vector<double> data;
    QTableWidget* table;

public:
    MainWindow(QWidget* parent = nullptr) : QMainWindow(parent)
    {
        data = {1.0, 2.0, 3.0};
        table = new QTableWidget(this);
        table->setRowCount(1);
        table->setColumnCount(data.size());
        for (size_t i = 0; i < data.size(); i++)
            table->setItem(0, i, new QTableWidgetItem(QString::number(data[i])));
        connect(table, SIGNAL(itemChanged(QTableWidgetItem*)),
                SLOT(on_table_itemChanged(QTableWidgetItem*)));
        setCentralWidget(table);

        QMenuBar* menubar = new QMenuBar(this);
        QMenu* file = new QMenu("File", menubar);
        QAction* save = new QAction("Save", this);
        file->addAction(save);
        menubar->addMenu(file);
        setMenuBar(menubar);
        connect(save, SIGNAL(triggered()), SLOT(on_actionSave_triggered()));
        setGeometry(0, 0, 400, 100);
    }

private slots:
    void on_actionSave_triggered()
    {
        std::cout << "data: " << data[0] << ", " << data[1] << ", " << data[2] << std::endl;
        std::cout << "item 0: " << table->item(0, 0)->text().toDouble()
                  << ", item 1: " << table->item(0, 1)->text().toDouble()
                  << ", item 2: " << table->item(0, 2)->text().toDouble() << std::endl;
    }
    void on_table_itemChanged(QTableWidgetItem* item)
    {
        data[item->column()] = item->text().toDouble();
    }
};

main.cpp:

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char* argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

Upvotes: 1

Views: 822

Answers (1)

Minh
Minh

Reputation: 1725

In your case, when the save action is triggered, you can first set focus to the QTableWidget object. The editor will lose focus and commit the data. I consider this a better way than the one I suggested in the comment (since it assumes the editor to be a QLineEdit object and uses dynamic_cast).

#include "mainwindow.h"

#include <QDebug>
#include <QDialog>
#include <QMenuBar>
#include <QPushButton>
#include <QTableWidget>
#include <QVBoxLayout>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QTableWidget *table_widget = new QTableWidget{this};
    table_widget->setRowCount(1);
    table_widget->setColumnCount(1);
    table_widget->setItem(0, 0, new QTableWidgetItem(QString{"Some text"}));
    connect(table_widget, &QTableWidget::itemChanged, [](){qDebug() << "Item changed";});

    QDialog *dialog = new QDialog{this};
    connect(dialog, &QDialog::accepted, [](){qDebug() << "Dialog accepted";});
    connect(dialog, &QDialog::rejected, [](){qDebug() << "Dialog rejected";});

    QMenuBar *menu_bar = new QMenuBar(this);
    QMenu *file_menu = new QMenu("File", menu_bar);
    QAction *save_action = new QAction("Save", this);
    file_menu->addAction(save_action);
    menu_bar->addMenu(file_menu);
    connect(save_action, &QAction::triggered, table_widget, QOverload<>::of(&QWidget::setFocus));
    connect(save_action, &QAction::triggered, [=](){qDebug() << "Save triggered";});

    QPushButton *save_button = new QPushButton{"Save", this};
    connect(save_button, &QPushButton::clicked, [](){qDebug() << "Save clicked";});
    QVBoxLayout *layout = new QVBoxLayout{dialog};
    layout->addWidget(menu_bar);
    layout->addWidget(table_widget);
    layout->addWidget(save_button);

    QPushButton *open_button = new QPushButton{"Open dialog", this};
    connect(open_button, &QPushButton::clicked, dialog, &QDialog::show);
    this->setCentralWidget(open_button);
}

MainWindow::~MainWindow()
{
}

I also notice that if you use a push button to save the data, the focus changes automatically to the button, resulting in the same behavior. I guess using QToolBar and QToolButton in place of QMenuBar could work too.

Upvotes: 1

Related Questions