0
votes

For some reason the Qt compiler doesn't compile if you try to pass a QObject derived class as the rparent to a QWidget derived class.

What is the correct way to provide a parent to QWidget derived classes in a QObject derived class? I'm thinking of the following solutions:

  • Use a smart pointer with the QWidget classes, instead of giving the object a parent.
  • Derive from QWidget instead of QObject (Sounds wrong to me, since the class isn't a widget).
  • Pass a QWidget instance to the QObject derviced class which already has a parent, as I've tried to demonstrate in the example below:
#include <QApplication>
#include <QtWidgets>

class ErrorMsgDialog;
class Controller;
class MainWindow;

// This is the QWidget class used in the QObject derived class (DataReadController)
class ErrorMsgDialog : public QDialog
{
    Q_OBJECT

public:
    explicit ErrorMsgDialog(QWidget *parent = nullptr)
        : QDialog(parent)
    {
        errorLbl = new QLabel("An unknown read error occured!");
        QHBoxLayout* layout = new QHBoxLayout;
        layout->addWidget(errorLbl);
        setLayout(layout);
        setWindowTitle("Error!");
        setGeometry(250, 250, 250, 100);
    }
    ~ErrorMsgDialog() { qDebug() << "~ErrorMsgDialog() destructed"; }

private:
    QLabel* errorLbl;
};

// QObject derived class - I want to instatiate Qwidget derived classes here, with this class as parent
class DataReadController
    : public QObject
{
    Q_OBJECT
public:
    DataReadController(QWidget* pw, QObject *parent = nullptr)
        : QObject(parent)
    {
        m_errorMsgDialog = new ErrorMsgDialog(pw);
        //m_errorMsgDialog = QSharedPointer<ErrorMsgDialog>(m_errorMsgDialog);
        //m_dataReader = new DataReader(this); m_dataReader->moveToThread(m_dataReaderThread); connect(....etc

        //simulate that DataReader emits an error msg
        QTimer::singleShot(2000, [&]() {
            onErrorTriggered();
        });
    }

public slots:
    void onNewDataArrived() {}

    // called if reader emits an error message
    void onErrorTriggered() { m_errorMsgDialog->show(); }
private:
    ErrorMsgDialog* m_errorMsgDialog;
    //QSharedPointer<ErrorMsgDialog> m_errorMsgDialog;
    //DataReader* m_dataReader;// DataReader is not shown here, it would be moved to a different thread and provide some data
};

// MainWindow
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr)
        : QMainWindow(parent)
    {
        parentWidget = new QWidget(this);   
        m_dataReadController = new DataReadController(parentWidget, this);
        setGeometry(200, 200, 640, 480);

        //Close after 5 seconds.
        QTimer::singleShot(5000, [&]() {
            close();
        });
    }

private:
    QWidget* parentWidget; // QWidget to pass to OBject derive class for parenting QWidgets
    DataReadController* m_dataReadController;
};

// Main
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

#include "main.moc"

This test crashes if I use QSharedPointer with ErrorMsgDialog. Any suggestions on the way to do this? Maybe none of my suggested solutions is the best practise?

1
QWidget expects a QWidget* as parent. Though QWidget is itself derived from QObject, this doesn't change the first requirement. There is nothing wrong to have a pointer to a QWidget in a class derived from QObject. However, concerning ownership, a QWidget should be owned by another QWidget (and that's what the parentship actual means). Concerning your ErrorMsgDialog, MainWindow w would be a sufficient parent. That doesn't mean that DataReadController couldn't add/remove these instances (or just show/hide). - Scheff's Cat
@Scheff Thanks. I'm not sure if I understand your 2 last sentences correct. Do you mean by 'MainWindow w would be a sufficient parent', that my example is a good way to provide a parent to 'ErrorMsgDialog', since the passed widget, 'parentWidget', is a child of MainWindow w? - NorwE
I would add a signal to DataReadController that emits an error with the message as an argument, and connect this signal to a slot in MainWindow that shows the message in a dialog. Think about how some Qt classes work, for example QSerialPort. You have a signal like errorOccurred which you can connect to when you want to show an error message. It doesn't show any dialogs on its own. You should follow a similar design. - thuga

1 Answers

1
votes

Correct storage and life-time management of dynamically created objects is not quite easy in C++. After a long time of struggling, I started to prefer certain techniques which I still use quite successful:

  1. local variables (storage and life-time managed by scopes)
  2. non-pointer member variables
  3. smart pointers with clear ownership (shared pointer) or non-ownership (weak pointer).

With this, I banned raw pointers from my sources nearly completely resulting in much maintenance friendlier code and less annoying debugging.

Of course, when external libraries (like e.g. widget sets) come into play, then I am bound to their APIs.

Concerning gtkmm 2.4, the above techniques worked quite good as well. gtkmm provides

  • smart pointers for sharable objects
  • some kind of ownership for widgets concerning child widgets.

