-1

I looked up all the different compiler errors that my design seems to cause. All the answers and fixes make sense but I reached a point where fixing one thing raises another bad thing.

My C++ project is getting bigger and bigger so I'm trying to generalize my problem to benefit from the experienced C++ developers out there.

The software I am writing is an XML-parser, that creates UI objects in runtime. I designed ContainerInterface for the different types of containers I want to use. For example TabWidget is a subclassed QTabWidget and it also inherits ContainerInterface. For now, those are AbsoluteWidget, TreeWidget and TabWidget. All having implementations for the following pure virtual functions defined in ContainerInterface:

virtual PushButton* createButton(const QString& label, const QString& define, const QPoint& topLeft, const QSize& size) = 0;

virtual CheckBox* createCheckBox(const QString& label, const QString& define, const QString& header, const QPoint& topLeft, const QSize& size) = 0;

virtual ComboBox* createComboBox(const QString& label, const QString& define, const QString& header, const QPoint& topLeft, const QSize& size) = 0;

virtual Image* createImage(const QString& file, const QString& define, const QPoint& topLeft, const QSize& size) = 0;

virtual Led* createLed(const QString& define, const QString& onColor, const QString& offColor, const QPoint& topLeft, const QSize& size) = 0;

virtual Text* createText(const QString& define, const QString& label, const QPoint& topLeft, const QSize& size) = 0;

So in the parser, I can use the ContainerInterface, for example:

void XmlReader::readCheckBox(ContainerInterface* container, const QString& header)
{
    Q_ASSERT(xml.isStartElement() && xml.name() == "checkbox");
    QXmlStreamAttributes attr = xml.attributes();
    CheckBox* checkBox = container->createCheckBox(getLabel(attr), getDefine(attr), getHeader(attr, header), getTopLeft(attr), getSize(attr));
    m_centralWidget->setUIElement(getDefine(attr), checkBox); //this is why i need a return value anyway
}

This saved me a lot of code and works nice. So i would like the ContainerInterface to also have:

virtual TabWidget* createTabWidget(const QPoint& topLeft, const QSize& size) = 0;

virtual TreeWidget* createTreeWidget(const QStringList& labels, const QPoint& topLeft, const QSize& size) = 0;

And now we come to the part where I'm having a hard time: this would need the implementation of createTabWidget in TabWidget and so on (which is fine because I could have a Tabwidget included in a Tabwidget, itself included in another TabWidget). If I use the same design that I used for the other elements (e.g. CheckBox), this would return a pointer to a new TabWidget:

TabWidget* TabWidget::createTabWidget(const QPoint& topLeft, const QSize& size)
{
    return new TabWidget(topLeft, size);
}

Doing so is giving me a real hard time debugging, so this raises several questions:

  1. Is the upper TabWidget::createTabWidget possible? (without those, it builds fine)
  2. Shall I include the files for the containers e.g. tabwidget.h in the container interface to avoid circular depencies? (this gives me expected class name before '{' token)
  3. Do I need to forward declare TabWidget in TreeWidget then? (this gives me an invalid use of incomplete type error)
tobilocker
  • 891
  • 8
  • 27
  • The XML parser you're looking for comes with Qt (`QUiLoader`), and you can use it at runtime. Why are you writing another one? See [this answer](http://stackoverflow.com/a/19327470/1329652) for a complete example. – Kuba hasn't forgotten Monica Mar 30 '16 at 13:09
  • You seem to have a lot of manually created class-specific factory methods. They are unnecessary. You can leverage the `QMetaObject` machinery to automatically determine the class constructor arguments for a given `QObject` type, and automatically map them to XML. – Kuba hasn't forgotten Monica Mar 30 '16 at 13:13
  • Despite the fact i doubt that anyone could guess all the purposes of parsing an xml to create UI objects, there are so many answers to this. I go with: because it is my job. – tobilocker Mar 30 '16 at 13:16
  • I'm simply making you aware that it exists, and that you could take its code and adapt it to your needs. Qt comes with source, use it :) It's not your job to reinvent the wheel, I'm sure. – Kuba hasn't forgotten Monica Mar 30 '16 at 13:19
  • Ok then i go with the next answer: I am not designing the XML. This is what the person who pays me does, my job is inventing the wheel. Good point anyway, but this gets off-topic. – tobilocker Mar 30 '16 at 13:22
  • But that's what I meant: you have an XML schema, and an existing parser that creates objects from a different schema (the one used in `.ui` files). Where do you start: from scratch, or by at least having a look at the existing parser to see if it could be easy to adapt to your needs? I suggest you at least have a look at how `QUiLoader` is implemented, and perhaps see what design patterns were used there. – Kuba hasn't forgotten Monica Mar 30 '16 at 14:16
  • Using absolute widget positioning, without layouts, is almost always a recipe for disaster. The XML schema may need to be revisited, or you may wish to approximate it with setting minimal widget sizes so that the laid-out interface has same absolute positions and sizes while allowing layouts to deal with system-imposed metric changes (e.g. larger fonts, window resizes, etc.) – Kuba hasn't forgotten Monica Mar 30 '16 at 15:03
  • @KubaOber I really do appreciate your ambition about this question and believe me, i share your worries and i had those discussions with my boss. That's just not the point. If they want red dotted lines to be drawn with blue ink, i do so. Searching for the most scalable, generic platform-independent approach really, really is pointless here. I been through this discussion a lot, believe me. – tobilocker Mar 31 '16 at 09:03

