Getting Started with wxWidgets in Erlang


(DRAFT - 2011-04-10)
Michael Turner
Tama Translation Systems

Introduction

Erlang is a programming language used mostly on the server side, although at least one popular graphics-intensive application, Wings 3D, has been written in it. The best-supported graphics API for Erlang is wxWidgets, a large, mature, stable multi-platform API for GUI programming.

This tutorial shows you how to use basic wxWidgets in Erlang.

Table of Contents

  1. The How and Why of This Tutorial
    1. Keep Those Fingers Moving
    2. Objection? Overruled!
  2. Setting Up
    1. Find the wx header files
    2. Read wx module definitions
    3. Initialize wx
  3. Frames: A Taste
    1. Make a frame
    2. Recovering from shell exceptions
    3. Frames: creative destruction
  4. The Status Bar
    1. Make a Status Bar
    2. Set the Status Bar Text
  5. The Menu Bar
    1. Make a Menu Bar
    2. Set the Menu Bar for a Frame
  6. Menus
    1. Make a Menu
    2. Add a Menu Item
    3. Make a Help Menu
  7. Events
    1. Using connect to see events
    2. Menu selection events
    3. Callbacks
  8. Dialog boxes
    1. Making a modal dialog
    2. Showing a modal dialog

The How and Why of This Tutorial

This tutorial's approach is highly interactive: you make each new wxWidget call work at the Erlang shell command line, first. You see it work, immediately. Only afterward do you add it to a program. For example:

To put some text in the status bar, type this at the command line:

wxFrame:setStatusText(F, "Quiet here.").

Now you should see this:

An example step of the tutorial

This approach is better for learning GUI programming. Here's why:

Keep Those Fingers Moving

Do you want to get the most out of this tutorial? Do you have a computer-readable copy of it, on your computer running Erlang? Then here's the most important thing: don't copy-paste lines from the tutorial into the shell. Would you try to learn to speak a language by recording other people's spoken sentences, then playing them back? Where the tutorial says to type lines in, type them in.

It can be hard to resist the temptation to copy-paste. Print lessons out and work through the hardcopy, or get them in book form (e-book or dead-tree.) This helps in another way: It leaves more screen space. At times, you'll need it. You'll have an Erlang shell window, some GUI elements displayed (one would hope so!), and often a program editor. Sometimes you'll want to see them all at the same time.

Objection? Overruled!

Of course there are drawbacks to this approach. Nothing's perfect. Some likely objections:

Ask yourself: how would you be learning otherwise? By staring at turgid sample code? Flipping through cryptic manual pages? Reading a wordy book that too often describes what it could be showing you? You probably already know how well that works.

Learning this way is more fun. It will seem, at least, like it's not taking very long. It might also mean you learn faster even in real time. (For one thing, you'll probably procrastinate less.)

There will be times with wxWidgets when it will be no fun at all. So why not get started the fun way?

Why not get started right now?

Setting Up