When I switched to Qt, I saw all the news and raw pointers in the tutorial samples which made me afraid a bit. After some experiments, I came to the conclusion that I will be able to write ful-featured Qt applications similar like I did before in gtkmm – without nearly any need of new by (again) defining widgets as local variables (e.g. in main()) or member variables of other classes derived (directly or indirectly) from QWidget.

Beside of this, over the time, I realized the general Qt concept of Object Trees & Ownership but I must admit that I rarely rely on this in daily business.

Concerning the specific problem of OP:

A QWidget is derived of QObject. Hence, the usual ownership principle of QObjects apply. Additionally, a QWidget expects another QWidget as parent. A QWidget may be parent of any QObjects but not vice versa.

Hence, I would suggest the following ownership:

  • MainWindow is parent of DataReadController
  • MainWindow is parent of ErrorMsgDialog (which is created in DataReadController).

DataReadController stores a pointer to ErrorMsgDialog as raw pointer. (I believe the ownership provided by a QSharedPointer would collide with the ownership of MainWindow.)

(As already described above, in my own programming, I would try to prevent pointers at all and use (non-pointer) member variables instead. However, IMHO that's a matter of style and personal preference.)

The modified sample of OP testQParentship.cc:

#include <QtWidgets>

class Label: public QLabel {
  private:
    const QString _name;
  public:
    Label(const QString &name, const QString &text):
      QLabel(text),
      _name(name)
    { }

    virtual ~Label()
    {
      qDebug() << _name + ".~Label()";
    }
};

class HBoxLayout: public QHBoxLayout {
  private:
    const QString _name;
  public:
    HBoxLayout(const QString &name):
      QHBoxLayout(),
      _name(name)
    { }

    virtual ~HBoxLayout()
    {
      qDebug() << _name + ".~HBoxLayout()";
    }
};

class ErrorMsgDlg: public QDialog {
  private:
    const QString _name;
  public:
    ErrorMsgDlg(const QString &name, QWidget *pQParent):
      QDialog(pQParent),
      _name(name)
    {
      QHBoxLayout *pQBox = new HBoxLayout("HBoxLayout");
      pQBox->addWidget(
        new Label("Label", "An unknown read error occured!"));
      setLayout(pQBox);
      setWindowTitle("Error!");
    }

    virtual ~ErrorMsgDlg()
    {
      qDebug() << _name + ".~ErrorMsgDlg()";
    }
};

class DataReadCtrl: public QObject {
  private:
    const QString _name;
    ErrorMsgDlg *const _pDlgErrorMsg;

  public:
    DataReadCtrl(const QString &name, QWidget *pQParent):
      QObject(pQParent),
      _name(name),
      _pDlgErrorMsg(
        new ErrorMsgDlg(name + "._pDlgErrorMsg", pQParent))
    {
      //simulate that DataReader emits an error msg
      QTimer::singleShot(2000, [&]() {
        onErrorTriggered();
      });
    }

    virtual ~DataReadCtrl()
    {
      qDebug() << _name + ".~DataReadCtrl()";
    }

    void onErrorTriggered()
    {
      _pDlgErrorMsg->show();
    }
};

class MainWindow: public QMainWindow {
  private:
    const QString _name;
    DataReadCtrl *_pCtrlReadData;

  public:
    MainWindow(const char *name):
      QMainWindow(),
      _name(name),
      _pCtrlReadData(nullptr)
    {
      _pCtrlReadData
        = new DataReadCtrl(_name + "._pCtrlReadData", this);
      //Close after 5 seconds.
      QTimer::singleShot(5000, [&]() {
        qDebug() << _name + ".close()";
        close();
      });
    }

    virtual ~MainWindow()
    {
      qDebug() << _name + ".~MainWindow()";
    }
};

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // setup GUI
  MainWindow winMain("winMain");
  winMain.show();
  // runtime loop
  return app.exec();
}

and a minimal Qt project file testQParentship.pro:

SOURCES = testQParentship.cc

QT += widgets

Compiled and tested in cygwin64 on Windows 10:

$ qmake-qt5 testQParentship.pro

$ make && ./testQParentship
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o testQParentship.o testQParentship.cc
g++  -o testQParentship.exe testQParentship.o   -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread 
Qt Version: 5.9.4

Snapshot of testQParentship

After the QTimer::singleshot() in MainWindow is due, the main window is closed. The output of diagnostics illustrates that the object tree is destructed properly (instead of "thrown" away when OS releases process memory).

"winMain.close()"
"winMain.~MainWindow()"
"winMain._pCtrlReadData.~DataReadCtrl()"
"winMain._pCtrlReadData._pDlgErrorMsg.~ErrorMsgDlg()"
"HBoxLayout.~HBoxLayout()"
"Label.~Label()"