3 Answers3

1

Looks like you're missing a basic concept, and that's the separation of declaration and definition.

Your .h files should contain one class definition. So TabWidget.h should contain class TabWidget etc. The corresponding methods are defined in the .cpp file.

Because of this, TabWidget.h doesn't need the implementation of PushButton. It only uses PushButton*, the pointer. That means the compiler just needs to know that PushButton is a class type : class PushButton;. However, it's quite possible that TabWidget.cpp is calling new Pushbutton, and for that you need to include PushButton.h in TabWidget.cpp.

So you see there are no cyclic dependencies. The dependencies are directional: .cpp files depend on .h files but not vice versa.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • Thanks for pointing out that my given examples can confuse a reader but this is not the case. `Your .h files should contain one class definition` -> they do, `The corresponding methods are defined in the .cpp file` -> they are. Like i said, there is now problem with any objects like `PushButton` and `CheckBox`, but with objects that can contain themselfes, in my case reffered to as `Conatiners` E.g. `TabWidget`. Simply because a `PushButton` having another `PushButton` is pointless, but a `TabWidget` holding a `TabWidget` is not. So, the problem(s) start with those.Everything else works fine – tobilocker Mar 30 '16 at 11:02
  • @tobilocker: Your numbered questions 2 and 3 are definitely not about classes "containing" themselves. But even for those, there are no problems if you keep your class definition in the header and the implementation in the .cpp file. You may insist that's untrue, and the problems really start with those, but there are millions of C++ programmers who have no problem with this. – MSalters Mar 30 '16 at 11:11
  • Actually i am glad to hear this and i expected it to be a common design pattern. That comment is already a great advice because it says that my general idea for a design is possible and common. Probably the flaw shows up somewhere else at the end. Normally i would have done my homework before putting such a general question here, but i never had so many error types on a trial and error approach. So thanks for the answer and i will update the question as soon as i spotted the flaw. – tobilocker Mar 30 '16 at 11:17
0

You are using your ContainerInterface for creating the GUI components, so do not break the concept by adding a createTabWidget() method to the TabWidget class.

Intruduce a parent argument into your create...() methods instead. It can be a nullptr by default.

interface:

virtual TabWidget* createTabWidget(const QPoint& topLeft, const QSize& size, Component* parent = nullptr) = 0;

usage:

// Create a top-level tab widget, parent is null.
TabWidget* outerWidget = container->createTabWidget(position, size);

// Create a child tab widget, set the parent.
TabWidget* innerWidget = container->createTabWidget(position, size, outerWidget);

This solution assumes that all your GUI components (TabWidget, ComboBox, Image, ...) all derived from a common base class, a Component in my example.

Tomas
  • 2,170
  • 10
  • 14
  • They all are derived from `QWidget`, which is good because it allows any form of nesting UI Elements in the containers. But it does not allow me to have a common Base Class as suggested, since e.g. `QTabWidget` and `QTreeWidget` are subclassed `QWidget`s and i do subclass them to `TabWidget` and `TreeWidget`. But it allows me to pass in any type as `QWidget` as a parent (which i actually did for every object) As example, in the implementation of `createCheckBox` in `AbsoluteWidget` (just a `QWidget`) i do: `return new CheckBox(label, define, header, topLeft, size, this);`. – tobilocker Mar 30 '16 at 09:56
  • The thing is, that `Button` and `CheckBox` (and so on) do not inherit `ContainerInterface` so it is straightforward, but `TreeWidget` and `TabWidget` do – tobilocker Mar 30 '16 at 09:57
  • I understand. So, if the `TabWidget` is derived from the `ContainerInterface`, I think it's a good idea (despite what I said before) to have the `TabWidget::createTabWidget` method. Just use the *forward declarations* in your *ContainerInterface.h*, and do not include your gui component headers there. – Tomas Mar 30 '16 at 10:14
  • Oh, and having a pure virtual `createTabWidget` in `ContainerInterface` requires an implementation in `TabWidget` – tobilocker Mar 30 '16 at 10:14
  • Yes, I does. And what's the problem there? – Tomas Mar 30 '16 at 10:25
  • i thought you suggested to not implement it in TabWidget. Since your answer states that the idea of a design like that i possible is already a good help. Pointing out to use the parent, when trying to nest those containers also. But right now, since i have so many issues i will clean up to the point where the interface does not create any containers and then try to figure out what really is the problem (i will update the question then). Thanks for the fast help :) – tobilocker Mar 30 '16 at 10:30
  • Yeah, I suggested not to implement the `TabWidget::createTabWidget()` because I didn't know that you derive your `TabWidget` from the `ContainerInterface`. Now your approach seems to me as a good idea. But I still do not understand where is the major problem, sorry. I would say just: 1. Use the pointers and forward declarations in your *.h* files, especially the *ContainerInterface.h*; 2. Implement methods in your *.cpp* files, where you can include what you want. – Tomas Mar 30 '16 at 10:41
