In the previous post, we saw how to fire a simple message box from iex. In this blog post, we’ll go deeper (not too much!) and present the elements wxFrame, wxButton, and wxTextCtrl.

The app we’ll build looks like this:

Before clicking the button

Creating a new mix application

Let’s create a new mix application by doing

mix new advance_hello_world
cd advance_hello_world

Exporting Erlang macros as functions

wxErlang is an Erlang binding. Even though we can use it from Elixir, there are a few gotchas that we need to know. One of the issues is that wxErlang defines several (a ton) of constants as Erlang macros that we can’t use directly from Elixir. But there is a trick that we can use, and that mix makes it very simple to do.

Create a src folder in the root of the project and a file named wx_const.erl

mkdir src
touch src/wx_const.erl

In the file add the following Erlang code

-module(wx_const).
-compile(export_all).

-include_lib("wx/include/wx.hrl").

wx_id_any() ->
  ?wxID_ANY.

wx_fontfamily_default() ->
  ?wxFONTFAMILY_DEFAULT.

wx_fontstyle_normal() ->
  ?wxFONTSTYLE_NORMAL.

wx_fontweight_bold() ->
  ?wxFONTWEIGHT_BOLD.

Here, we’re creating a new module in Erlang that exposes functions that return the value of the Erlang macro. Although we can’t call Erlang macros directly from Elixir, we can call functions. So, we can call :wx_const.wx_id_any() which returns whatever value is stored in ?wxID_ANY macro.

wxFrame

With the macros thing out of the way, let’s talk about the object wxFrame. Through the documentation we can know that

A frame is a window whose size and position can (usually) be changed by the user.

It usually has thick borders and a title bar, and can optionally contain a menu bar, toolbar, and status bar.

A frame can contain any window that is not a frame or dialog.

Creating a frame is as simple as calling.

frame = :wxFrame.new(:wx.null(), wx_id_any(), 'Advance Hello World!')

Where wx.null() is used as the parent window because in this case we have none. wx_id_any() is the id of the frame. In this case, we let wxWidgets assign any id. The last parameter is the frame title as a charlist.

wxButton

As you might expect, wxButton allows us to create buttons. Through the documentation we can know that

A button is a control that contains a text string, and is one of the most common elements of a GUI.

As explained in the docs, to create button, we can write the following:

button = :wxButton.new(frame, wx_id_any(), opts)

We tell the button that its parent window is frame.

wxTextCtrl

We’ll add text to our frame using a wxTextCtrl. As per the docs:

A text control allows text to be displayed and edited.

text_label = :wxTextCtrl.new(frame, wx_id_any(), opts)

An example is worth a thousand words

The app we’ll build looks like this:

Before clicking the button

Before clicking the button

After clicking the button

After clicking the button

In the folder lib, open the file named advance_hello_world.ex and replace the content with the following

defmodule AdvanceHelloWorld do
  import :wx_const

  def run() do
    :wx.new()
    
    frame = :wxFrame.new(:wx.null(), wx_id_any(), 'Advance Hello World')

    label = :wxTextCtrl.new(frame, wx_id_any(), [{:value, 'Hello'}, {:size, {150, 70}}])

    font = :wxFont.new(42, wx_fontfamily_default(), wx_fontstyle_normal(), wx_fontweight_bold())

    :wxTextCtrl.setFont(label, font)

    button =
      :wxButton.new(frame, wx_id_any(), [
        {:label, 'Click me!'},
        {:pos, {0, 64}},
        {:size, {150, 50}}
      ])

    :wxButton.connect(button, :command_button_clicked, [
      {:callback, &handle_click/2},
      {:userData, %{label: label}}
    ])

    :wxFrame.show(frame)
  end

  def handle_click({:wx, _, button, %{label: label}, _command}, _event) do
    :wxTextCtrl.setValue(label, 'World')
    :wxButton.setLabel(button, 'Clicked!')
    :wxButton.disable(button)
  end
end

and in mix.exs add :wx as an extra_application

def application do
  [
    extra_applications: [:logger, :wx]
  ]
end

Now start an iex session with the mix project loaded

iex -S mix

And we can run the “advance hello world” app by doing

iex> AdvanceHelloWorld.run()

Amazing!

Understanding the app

Let’s check the code in more detail

def run() do
  :wx.new()
  ...

In the previous blog post, we saw that first we need to create a context by calling :wx.new(). This will store a reference to wxWidgets in the current process dictionary.

frame = :wxFrame.new(:wx.null(), wx_id_any(), 'Advance Hello World')

This creates a new frame, which is a top level element, so we pass :wx.null(). We let wxWidgets assign an id automatically. “Advance Hello World” will be the title of the frame. Remember that strings in Erlang are charlist, not binaries.

Here we can see that the wx_id_any() function is one of the functions we defined in Erlang at the beginning of this post. We are getting the value of the ?wxID_ANY Erlang constant. We are importing all Erlang functions with import :wx_const at the top of the file.

label = :wxTextCtrl.new(frame, wx_id_any(), [{:value, 'Hello'}, {:size, {150, 70}}])

font = :wxFont.new(42, wx_fontfamily_default(), wx_fontstyle_normal(), wx_fontweight_bold())

:wxTextCtrl.setFont(label, font)

This code creates a label with the text “Hello” (as a charlist again) with a font-size of 42, the default font-family, normal font-style and bold font-weight. Notice that we tell the control to use frame as its parent window.

Now the fun part: the button.

def run() do
  ...
  
  button =
    :wxButton.new(frame, wx_id_any(), [
      {:label, 'Click me!'},
      {:pos, {0, 64}},
      {:size, {150, 50}}
    ])

  :wxButton.connect(button, :command_button_clicked, [
    {:callback, &handle_click/2},
    {:userData, %{label: label}}
  ])
  
  ...
end

def handle_click({:wx, _, button, %{label: label}, _command}, _event) do
  :wxTextCtrl.setValue(label, 'World')
  :wxButton.setLabel(button, 'Clicked!')
  :wxButton.disable(button)
end

To create the button, we set frame as the parent window, and we set a few properties: the label text, position, and size.

With :wxButton.connect we attach our button to an event. Every time the button is clicked, it’s going to call the callback handle_click/2. We also store a reference to the label we created earlier, so we can access it later.

handle_click/2 handles the click message, and it has two parameters. The first is the message that contains the element that triggered the callback. In our case, it is the button. And the second one is information related to the event. Notice that in the first parameter, we get the user data we passed when connecting the button to the event.

The first parameter of handle_click has the following structure

{:wx, _id, component that fired the event, user data, wxCommand}

while the second one has this structure

{:wx_ref, _id, :wxCommandEvent, []}

In the example, we change the label content from “Hello” to “World”. We change the button label to “Clicked!” and disable it.

Conclusion

We’ve created a more complex “Hello World” example this time. Now, we know how to create a frame with a label and a button. We attach an event handler to the button, and we update the label and the button text on the click event. Then we disable it.

We exported some constants from Erlang to Elixir by defining a module in the src folder and exporting each constant as a function.

In a follow-up post, we’ll explore the wx_object behavior to capture key events.