C++ header-only library to make declarative UIs for wxWidgets.
This overview provides an overview of wxUI
, but is not intented to be a tutorial on wxWidgets
. We assume the reader has working knowledge of wxWidgets
. Great documentation and guides are available at https://www.wxwidgets.org.
In wxUI
, you use wxUI::Menu
to declare the layout of your menu items and the "actions" they call. Similarly, you use wxUI::Sizer
to declare the layout of Controllers for your application.
wxUI::Menu
is a way to lay out menus in a declarative, visual way.
The general concept is you declare a set of structures and then attachTo
a frame.
HelloWorldFrame::HelloWorldFrame()
: wxFrame(NULL, wxID_ANY, "Hello World")
{
wxUI::MenuBar {
wxUI::Menu {
"&File",
// ...
wxUI::Item { "&Example with wxUI...\tCtrl-F", [this] {
ExampleDialog dialog(this);
dialog.ShowModal();
} },
wxUI::Separator {}, wxUI::Item { wxID_EXIT, [this] {
Close(true);
} },
// ...
}
}.attachTo(this);
In wxWidgets
the general paradigm is to create an enumeration of identity ints that you associate with a member, then you would bind, either statically or dynamically, to a function. With wxUI::Menu
the construction of the identify and assocation with a function is handled automatically. By default wxUI::Menu
starts the enumeration with wxID_AUTO_LOWEST
and increments for each item. Take caution if you use these enumerations as it may collide with other ids assocated with the frame.
The top level wxUI::MenuBar
holds a collection of wxUI::Menu
objects. The wxUI::Menu
object consists of a name of the menu, and a collection of "Items", which can be one of wxUI::Item
(normal), wxUI::Separator
, wxUI::CheckItem
, and wxUI::RadioItem
.
Menu Items are generally a name with a handler closure, such as a lambda, or name and id with a closure. Menu Items can also be assocated with wxStandardID
. Many of these like wxID_EXIT
and wxID_HELP
have predefined name, help, and handlers, so declaration with just an ID is allowed.
Handlers are callable items that handle events. The handler can be declared with both no arguments or the wxCommandEvent
argument for deeper inspection of the event.
wxUI::Separator {}, wxUI::Item { "&ExtendedExample...", [this] {
ExtendedExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&MultibindExample...", [this] {
MultibindExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&SplitterExample...", [this] {
SplitterExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&GenericExample...", [this] {
GenericExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&ForEachExample...", [this] {
ForEachExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&Example Item...", [] {
wxLogMessage("Hello World!");
} },
wxUI::CheckItem { "&Example Checked Item...", [](wxCommandEvent& event) {
wxLogMessage(event.IsChecked() ? "is checked" : "is not checked");
} },
Menu items (except Separator
) follow the general pattern:
Items { ID } // for primatives that have a system handler
Items { ID, "Name" }
Items { ID, "Name", "Help" }
Items { ID, Handler }
Items { ID, "Name", Handler }
Items { ID, "Name", "Help", Handler }
Items { "Name", Handler }
Items { "Name", "Help", Handler }
wxUI::Menu
also allows nesting of menus. This allows complicated menus to be composed easily.
wxUI::Menu {
"&Extra", wxUI::Menu {
"Pets",
wxUI::CheckItem { "Cats", [](wxCommandEvent& event) {
wxLogMessage("Cats %s checked", event.IsChecked() ? "are" : "are not");
} },
wxUI::CheckItem { "Dogs", [](wxCommandEvent& event) {
wxLogMessage("Dogs %s checked", event.IsChecked() ? "are" : "are not");
} },
},
// ...
wxUI::Separator {}, wxUI::Item { "&ExtendedExample...", [this] {
ExtendedExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&MultibindExample...", [this] {
MultibindExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&SplitterExample...", [this] {
SplitterExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&GenericExample...", [this] {
GenericExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&ForEachExample...", [this] {
ForEachExample dialog(this);
dialog.ShowModal();
} },
wxUI::Item { "&Example Item...", [] {
wxLogMessage("Hello World!");
} },
wxUI::CheckItem { "&Example Checked Item...", [](wxCommandEvent& event) {
wxLogMessage(event.IsChecked() ? "is checked" : "is not checked");
} },
},
The wxUI::MenuBar
and related objects are generally "lazy" objects. They hold the details of the menu layout, but do not call any wxWidget primatives on construction. When attachTo
a frame is invoked does the underlying logic construct the menu structure.
The basics of wxUI
layout is the Layout. You use a specific type of Layout, with the wxUI::VSizer
(Vertical Sizer or "row") and wxUI::HSizer
(Horizontal Sizer or "column") being the most common. When a Layout is set as the top level, it uses the layout as a sort of "blueprint" for stamping out the UI by constructing the ownership hierarchy and layout.
VSizer {
wxSizerFlags().Expand().Border(),
VSizer {
"Text examples",
// ...
}
.attachTo(this);
In the above example we have constructed a vertical layout sizer that will use a wxSizer
with the wxSizerFlags
set to expand with a default border. Then the first item in the sizer is a second layer sizer with horizontal layout. The wxSizerFlags
are propogated to each layer so the horizontal layout in this example would also be set to expand with a default border. The second sizer would be created as a "named" box horizonal sizer.
A Layout takes a collection of "Items", which can be either additional Layout (to create a tree of Layouts), Controllers, anything that is convertable wxSizer*
. Here is the general form of constructions for Sizers:
Sizer { Items... }
Sizer { SizerFlags, Items... }
Sizer { "Name", Items... }
Sizer { "Name", SizerFlags, Items... }
wxUI
supports 3 flavors of Sizers: wxUI::VSizer
(Vertical Sizers), wxUI::HSizer
(Horizontal Sizers), and wxUI::FlexGridSizer
(Flexible Grid Sizers). Both wxUI::VSizer
and wxUI::HSizer
can be created with a string to create a "named" box.
Note: Because Sizers are intented to be "recursive" data structures, it is possible for a wxUI::VSizer
to contain a wxUI::VSizer
. However, be aware that if an empty wxUI::VSizer
is created with just a wxUI::VSizer
as the argument, we collapse that to be a single wxUI::VSizer
. ie, this:
wxUI::VSizer { wxUI::VSizer { "Current Frame" } }.attachTo(this);
is equivalant to:
wxUI::VSizer { "Current Frame" }.attachTo(this);
One special type of Layout is Generic
. There are cases where you have to construct a wxWindow*
with a parent. This is a case to use Generic
:
VSizer {
wxSizerFlags().Expand().Border(),
Generic {
[](wxWindow* window) {
return new wxButton(window, wxID_ANY, "Generic");
} },
// ...
CreateStdDialogButtonSizer(wxOK),
}
.attachTo(this);
Essentially, you supply a object that converts to wxSizer*
or wxWindow*
, or a closure or function that returns a wxWindow*
when supplied with a wxWindow*
, and it will be inserted into the Layout.
HSplitter
and VSplitter
are special types of Layout objects that take in two Controllers.
VSizer {
wxSizerFlags().Expand().Border(),
VSplitter {
TextCtrl { "This is Left Side.\n" }
.withStyle(wxTE_MULTILINE)
.withSize(wxSize(200, 100)),
HSplitter {
rightUpper = TextCtrl { "This is Right Top.\n" }
.withStyle(wxTE_MULTILINE)
.withSize(wxSize(200, 100)),
Button { "Incr" }
.bind([this]() {
auto original = std::string { *rightUpper } + "\nThis is Right Top.\n";
*rightUpper = original;
}),
} },
// ...
CreateStdDialogButtonSizer(wxOK),
}
.attachTo(this);
Often times you will need to layout several widgets which only are different in their wxWindowID and Name. Or perhaps there are cases where the items to be laid out are dynamic. ForEach
allows you to specify a range of values or std::tuples
that are arguements to a closure that will returns a Controller. These will then be added one at a time.
HSizer {
ForEach {
{ wxART_PLUS, wxART_MINUS, wxART_FIND },
[](auto identity) {
return wxUI::BitmapButton { wxArtProvider::GetBitmap(identity) };
} },
},
Ranges are valid arguments for ForEach
, which allows you to build up complicated layouts at run time.
HForEach(
std::vector<std::tuple<wxWindowID, std::string>> { { wxID_CANCEL, "A" }, { wxID_OK, "B" } } | std::views::filter([](auto s) { return std::get<1>(s) == "B"; }),
[](auto identity, auto name) {
return wxUI::Button { identity, name };
}),
Often times you would be laying out a set of buttons in a horizontal sizer. The HForEach
and VForEach
functions are provided as convenience functions:
HForEach(
std::vector { wxART_PLUS, wxART_MINUS, wxART_FIND },
[](auto identity) {
return wxUI::BitmapButton { wxArtProvider::GetBitmap(identity) };
}),
Controllers are the general term to refer to items that behave like a wxContol
. In wxUI
we attempt to conform a consistent style that favors the common things you do with a specific wxControl
.
HSizer {
"Details",
CheckBox { "Show" },
Choice { "Less", "More" },
TextCtrl { wxSizerFlags(1).Expand().Border(), "Fill in the blank" }
.withStyle(wxALIGN_LEFT),
},
By default Controllers are constructed using the default arguments for position, style, size, etc. Controllers are designed to use Method Chaining to specialize the way the controller is constructed. In the example above we see that wxUI::TextCtrl
is being augmented with the style wxALIGN_LEFT
.
The list of Methods supported by all controllers:
withPosition(wxPoint pos)
: Specifies thepos
of thewxControl
.withSize(wxSize size)
: Specifies thesize
of thewxControl
.withWidthSize(int size)
: Specifies the width ofsize
of thewxControl
.withHeightSize(int size)
: Specifies the height ofsize
of thewxControl
.withStyle(long style)
: Adds the style flags for thestyle
of thewxControl
.withoutStyle(long style)
: Removes the style flags for thestyle
of thewxControl
.setStyle(long style)
: Sets the style flag for thestyle
of thewxControl
.setFont(wxFontInfo)
: Sets the font of thewxControl
.setEnabled(bool)
: Enables or disables thewxControl
.
Controllers support "binding" a function call to their event handlers. When the event for that controller is emitted, the function-like object supplied will be called. You can bind multiple events on a single controller. For convenience, some controllers have default events that will be used if none is supplied.
Button { wxSizerFlags().Border(wxRIGHT), "Left" }
.bind([] { wxLogMessage("Pressed Left"); }),
Button { wxSizerFlags().Border(wxLEFT), "Right" }
.bind([](wxCommandEvent&) { wxLogMessage("Pressed Right"); }),
For convenience the event parameter of the function can be omitted in cases where it is unused.
Often the value of a Controller in a layout needs to be referenced, or sometimes the backing wxWindow
itself needs to be used directly. This could be for reading a currently typed in value in a TextCtrl
, or to change the selection of a Choice
. Controllers support Proxy
objects, a way to get the handle to the underlying wxWindow
that is created for the Controller.
Some Controllers do not support values that are intended to change, such as a Line
, and others can have several values of interest, such as a ComboBox
. Proxy
objects can have several accessors that allow access to these, most commonly called value()
and selection()
(see Supported Controllers for details of each supported Controller). These accessors are proxy objects support get()
and set()
functions, as well as a set of appropriate overloads for the underlying type, allowing more ergonomic interaction with the code. Proxy
also supplies operator*
which reference the most common accessor.
Proxy
supply control()
, which is intended to allow access to the underlying controller. Proxy
overloads operator->
to allow a "natural" syntax for calling functions on the underlying Controller.
As Proxy
objects need to be a named variable that exist outside of a Controller, and require being "attached". This is done with the operator=
, allowing for an ergonomic way to attach Proxy
objects to controls. Accessing a proxy object that has not been attached to a controller will cause an exception to be raised.
class ExtendedExample : public wxDialog {
public:
explicit ExtendedExample(wxWindow* parent);
void Reset();
private:
wxUI::TextCtrl::Proxy mText;
};
ExtendedExample::ExtendedExample(wxWindow* parent)
: wxDialog(parent, wxID_ANY, "ExtendedExample")
{
using namespace wxUI;
VSizer {
mText = TextCtrl { "Hello" }
}
.attachTo(this);
}
ExtendedExample::Reset() {
mText->DiscardEdits();
}
The "Controllers" currently supported by wxUI
:
wxUI | wxWidget | Default Event | Proxy accessors value |
---|---|---|---|
Bitmap |
wxStaticBitmap |
n/a | n/a |
BitmapButton |
wxBitmapButton |
EVT_BUTTON |
n/a |
BitmapComboBox |
wxBitmapComboBox |
EVT_COMBOBOX |
selection -> int value -> std::string default: value |
BitmapToggleButton |
wxBitmapToggleButton |
EVT_TOGGLEBUTTON |
value -> bool default: value |
Button |
wxButton |
EVT_BUTTON |
n/a |
CheckBox |
wxCheckBox |
EVT_CHECKBOX |
value -> bool default: value |
Choice |
wxChoice |
EVT_CHOICE |
selection -> int default: selection |
ComboBox |
wxComboBox |
EVT_COMBOBOX |
selection -> int value -> std::string default: value |
Gauge |
wxGauge |
n/a | range -> int value -> int default: value |
Hypertext |
wxHypertextCtrl |
n/a | n/a |
Line |
wxStaticLine |
n/a | n/a |
ListBox |
wxListBox |
EVT_LISTBOX |
selection -> int default: selection |
RadioBox |
wxRadioBox |
EVT_RADIOBOX |
selection -> int default: selection |
Slider |
wxSlider |
EVT_SLIDER |
value -> int default: value |
SpinCtrl |
wxSpinCtrl |
EVT_SPINCTRL |
value -> int default: value |
Text |
wxStaticText |
n/a | label -> std::string default: label |
TextCtrl |
wxTextCtrl |
EVT_TEXT |
label -> std::string default: label |
Additional "Contollers" should be easy to add in future updates.
From time to time you may need to do some complicated custom wxWidget "controller" construction. Use Custom
"controller" to hook into the construction of the widget tree. A Custom
object is created supplying a function that conforms to the CreateAndAdd
function.
template <typename T>
concept CreateAndAddFunction = requires(T function, wxWindow* window, wxSizer* sizer)
{
function(window, sizer, wxSizerFlags {});
};
An example of how to use could be as follows:
HSizer {
Custom {
[](wxWindow* window, wxSizer* sizer, wxSizerFlags flags) {
for (auto&& title : { "1", "2", "3" }) {
Button { title }.createAndAdd(window, sizer, flags);
}
},
},
},
CreateStdDialogButtonSizer(wxOK),
wxRadioBox
requires a list of strings to operate correctly, so RadioBox
requires a std::vector
of strings. Note, you can provide an empty std::vector
, but a crash may occur if you do so. In addition, because RadioBox
can take in a string as a "caption", a key-value is necessary to prevent char
-arrays from being interpreted as initializer_list<std::string>
.
Button
and BitmapButton
support the setDefault
function which allows you to set them as the default button.