0

this would need the implementation of createTabWidget in TabWidget and so on

That's false. To use pointers and references to most types, you don't need the type's implementation to be visible, nor even the type's declaration, but only a forward declaration.

So, the following is how your project might look:

// ContainerInterface.h
#ifndef ContainerInterface_h
#define ContainerInterface_h
// No includes necessary

class QString;
class PushButton;
class TabWidget;
class ContainerInterface {
public:
  virtual PushButton* createButton(const QString &) = 0;
  virtual TabWidget* createTabWidget(const QString &) = 0;
};

#endif // ContainerInterface_h

// TabWidget.h
#ifndef TabWidget_h
#define TabWidget_h

#include <QTabWidget>
#include "ContainerInterface.h"

class TabWidget : public QTabWidget, ContainerInterface {
  ...
};

#endif // TabWidget_h

// TabWidget.cpp
#include "TabWidget.h" // must always be the first file included!
#include "PushButton.h"

TabWidget * TabWidget::createTabWidget(const QString & foo) {
  ...
}

PushButton * TabWidget::createPushButton(const QString & foo) {
  ...
}

This will compile, but it doesn't mean it's good design. It's quite bad that container interface must know how to create an instance of given type. The ContainerInterface idea seems fundamentally broken and unscalable.

You should flip the problem around:

Have a map of widget creators that map from an XML tag or class to a functor that takes the XML stream and returns a QWidget* pointer. Every concrete widget class simply needs to be registered in that map.

E.g.:

// ItemFactory.h
#ifndef ItemFactory_h
#define ItemFactory_h
#include <QMap>

class QXmlStreamReader;
class ItemFactory {
  QMap<QString, QWidget*(*)(QXmlStreamReader &)> m_loaders;
public:
  QWidget * loadItem(const QString & tag, QXmlStreamReader & reader);
  void registerLoader(const QString & tag, QWidget*(*loader)(QXmlStreamReader &));
  static ItemFactory & instance(); // if you want it a singleton
};

#endif // ItemFactory_h

// ItemFactory.cpp
#include "ItemFactory.h"
Q_GLOBAL_STATIC(ItemFactory, itemFactory)

QWidget * ItemFactory::loadItem(const QString & tag, QXmlStreamReader & reader) {
  auto it = m_loaders.find(tag);
  if (it == m_loaders.end())
    return nullptr;
  return (*it)(reader);
}

void ItemFactory::registerLoader(const QString & tag, QWidget*(*loader)(QXmlStreamReader &) {
  m_loaders.insert(tag, loader);
}

ItemFactory & ItemFactory::instance() {
  return *itemFactory;
}

// CheckBox.h
...
class CheckBox : public QCheckBox {
public:
  ...
  static void registerType();
}

// CheckBox.cpp
#include "CheckBox.h"
#include "ItemFactory.h"

void CheckBox::registerType() {
  ItemFactory::instance().registerLoader(QStringLiteral("CheckBox"), +[](QXmlStreamReader & xml) -> QWidget* {
    Q_ASSERT(xml.isStartElement() && xml.name() == "checkbox");
    auto attr = xml.attributes();
    auto checkBox = new CheckBox(getLabel(attr), getDefine(attr), getHeader(attr, header), getTopLeft(attr), getSize(attr));
    checkBox.setProperty("define", getDefine(attr)); // bundle the define in the widget in a generic way
  });
}

It should be easy then to allow the container to take a pointer to a generic QWidget* and add it, using the widget->property("define").toString() to get the "define" that you presumably need.

Your main problem, as is common with many other questions, is not stating what your purpose is. Yeah, you have an XML file that defines the UI, but that doesn't enforce a particular C++ design, not yet.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Thanks for your lehgthily answer. I have to admit that this might not be a great question nor my coding is very sophisticated. Everything else i would like to say is just pure sarcasm. – tobilocker Mar 31 '16 at 08:54