1

Context

In many GUI frameworks, it is very common that widgets are placed in a container to arrange them visually in a window. For example, in Gtkmm, a window with a label and a button (side by side) will look like this:

int main(int argc, char *argv[])
{
    auto app = Gtk::Application::create(argc, argv, "no.abstract.factory");

    // Create widgets to appear in the window:
    Gtk::Label label{"A label"};
    Gtk::Button button{"A button"};

    // Create a container to hold widgets and
    // add widgets inside the container at
    // specific locations. This gives control
    // on where widgets will be located in the
    // window when displayed to users.
    Gtk::Grid container;
    container.attach(label, 0, 0, 1, 1);
    container.attach(button, 1, 0, 1, 1);

    // Create the window and add the container
    // to it:
    Gtk::Window window;
    window.add(container);

    window.show_all();

    return app->run(window);
}

To avoid depending too much on the GUI framework, the abstract factory pattern could be used, as described in the book Design Patterns, by Gamma and al. Using this pattern, interfaces (or abstract classes) are created to represent the widgets and constructor calls are hidden behind factory:

IGUIFactory* factory = new GtkmmGUIFactory();  // Or QtGUIFactory() ...

// Later in the code (we now have no idea what is the
// underlying GUI framework) ...

ILabel* label = factory->CreateLabel();
IButton* button = factory->CreateButton();

// ...

With this approach, the application code does not depend on the GUI framework, except for the factory, which means there is minimal work to be done if another GUI framework is to later be adopted.

Question

In the Gtkmm example above not only are widgets created, but they are also added to a container for arrangement in a window. The container is then itself added to the window.

With the abstract factory patten approach, however, with interfaces hiding away all of the GUI framework's details, how can I actually arrange my widgets in the container? If I create an interface to abstract the container, The call to Gtk::Grid::attach will never be accessible because the widgets it takes as its first argument will be of the interface type, not the Gtkmm type. I could also use casting, but that would mean the framework's widget interfaces is derived from publicly and I want to avoid that.

So in this context, how can GUI elements interact with each other once they are created and the GUI handles are lost? For example, how could we re-write the Gtkmm example above but in an abstract way, so as to not depend too much on the framework?

Note: my example is in C++ because it is the language I will ultimately be using, but I don't mind if the answer is not, as long as the answer is clear.

4
+50

You solve this with composition, not inheritance (you can even call it a "facade" if you want to). Excuse broken syntax, I haven't written C++ for a while...

class IGrid {
  public:
    void attachLabel(ILabel *label, <other parameters>) = 0;
}

class GtkmmGrid : public IGrid {
  public:
    GtkmmGrid(Gtk::Grid underlying) : _underlying(underlying) { }

    void attachLabel(ILabel *label, <other parameters>) {
      _underlying.attach(label, <other parameters>)
    }

  private:
    Gtk::Grid _underlying;
}

Next, add IGrid* createGrid() to your abstract factory, and you can now do:

IGrid *grid = factory->createGrid();
ILabel *label = factory->createLabel();

grid.attachLabel(grid, <other parameters>);

In theory, you are now insulated from changes to your GUI framework. In practice, this never happens - your other GUI framework might take different parameters on the attachLabel call, or in might not even have the concepts of grids and labels. This to me is a clear case of YAGNI - start worrying about this problem when you actually want to support a second GUI framework and know what the shared abstractions between the two frameworks are. At the moment, you risk writing a lot of code which doesn't actually bring you any value in the long run.

What is even more likely in my experience is that you never change to a second GUI framework.

6
  • How does this line work: _underlying.attach(label, <other parameters>) with label being of type ILabel? Gtk::Grid::attach needs a Gtkmm type, like Gtk::Label.
    – BobMorane
    Sep 11 at 16:10
  • 2
    The simplest is to downcast, however much you may dislike that as an LSP violation. If you want to avoid it, you need to go to a higher level of abstraction again, build up an abstract model of the GUI and have a way of translating that abstract model into each concrete GUI implementation. I would again say "YAGNI". Sep 11 at 16:35
  • Downcasting is fine in this situation because you don't need to adjust the code every time a new sub-type is added.
    – Daniel T.
    Sep 14 at 11:38
  • @DanielT. Well, it means if you pass in the wrong type of ILabel you get an exception which means you need to handle that somehow, and that's a pain... but I still agree it's the most practical solution to this. Sep 14 at 11:42
  • 1
    Think about it though. In this context, in order to pass in the wrong subtype, you would have to be using two different GUI systems at the same time and freely mixing them. In this case, you want to get the exception and the only reasonable way to handle it is to change the calling code. There is no notion of "handling" the exception.
    – Daniel T.
    Sep 14 at 11:49
2

So in this context, how can GUI elements interact with each other once they are created and the GUI handles are lost?

They can't. The 'contract' of an AbstractGrid that can attach an AbstractLabel essentially states that it can deal with any abstract label type.

You could use a generic approach, but that will get very messy, very quickly.

class AbstractGrid<LabelType> {
    void attach(LabelType *label) { ... }
}

A different approach:

Instead of going through a set of abstract interfaces to manipulate the GUI framework, you implement a conceptual UI model. A GtkmmUI monitors this model and mirrors it in the actual GUI.

class Grid {
    void attach(Label *label) { ... }
    void registerLabelAttachedCallback(LabelAttachedCallback *cb) { ... }
}

class Label {
    string text();
    void registerTextChangedCallback(TextChangedCallback *cb) { ... }
}

This is essentially MVVM on a very low level - since it involves detailed UI concepts, such as labels. Of course you can also go for a higher-level model, leaving more decisions to the translation code, or for an analogous approach with an MVU design.

The essential theme of all these options is that you abstract over the whole UI, not its individual components. I expect that this will also make it easier to deal with mismatches in different GUI frameworks.

1

Create an interface to abstract a generic widget of the GUI framework, e.g. IWidget.

Then, use composition and store the handle to the actual widget. One idea would be to use precompilation directives to change the type of this handle, if you want to change the GUI engine later.

class IWidget {
  public:
    BASE_WIDGET_TYPE* _widgetHandle; // BASE_WIDGET_TYPE is resolved to a concrete 
                                     // base widget type from the GUI engine, in pre-compilation
}
class ILabel : public IWidget {
  ...
}
class IButton : public IWidget {
  ...
}

In OP's example, grid.attach expects a generic Gtk::Widget, so you would write the abstracted method like:

class GtkGrid : public IGrid {
  public:
    GtkGrid(Gtk::Grid grid) : _grid(grid) { }
    void Attach(IWidget* widget, <other parameters>);

  private:
    Gtk::Grid _grid;
}

//Implementation
void GtkGrid::Attach(IWidget* widget, <other parameters>) {
  _grid.attach(widget->_widgetHandle, <other parameters>); // No cast required
}

Please note that no cast is required here, since the actual base widget type is set during pre-compilation, and can change if needed. This assumes you won't need two GUI engines at the same time, at runtime.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.