QVINTVS · SCRIBET

XRC-Menüs mit wxWidgets

Auch mit wxWidgets muss man GUIs nicht (allein) im Code schreiben. Problematisch wird es allerdings, wenn es um Menüeinträge geht.

Eigentlich bin ich ja mehr ein Fan von Gtk+ und seinem C++-Interface gtkmm, allerdings ist gtkmm3 bis heute nicht für Windows verfügbar; die letzte verfügbare Windows-Version von gtkmm datiert irgendwo auf 2010. Für GUIs, die unter Windows laufen sollen, ist das etwas suboptimal; daher musste dafür eine Alternative her. So bin ich also bei wxWidgets gelandet, das von sich selbst verspricht, auf allen unterstützten Plattformen nativ auszusehen. wxWidgets ist im Gegensatz zu gtkmm trotz vordergründiger Modularität ziemliche Bloatware; ich kann es auf den Teufel nicht ausstehen, wenn eine Bibliothek meint, sie müsse alle Bedürfnisse des (Desktop-)Programmierers auf einmal abdecken. „Do one thing, and do it well“ ist mir wesentlich lieber. Naja, egal. Die API für die eigentliche GUI-Programmierung ist jedenfalls recht angenehm, insbesondere, seit das Projekt sich langsam von dem übermäßigen Gebrauch von Makros verabschiedet. Irgendwann wird man wohl auch diese neue Template-Library von C++ benutzen. Ahem.

GUIs im Code zu designen ist zwar möglich, und ist für allem für denjenigen, der eine GUI-Bibliothek gerade erst kennenlernt, auch sehr wichtig, um einen Einblick in die grundlegende Funktionsweise der Bibliothek zu erhalten. Sobald man diesen aber erworben hat und vor dem Problem einer größeren Benutzeroberfläche steht, wird das schnell recht unangenehm, da der Code rasch an Umfang zu- und an Übersichtlichkeit abnimmt. Das ist der Punkt, an dem man sich überlegen sollte, einen GUI-Designer zu benutzen.

Für gtk(mm) gibt es Glade, der einen ganz hervorragenden Job tut. Für wxWidgets habe ich keine quelloffene Software gefunden, die sich mit dem freien Glade messen könnte; am nächsten heran kommt der wxFormbuilder, der in einigen Belangen allerdings ziemlich penibel ist. So können als Kindelemente eines wxSplitterWindow etwa ausschließlich wxPanel-Objekte eingesetzt werden, obwohl das vonseiten der Programmbibliothek kein Erfordernis ist. Da dies allerdings auch ohne jegliche Information einfach nicht durchgeführt wird, kommt man rasch zu dem Schluss, es handele sich um einen Bug, obwohl dem gar nicht so ist. Sinnvolle Fehlermeldungen täten dem Programm sehr gut.

wxWidgets akzeptiert das Laden von Code über die XRC-API, die speziell gelinkt werden muss, da sie ein separates Modul (xrc) darstellt. XRC als Format hat eine umfangreiche, aber gut leserliche Dokumentation, die bei Problemen mit dem wxFormbuilder durchaus eine der ersten Anlaufstellen sein sollte. Hat man die GUI einmal designed, kann sie leicht aus der OnInit()-Methode der eigenen App geladen werden:

#include <wx/wx.h>
#include <wx/xrc/xmlres.h>

class MyApp: public wxApp
{
  virtual bool OnInit();
private:

  wxFrame* mp_mainwindow;
};

bool MyApp::OnInit()
{
  wxXmlResource::Get()->InitAllHandlers();
  wxXmlResource::Get()->Load("../ui/mainwindow.xrc");

  mp_mainwindow = wxXmlResource::Get()->LoadFrame(NULL, "mainwindow");

  mp_mainwindow->Show(true);
  return true;
}

IMPLEMENT_APP(MyApp)

Worauf man allerdings besonders achten sollte, ist die Vergabe von Namen bei Menüleisten bzw. ihren einzelnen Einträgen. Ich habe mehrere Stunden verbracht, bis ich verstanden habe, wie ich den einzelnen Menüeinträgen eine ID geben kann, die es mir erlaubt, hinterher aus dem Code auf den Menüeintrag zuzugreifen und ein Event an ihn zu binden. Von GTK+ kommend bin ich wie selbstverständlich davon ausgegangen, dass es sich bei der Menüleiste um ein ganz normales Widget handelt. GTK+ erlaubt es schließlich auch, mehrere Menüleisten über ein Fenster zu verteilen und behandelt sie nicht anders als andere Widgets auch. Nicht so wxWidgets. Schaut man sich die Dokumentation an (was ich natürlich erst getan habe, nachdem ich versucht hatte, Menüs wie jedes andere Widget auch aus dem XRC zu laden), stellt man fest, dass Menüs keine Subklasse von wxWindow sind, mithin also keine Widgets, sondern etwas ganz eigenes. Mehere Menüleisten in einem Fenster werden wohl auch nicht unterstützt.

Irreführend in dieser Hinsicht ist die GUI von wxFormbuilder. Sie erlaubt es, eine ID für einen Menüeintrag festzulegen. Haken bei der Sache: Dieses Feld ist reine Kosmetik und hat keine Auswirkung auf den generierten XRC-Code. Da man Menüeinträge nicht per XRCCTRL() laden kann, kommt man da dann schnell ins schwimmen. Erst, wenn man sich die XRC-Formatdokumentation anschaut wird klar, wie man vorzugehen hat:

Numeric ID of a window or menu item is derived from the name. If the value represents an integer (in decimal notation), it is used for the numeric ID unmodified. If it is one of the wxID_XXX literals defined by wxWidgets (see Stock Items), its respective value is used. Otherwise, the name is transformed into dynamically generated ID.

Das ist wörtlich zu nehmen. Man muss als Namen (also name-Attribut des <object>-Knotens) eine Bezeichnung wie wxID_EXIT benutzen, sodass das resultierende XRC so aussieht:

<object class="wxMenuBar" name="m_menubar1">
  <label>MyMenuBar</label>
  <object class="wxMenu" name="menu_file">
    <label>File</label>
    <object class="wxMenuItem" name="wxID_EXIT">
      <label>_Quit</label>
    </object>
  </object>
</object>

Damit wird dem Menüpunkt FileQuit die ID wxID_EXIT zugewiesen. Ein Eventhandler kann dann wie folgt daran gebunden werden (hier taucht wxID_EXIT wieder auf):

// ...in OnInit()...
mp_mainwindow->Bind(wxEVT_COMMAND_MENU_SELECTED,
                    &MyApp::On_Menu_File_Quit,
                    this,
                    wxID_EXIT);

Will oder kann man nicht (nur) die von wxWidgets vorgegebenen IDs verwenden, hilft das Makro XRCID(), das die von wxWidgets automatisch vergebene ID für einen Menüpunkt mit dem angegebenen Namen herausfindet:

<object class="wxMenuBar" name="m_menubar1">
  <label>MyMenuBar</label>
  <object class="wxMenu" name="menu_file">
    <label>File</label>
    <object class="wxMenuItem" name="wxID_EXIT">
      <label>_Quit</label>
    </object>
    <object class="wxMenuItem" name="custom_menu_entry">
      <label>_Custom stuff</label>
    </object>
  </object>
</object>
mp_mainwindow->Bind(wxEVT_COMMAND_MENU_SELECTED,
                    &MyApp::On_Menu_File_Custom_Entry,
                    this,
                    XRCID("custom_menu_entry"));

Dabei steht "menu_entry_name" für den Namen, den man in wxFormbuilder für den Menüpunkt vergeben kann.

Valete.