Skip to main content

Portable GUI with wxWidgets and CMake

Building portable command line applications is fairly straight forward. The only problem you really have to solve is whether to prefix arguments with a "-", or with a "/" if you want to conform with Windows utilities.

However, if you want to make a portable GUI application, your life is quite a bit more complicated. One of my favorites for the way it solves OS 'feel' issues (i.e., GUI equivalent of "-" versus "/" problem), is wxWidgets. By using the platform's native GUI components, it gives the right look and feel of a native application. Furthermore, it has a commercial friendly license.

The Wx-Calc App

In order to demonstrate how to use wxWidgets, we'll make a small calculator program, Wx-Calc.

First, we'll create the directory structure:

$ mkdir -p wx-calc/src
$ cd wx-calc

CMake Setup

For this application we will be using CMake, as our build system.

So we'll create the top-level /CMakeLists.txt:

cmake_minimum_required(VERSION 2.8)
project(WXCALC)

find_package(wxWidgets COMPONENTS core base REQUIRED)

add_subdirectory("src")

The find_package command sets the wxWidgets_USE_FILE variable to the location of the wxWidgets includes, and the wxWidgets_LIBRARIES variable to the wxWidgets library files.

Next lets add the src/CMakeLists.txt to create our binary:

include_directories(.)
include("${wxWidgets_USE_FILE}")

# add resources on windows
if (WIN32)
  set(sources "${sources}; GUI/App.rc")
endif(WIN32)

file(GLOB sources *.cpp)

add_executable(wxcalc ${sources})

target_link_libraries(wxcalc ${wxWidgets_LIBRARIES})

install(TARGETS wxcalc RUNTIME DESTINATION bin)

Finally, let's add an obligatory 'Hello World' main() to our project (Main.cpp):

#include <cstdio>

int main(
    int const argc,
    char const * const * const argv)
{
  printf("Hello world\n");

  return 0;
}

At this point, test that you can compile and run the program.

$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./bin/wxcalc

Start a wxWidgets Application

Next we need to replace the Hello World main function. We'll change the /src/Main.cpp file so that it contains:

#include <wx/wxprec.h>
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
#include <wx/display.h>
#include "WxCalcWindow.hpp"

class App:
  public wxApp
{
  public:
    virtual bool OnInit()
    {
      WxCalcWindow * win = new WxCalcWindow();
      win->Show(true);

      return true;
    }

};


wxIMPLEMENT_APP(App);

The App implements the base wxApp class which the main() generated by the wxIMPLEMENT_APP() macro. Get used to macros, wxWidgets uses lots of them.

At this point, you're probably panicking about the win pointer not getting freed at the end of this function. It gets deleted when Close() is called, which is invoked when a user close the window. Furthermore, it delete all of its child components as well when this happens, so below we'll be using the new keyword without any smart pointers. Don't panic. More details are available here and here.

Next we need to create the WxCalcWindow class we referenced. In /src/WxCalcWindow.hpp we'll put:

#include <wx/wx.h>

class WxCalcWindow :
  public wxFrame
{
  public:
    WxCalcWindow();

    wxDECLARE_EVENT_TABLE();
};

And in /src/WxCalcWindow.cpp we'll put:

#include "WxCalcWindow.hpp"

// declare the window's event table
wxBEGIN_EVENT_TABLE(WxCalcWindow, wxFrame)
wxEND_EVENT_TABLE()

WxCalcWindow::WxCalcWindow() :
  wxFrame(NULL, wxID_ANY, "Wx-Calc", wxPoint(100,100), wxDefaultSize)
{
  // do nothing
}

If you compile and run it now

$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./bin/wxcalc

you should see something that looks like:

blank window

Adding Components

Our window at this point is rather useless. Next lets add some components to our calculator. At the very least, we want a display, and several buttons.

Roughly, we want our calculator to look like this artful Post-It note. blank window

We'll contruct all of the components in the constructor of WxCalcWindow. First we start with a top-level wxBoxSizer, which will align our display and button grid vertically.

  wxBoxSizer * topSizer = new wxBoxSizer(wxVERTICAL);