Unlike systems designed specifically for rapid prototyping of GUIs, like Tcl/Tk, there's some preparation required to start doing graphics from the Erlang shell command line. This section leads you through the steps. In this part, it's assumed you that you already

  • have Erlang installed properly (see Appendix A [TBD]);
  • have learned how to use the Erlang shell;
  • have the use of a text editor appropriate for writing programs.
  • To get started, prepare your Erlang shell session to make understanding easier.

    Find the wx header files

    Seeing is believing - but only if you can see things clearly. Not all wx calls will give you clear graphical results; In the Erlang shell session, you'll often see only return values. These values can be cryptic, especially if you're not used to Erlang. Since Erlang is a functional language, every wx call returns a value. Many of these values will be tuples . Many of those tuples will be the contents of records . It's easier to understand wx return values shown in record format. The Erlang shell needs to be told the wx record definitions, however. So: where are those record definitions?

    Find where wx lives on your system. Type this:

    My_wx_dir = code:lib_dir(wx).
    On Windows XP, My_wx_dir might look something like this:
    "c:/PROGRA~1/ERL57~1.2/lib/wx-0.98.2"

    Read wx module definitions

    Read the wx record definitions from that directory:
    rr (My_wx_dir ++ "/include/wx.hrl").
    rr (My_wx_dir ++ "/src/wxe.hrl").
    Both rr calls should return a list of modules. If you get the rr calls wrong, you'll get empty lists back.

    Open your text editor, if you haven't already. Paste the three commands above to a scratch file. Later, when you start a new lesson, or resume one, or when you need to restart because of a crash, you can copy-paste them into the shell, Try this now, to make sure it works. Quit Erlang, restart it, then copy-paste those commands into the shell.

    Unfortunately, we can't use Erlang macro definitions in the shell. That's another good reason to locate the wx.hrl: to look up wx macro symbol values as needed for shell commands.

    Initialize wx

    To start wx up, type this:

    Wx=wx:new().
    Did it work? Maybe there's a message saying SMP (symmetric multiprocessing) isn't enabled, or "SMP emulator required." In some Windows Erlang releases, SMP isn't enabled by default. Exit the Erlang shell and restart it with a "-smp" flag, e.g., with a DOS command like this:
    "C:\Program Files\erl5.8.1.1\bin\werl.exe" -smp
    then try again.

    When wx:new() works, you'll get a record back, something like this:

    #wx_ref{ref = 0,type = wx,state = []}
    These are only one per process, and the Erlang shell is a process. Ignore the value for now.

    Frames: A Taste

    In wxWidgets, a window is just a kind of frame. Let's make a simple one, then add to it.

    Make a frame

    To make a frame, type this:

    F=wxFrame:new(Wx, -1, "Hello, World!").

    (The second parameter can be an integer ID. The -1 tells wxWidgets to assign the frame some ID by default.)

    Now, where is this new frame? We got a record back, something like this:

    #wx_ref{ref = 35,type = wxFrame,state = []}
    But nothing changed elsewhere on the screen. Why not? Because you must ask to see the frame. Type this:
    wxFrame:show(F).
    This should return true. Now you should be able to see something like this:

    Recovering from shell exceptions

    To get rid of the frame, you could just click the close control. But first try this nonsense call instead:

    nothing:doing().
    That made the frame disappear, along with the exception error message. This is because wxWidgets runs the graphics in its own process, and exceptions not caught in the shell kill any process started there.

    This kind of total GUI loss could happen a lot, just from keying errors. You'd have to start over whenever you made an exception-raising error. You don't want to type everything in from the beginning, when it happens. So, before you do anything else in a shell session, give the command

    catch_exception (true).
    You'll probably take breaks from this tutorial anyway. Set things up so that you can take up where you left off. Paste what you've typed above into the scratch file. The commands so far should now look like this:
    catch_exception (true).
    My_wx_dir = code:lib_dir(wx).
    rr (My_wx_dir ++ "/include/wx.hrl").
    rr (My_wx_dir ++ "/src/wxe.hrl").
    Wx=wx:new().
    F=wxFrame:new(Wx, -1, "Hello, World!").
    wxFrame:show(F).

    Frames: creative destruction

    With the frame displayed again, type this:

    wxFrame:destroy(F).
    It should return ok and the frame should disappear. Now, just for fun, make some new frames. Scroll back through the commands, and, by editing them:
    1. Make a wxFrame F1 with title "Hey!"
    2. Show F1.
    3. Make a wxFrame F2 with title "Boo!"
    4. Show F2.
    5. Then wxFrame:destroy them both.
    Don't bother pasting those last few commands into your scratch file, though. We'll be building from where we left off with first destroy call.

    The Status Bar

    A status bar is handy not just for program features, but also debugging. At this point, you might want to quit and restart. Paste your scratchpad contents into the shell.

    Make a Status Bar

    Type this in:

    wxFrame:createStatusBar(F).

    Your frame should now look like this:

    Set the Status Bar Text

    Put some text in the status bar:

    wxFrame:setStatusText(F, "Quiet here.").

    You should now see this:

    Take a moment to add these commands to your scratchpad startup. Try setting the status to some other string values, then back to "Quiet here."

    You might also try this:

    SB = wxFrame:getStatusBar(F).
    wxStatusBar:pushStatusText(SB, "A LITTLE LOUDER NOW.").
    wxStatusBar:popStatusText(SB).

    You should end up with the same status bar text you started with.

    The Menu Bar

    Frames in wxWidgets conventionally have one menu bar. In this way, the menu bar is like the status bar. However, menu bars are usually made up of other things: They need to be composed of menus.

    In wxWidgets, complex things are usually composed from the bottom up. The wxWidgets API makes no assumptions about what goes inside what. For the more complex things, it takes more steps to make anything you can see.

    Let's get a menu bar visible as soon as possible. Consider copying each command below to your scratchpad/restart file, as you go.

    Make a Menu Bar

    Type this:

    MenuBar = wxMenuBar:new().

    You'll get something like this back:

    #wx_ref{ref = 37,type = wxMenuBar,state = []}
    But you see no menu bar in the window frame. Nothing changed in the window. What could be the problem?

    Well, did we say how this menu bar relates to any frame? You know where you want it to go. But wx can't read your mind. Yes, F is your only window frame so far. But wx doesn't assume you want to put MenuBar in F.

    Set the Menu Bar for a Frame

    Try this: making MenuBar a part of wxFrame F.

    wxFrame:setMenuBar (F, MenuBar).
    Maybe that returns ok but ... still you see nothing! Did anything actually happen? There's a way to ask about a frame's menu bar. Type this:

    wxFrame:getMenuBar (F).

    What do you get back in the shell? It's the same wx_ref returned from the setMenuBar call. Something's happening. The problem is: there are no menus to show anyway. Let's do something about that.

    Menus

    It'll take a few steps to add a menu item and display it.

    Make a Menu

    Most application GUIs have a File menu. Type this:

    FileMn = wxMenu:new().

    You should get something like this:

    #wx_ref{ref = 37,type = wxMenu,state = []}

    Again, wxWidgets doesn't know where you want this menu to go. You have to say which menu bar FileMn will attach to. Add (append) your FileMn to the wxMenuBar of wxFrame F. Use the conventional File menu label.

    wxMenuBar:append (MenuBar, FileMn, "&File").

    (The ampersand before 'F' means that entering Alt-F will be like clicking on the menu.)

    You should see a change -- something like this:

    But what good is a menu without menu items? Click on the File menu (or press Alt F). Nothing happens.

    [Well, the status bar clears. What's with that?]

    Add a Menu Item

    Every File menu has a Quit menu item. Let's make one. Type this:

    Quit = wxMenuItem:new ([{id,400},{text, "&Quit"}]).

    (The menu item ID -- 400 here -- must be specified; without one, wxWidgets won't generate a default, and you won't get a menu item. CHECK THIS.)

    Again, you won't see any graphical change, just a return value in the shell. A menu is like a menu bar: you need to add something to make it visible.

    wxMenu:append (FileMn, Quit).

    Click on the File menu (or type Alt F). You should see this:

    Reviewing all the commands for the simple menu you've made, you should see:

    MenuBar = wxMenuBar:new().
    wxFrame:setMenuBar (F, MenuBar).
    FileMn = wxMenu:new().
    wxMenuBar:append (MenuBar, FileMn,"&File").
    Quit = wxMenuItem:new ([{id,400},{text, "&Quit"}]).
    wxMenu:append (FileMn, Quit).

    What else can we add? Every decent app has a Help menu. And every Help menu should have an About item.

    Make a Help Menu

    Recycle your new and append commands above.

    HelpMn = wxMenu:new().
    wxMenuBar:append (MenuBar, HelpMn, "&Help").

    You should see something like this:

    Now add the About menu item. Recycle the commands you used to add the Quit item to the File Menu. You'll put together commands like this (note the different id value):

    About = wxMenuItem:new ([{id,500},{text,"About"}]).
    wxMenu:append (HelpMn, About).

    Click on the Help menu. You should see something like this:

    Take a moment to copy-paste the above commands to your scratchpad.

    Events

    Up to now, we've done nothing with events. You might think there are no events to see. If you entered "flush()" now, you'd see nothing. But in fact, every mouse click is triggering events inside wxWidgets. They are being handled in some default manner by wx. Often, the wx default is to ignore them. Let's catch an event, to see what it looks like.

    Using connect to see events

    Type this:

    wxFrame:connect (F, close_window).

    Click on the close control for the frame, then type this:

    flush().

    You should get back something like this:

    Shell got {wx,-202,{wx_ref,35,wxFrame,[]},[],{wxClose,close_window}}

    Note that clicking the close control doesn't close the window frame anymore. You've overridden the default action.

    Click on that control a few times, then minimize and maximize the window. Try calling flush again. You see your close_window events, but no minimize/maximize events.

    Also note how the Erlang shell writes out received events: It doesn't use the wx record definitions read in earlier. You're just seeing raw tuples. This makes it harder to know what these wx events are about.

    There's a way to see the events using the record definitions. The following fun returns an event sent to the shell (or empty, if none). Type it in:

    Ev = fun() -> receive E->E after 0 -> empty end end.

    Click on the close control again, then call the event-reader.

    Ev().

    You should see something like this:

    #wx{id = -202,
        obj = #wx_ref{ref = 35,type = wxFrame,state = []},
        userData = [],
        event = #wxClose{type = close_window}}
    

    Menu selection events

    Let's try connecting to more events. Type this:

    wxFrame:connect (F, command_menu_selected).

    Now try some menu selections. Select the File menu's Quit, then the Help menu's About. Then enter Ev() to look at the events generated. Except for id, the resulting events are the same.

    #wx{id = 400,
        obj = #wx_ref{ref = 35,type = wxFrame,state = []},
                      userData = [],
                      event = #wxCommand{type = command_menu_selected,
    				     cmdString = [],
    				     commandInt = 0,
    				     extraLong = 0}}
    

    Callbacks

    It's good to know that events are happening. Sometimes it's helpful to be able to see the details. But most of the time, we just want (non-default) things to happen in response to events. So we have to catch events and figure out what to do with them.

    Events in wx are handled in callback functions. First, let's make a function to call. Type this:

    Ding = fun (_,_) -> wx_misc:bell() end.

    Try it, using the types of parameters expected for these callbacks:

    Ding(#wx{},#wx_ref{}).

    Does it ring a bell? It should.

    Now connect it to your wxFrame F through the event close_window

    wxFrame:connect (F, close_window, [{callback, Ding}]).

    Try clicking the close control on the frame again. It should just beep. Try calling Ev. It should no longer return close_window events.

    Because simply beeping is a non-sensical thing to do for the close control, you might want to disconnect the event now.

    wxFrame:disconnect (F, close_window).

    Dialogs

    An "About" menu item really ought to take us to a modal dialog. But how do you make one? Here's the simplest way.

    Making a modal dialog

    Type the following line:

    D = wxMessageDialog:new (F, "Let's talk.").

    This should return with a value like

    #wx_ref{ref = 43,type = wxMessageDialog,state = []}
    but will show nothing graphically.

    Showing a modal dialog

    To show the dialog and interact with it, type this:

    wxMessageDialog:showModal (D).

    Somewhere on your screen, you should now see something like this:

    Because the dialog is modal, the showModal call won't return with a value in the shell until you hit the "OK" button. The return value should be 5100. If you look at the include file wx.hrl, you'll see that this is the value for wxID_OK.