Search code examples
c++qtqt5qthreadqinputdialog

QInputDialog & Threading ( worker, show dialog, wait for input, continue)


I have a question about a QInputDialog. I have spent the last 3 days on Google and thought it is time to ask here, since I cannot find any answer;

My application has a main thread (also known as the GUI-thread in Qt). This GUI-thread creates a worker. Which is ran in a different thread. This worker scans folders. It also sends the GUI-thread information about progress. Working fine.

Now here is the problem. The worker thread can come across a situation in which it must ask the user for input. A QString. It must wait for an answer before continuing scanning the remaining folders. However, a worker thread cannot show a QInputDialog it turns out. Only the GUI-thread.

I cannot use slots and signals either because they cannot return a value in Qt. I tried using a slot and a referenced QString, but it crashes sometimes. Not thread-safe I suppose.

I tried QMetaObject::invokeMethod but couldn't get it to work either. Also, is that even thread safe?

Anyone here that has a solution for this?

Below is my code if that helps, it has been "compacted" so I don't waste your valuable time on getting familiar with my variable names and actual stuff.

INITIAL CODE (QInputDialog in worker thread) MainWindow.cpp

void MainWindow::worker_create(){
    Worker* worker = new Worker;
    QThread* thread = new QThread;
    worker->moveToThread(thread);
    connect(thread, SIGNAL(started()), worker, SLOT (start_work()));
    connect(worker, SIGNAL(worker_status_changed(QByteArray)), ChanComObject, SLOT(worker_update(QByteArray)));
    connect(worker, SIGNAL(finished(QString)), this, SLOT(worker_destroy(QString)));
    connect(worker, SIGNAL(finished(QString)), worker, SLOT(deleteLater()));
    connect(worker, SIGNAL(finished(QString)), thread, SLOT (quit()));
    connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();
}

Worker.cpp

Worker::ask(){
    QStringList listToChooseFrom;
    listToChooseFrom.append("A");
    listToChooseFrom.append("B");
    QString answer = QInputDialog::getItem(this, "Title", "Message", listToChooseFrom, 0, false, &ok);
    return answer;
}

SNEAKY REFERENCE CODE (Referenced QString, works sometimes, other times it crashes) Sneaky reference trick I tried was like this.. first in MainWindow.cpp:

void MainWindow::worker_create(){
    // all other stuff from above, but an extra connect:
    connect(worker, SIGNAL(worker_asks(Qstring*)), SLOT(gui_thread_dialog(QString*)));
}
void MainWindow::gui_thread_dialog(*sneakyReturnValue){
     QString answer = QInputDialog::getItem(this, "Title", "Message", listToChooseFrom, 0, false, &ok);
    *sneakyReturnValue = answer;
}

Then in the worker..

Worker::ask(){
    QString sneakyReturnValue;
    emit worker_asks(*sneakyReturnValue);
    return sneakyReturnValue;
}

INVOKEMETHOD CODE (Can't get it working due to parent(), not sure if thread-safe either) The invokeMethod I tried I never got working, but it went like.. in MainWindow.cpp

Q_INVOKABLE QString askUser();

..and in MainWindow.cpp

QString MainWindow::askUser(){
    QStringList listToChooseFrom;
    listToChooseFrom.append("A");
    listToChooseFrom.append("B");
    return QInputDialog::getItem(this, "Title", "Message", listToChooseFrom, 0, false, &ok);
}

and finally in Worker.cpp

Worker::ask(){
    QString dialogReturn;
    QStringList listToChooseFrom;
    listToChooseFrom.append("A");
    listToChooseFrom.append("B");
    bool u = QMetaObject::invokeMethod(parent,
                              "askUser",
                              Qt::BlockingQueuedConnection,
                              Q_RETURN_ARG(QString, dialogReturn),
                              Q_ARG(QStringList, listToChooseFrom));
    if(!u){ qDebug() << "invokeMethod failed"; }
    result = dialogReturn;
}

But I couldn't get a reference to the GUI-thread working.. the first argument of invokeMethod. parent isn't working. I think my worker is not a child of my GUI-thread automatically. Well, parent() isn't working at least.


Solution

  • The closest solution you have proposed is to use QMetaObject::invokeMethod(), but you should not use parent() but pass it an object of the class that has the method to invoke, in this example I will pass the GUI:

    worker.h

    #ifndef WORKER_H
    #define WORKER_H
    
    #include <QDebug>
    #include <QStringList>
    #include <QInputDialog>
    #include <QThread>
    
    class Worker : public QObject
    {
        Q_OBJECT
        QObject *mGui;
    
    public:
        explicit Worker(QObject *gui, QObject *parent = nullptr):QObject(parent){
            mGui = gui;
        }
        virtual ~Worker(){}
    
    public slots:
        void start_work(){
            ask_user();
        }
        void ask_user(){
            QStringList choices;
            choices.append("one");
            choices.append("two");
            QString retVal;
            for(int i=0; i < 10; i++){
                QMetaObject::invokeMethod(mGui, "callGuiMethod", Qt::BlockingQueuedConnection,
                                          Q_RETURN_ARG(QString, retVal),
                                          Q_ARG(QStringList, choices));
                qDebug()<<retVal;
                QThread::sleep(5);//emulate processing
            }
        }
    
    signals:
        void finished();
    };
    
    #endif // WORKER_H
    

    widget.h

    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include "worker.h"
    
    #include <QThread>
    #include <QWidget>
    
    class Widget : public QWidget
    {
        Q_OBJECT
        QThread workerThread;
    public:
        explicit Widget(QWidget *parent = nullptr):QWidget(parent){
            worker_create();
        }
    
        Q_INVOKABLE QString callGuiMethod(QStringList items){
            return QInputDialog::getItem(0, "title", "label", items , 0, false);
        }
    
        ~Widget() {
            workerThread.quit();
            workerThread.wait();
        }
    
    public slots:
        void worker_create(){
            Worker *worker = new Worker(this);
            worker->moveToThread(&workerThread);
            connect(&workerThread, &QThread::started, worker, &Worker::start_work);
            connect(worker, &Worker::finished, worker, &QObject::deleteLater);
            connect(worker, &Worker::finished, &workerThread, &QThread::quit);
            connect(&workerThread, &QThread::finished, &workerThread, &QObject::deleteLater);
            workerThread.start();
        }
    };
    
    #endif // WIDGET_H
    

    The complete example can be found at the following link