For the display, we'll use the wxTextCtrl class, and add it to its own horizontal sizer that can stretch it across the screen.

  m_display = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition,
      wxDefaultSize, wxTE_RIGHT);
  // There is a wxTE_READONLY style option, but it still allow selecting
  m_display->Disable();
  // have the text span our window horizontally
  wxBoxSizer * displaySizer = new wxBoxSizer(wxHORIZONTAL);
  displaySizer->Add(m_display, 1, wxEXPAND, 0);

  topSizer->Add(displaySizer, 0, wxEXPAND, 0);

While most of this is straight forward, the arguments to Add() are a bit tricky.

The 1 we pass in sets the proportion, and allows it to expand in the same direction as the sizer (horizontally). The next argument, wxEXPAND, is what actually tells it to expand horizontally.

Then, when we add our horizontal sizer to our vertical sizer, we set the proportion to 0, to prevent it from expanding vertically.

For the buttons we'll start with a wxGridSizer, which aligns elements in row major order.

  // create the button grid with no space between buttons
  wxGridSizer * buttonSizer = new wxGridSizer(4, 0, 0);

The three arguments we pass into the wxGridSizer constructor specify that it will have four columns of elements, and there will be no spacing between elements.

We must then add our elements in row major order to get the correct layout. Which mean we need to add them in the order 1, 2, 3, +, 4, etc.. Because we associate events with button clicks, they're constructors look a bit different.

  // create all number buttons
  m_numButtons.resize(10);
  for (int num = 0; num < 10; ++num) {
    m_numButtons[num] = new wxButton(this, num, std::to_string(num));
  }

  // create operation buttons
  m_addButton = new wxButton(this, ID_ADD_BUTTON, "+");
  m_subButton = new wxButton(this, ID_SUB_BUTTON, "-");
  m_mulButton = new wxButton(this, ID_MUL_BUTTON, "*");
  m_divButton = new wxButton(this, ID_DIV_BUTTON, "/");
  m_clsButton = new wxButton(this, ID_CLS_BUTTON, "CLS");
  m_equalsButton = new wxButton(this, ID_EQUAL_BUTTON, "=");

  // create decimal and plus/minus buttons
  m_deciButton = new wxButton(this, ID_DECI_BUTTON, ".");
  m_signButton = new wxButton(this, ID_SIGN_BUTTON, "+/-");

We give it the parent window in which it will be rendered, the event ID the button should generate, and finally the text of the button.

Then we need to add them in row-major order to our wxGridSizer.

  // the order in which we add determines the place in the grid, so add in
  // row-major order.

  // first row
  for (int num = 1; num <= 3; ++num) {
    buttonSizer->Add(m_numButtons[num], 1, wxEXPAND, 0);
  }
  buttonSizer->Add(m_addButton, 1, wxEXPAND, 0);

  // second row
  for (int num = 4; num <= 6; ++num) {
    buttonSizer->Add(m_numButtons[num], 1, wxEXPAND, 0);
  }
  buttonSizer->Add(m_subButton, 1, wxEXPAND, 0);

  // third row
  for (int num = 7; num <= 9; ++num) {
    buttonSizer->Add(m_numButtons[num], 1, wxEXPAND, 0);
  }
  buttonSizer->Add(m_mulButton, 1, wxEXPAND, 0);

  // fourth row
  buttonSizer->Add(m_numButtons[0], 1, wxEXPAND, 0);

  buttonSizer->Add(m_deciButton, 1, wxEXPAND, 0);
  buttonSizer->Add(m_signButton, 1, wxEXPAND, 0);

  buttonSizer->Add(m_divButton, 1, wxEXPAND, 0);

  // special cls and equals buttons in the last row and last two columns, add
  // two spacers to move them over
  buttonSizer->AddStretchSpacer();
  buttonSizer->AddStretchSpacer();
  buttonSizer->Add(m_clsButton, 1, wxEXPAND, 0);
  buttonSizer->Add(m_equalsButton, 1, wxEXPAND, 0);

  topSizer->Add(buttonSizer, 1, wxEXPAND, 0);

Once we've finished adding them in order, we need to tell the window to adjust its size to its contents.

  SetSizerAndFit(topSizer);

At this point, if we run the application, we should see what looks like a calculator (an ugly one). squashed calculator

Event Handling

To setup the event handling, we need to add an enum containing our event ID, a function to execute when the event is triggered, and the entry to the event table.

enum event_ids {
  ...
  ID_ADD_BUTTON
  ...
}
void WxCalcWindow::onAdd(
    wxCommandEvent& event)
{
  // perform the add oepration
}
// declare the window's event table
wxBEGIN_EVENT_TABLE(WxCalcWindow, wxFrame)
  ...
  EVT_BUTTON(id, WxCalcWindow::onAdd)
  ...
wxEND_EVENT_TABLE()

We'll need to do this for every button. To reduce the number of event functions you create, I favor using/abusing templates.

template<int NUM>
void WxCalcWindow::onNumButton(
    wxCommandEvent&)
{
  // add digit to our number
  ...
}

template<int OP>
void WxCalcWindow::onOpButton(
    wxCommandEvent&)
{
  switch (OP) {
    ...
  }
}

// declare the window's event table
wxBEGIN_EVENT_TABLE(WxCalcWindow, wxFrame)
  EVT_BUTTON(ID_0_BUTTON, WxCalcWindow::onNumButton<ID_0_BUTTON>)
  EVT_BUTTON(ID_1_BUTTON, WxCalcWindow::onNumButton<ID_1_BUTTON>)
  ...
  EVT_BUTTON(ID_ADD_BUTTON, WxCalcWindow::onOpButton<ID_ADD_BUTTON>)
  EVT_BUTTON(ID_SUB_BUTTON, WxCalcWindow::onOpButton<ID_SUB_BUTTON>)
  ...
wxEND_EVENT_TABLE()

If you're writing an application with buttons determined at runtime (think Minesweeper), you'll want to use the wxEventHandler::Bind<>() function for dynamic event handling.

Updating the Display

At this point, the missing piece of the puzzle is updating the display. To do that, we'll use the SetValue() function on the wxTextCtrl.

  // update the display with our internally tracked running total
  m_display->SetValue(std::to_string(m_total));

If you use the GetValue() function to retrieve the value being displayed, you'll need to be aware the wxWidgets uses it's own string class wxString, which has a conversion constructor that takes std::string as a parameter. See the wxWiki page for details on convert wxString to other types.

Finishing Touches

Lets address some cosmetic issues at this point. Our flat buttons give our calculator a squashed appearance. Lets make our buttons nice and square.

  const wxSize buttonSize(64,64);

  // create all number buttons
  m_numButtons.resize(10);
  for (int num = 0; num < 10; ++num) {
    m_numButtons[num] = new wxButton(this, num, std::to_string(num), \
        wxDefaultPosition, buttonSize);
  }

  // create operation buttons
  m_addButton = new wxButton(this, ID_ADD_BUTTON, "+", wxDefaultPosition, \
      buttonSize);
  m_subButton = new wxButton(this, ID_SUB_BUTTON, "-", wxDefaultPosition, \
      buttonSize);
  m_mulButton = new wxButton(this, ID_MUL_BUTTON, "*", wxDefaultPosition, \
      buttonSize);
  m_divButton = new wxButton(this, ID_DIV_BUTTON, "/", wxDefaultPosition, \
      buttonSize);
  m_clsButton = new wxButton(this, ID_CLS_BUTTON, "CLS", wxDefaultPosition, \
      buttonSize);
  m_equalsButton = new wxButton(this, ID_EQUAL_BUTTON, "=", \
      wxDefaultPosition, buttonSize);

  // create decimal and plus/minus buttons
  m_deciButton = new wxButton(this, ID_DECI_BUTTON, ".", wxDefaultPosition, \
      buttonSize);
  m_signButton = new wxButton(this, ID_SIGN_BUTTON, "+/-", wxDefaultPosition, \
      buttonSize);

This should result in a better looking calculator. calculator with square buttons

Our final change will be to prevent the window from being resized, now that it looks OK. To do this, we'll change our call to the wxFrame constructor to add the style argument instead of letting it default to wxDEFAULT_FRAME_STYLE.

WxCalcWindow::WxCalcWindow() :
  wxFrame(NULL, wxID_ANY, "Wx-Calc", wxPoint(100,100), wxDefaultSize, \
      wxCLOSE_BOX | wxCAPTION),
  ...

The wxCLOSE_BOX is what gives us the X in the corner, and the wxCAPTION is what gives us the window title (wxCLOSE_BOX only has effect when used with wxCAPTION).

Further Reading

If you want the details on how to implement this calculator (such as the logic behind the buttons) see the GitHub repository for the full code.

The API of wxWidgets is fully documented, and the wxWiki provides explanations and examples for many of the classes.