Sunday, November 11, 2007

Getting Started with the wxRuby GUI Toolkit

Many readers have asked "What is the best GUI toolkit?". There is no single answer to this question, and I won't try to lobby for my GUI of choice. Use whatever works best for you and your users.

I use the wxRuby library. It's cross-platform, provides a native look-and-feel, and is easy to install (via a gem) and distribute with your application (via RubyScript2Exe). Further features can be found in the wxRuby wiki here. wxRuby is the Ruby interface to wxWidgets, a very stable and widely-used widget toolkit.

Installing wxRuby is simple using RubyGems:


gem install wxruby

Though you can use a GUI designer, creating most forms "by hand" is probably simpler than you suspect. A useful wxRuby user interface can be created in less than 50 lines of code, as we shall see...

Let's create a simple form with a label, text box, combo box, and button. We start by requiring the wx library and including the Wx namespace:

require 'wx'
include Wx

We create a new class which inherits from the Wx::Frame class and includes an initialize method:

class MyFrame < Frame
def initialize()
super(nil, -1, 'My Frame Title')
end
end

The Frame class' super constructor method takes the following arguments (all but Parent are optional):

Parent: The window parent. This may be NULL. If it is non-NULL, the frame will always be displayed on top of the parent window on Windows.

ID: The window identifier. It may take a value of -1 to indicate a default value.

Title: The caption to be displayed on the frame’s title bar.

Position: The window position. A value of (-1, -1) indicates a default position, chosen by either the windowing system or Widgets, depending on platform.

Size: The window size. A value of (-1, -1) indicates a default size, chosen by either the windowing system or Widgets, depending on platform.

Style: The window style (ie, if the minimize, maximize, and close boxes appear on the Frame).

Further details on the Frame class can be found in the wxRuby wiki here.

Next, let's add the code to the initialize method to create a panel, which will contain the other controls:

@my_panel = Panel.new(self)

The self passed to the new constructor is a reference to the Frame object, which we are passing as the parent of the Panel object.

Next, we'll create a variety of controls, passing the newly created @my_panel as the parent of each control:

@my_label = StaticText.new(@my_panel, -1, 'My Label Text',
DEFAULT_POSITION, DEFAULT_SIZE, ALIGN_CENTER)
@my_textbox = TextCtrl.new(@my_panel, -1, 'Default Textbox Value')
@my_combo = ComboBox.new(@my_panel, -1, 'Default Combo Text',
DEFAULT_POSITION, DEFAULT_SIZE, ['Item 1', 'Item 2', 'Item 3'])
@my_button = Button.new(@my_panel, -1, 'My Button Text')

I recommend assigning all your form controls to instance variables. This is the reason for the '@' preceding each control variable name.

We want to bind the button click to a "my_button_click" method which we will add to this class later. We do with the evt_button method:

evt_button(@my_button.get_id()) { |event| my_button_click(event)}

Now we'll proceed to the layout: arranging the controls in the Panel. We'll do this using a BoxSizer, which we'll have arrange the controls vertically:

@my_panel_sizer = BoxSizer.new(VERTICAL)
@my_panel.set_sizer(@my_panel_sizer)

Next we add each control to the panel's sizer by calling the sizer's add method:

@my_panel_sizer.add(@my_label, 0, GROW|ALL, 2)
@my_panel_sizer.add(@my_textbox, 0, GROW|ALL, 2)
@my_panel_sizer.add(@my_combo, 0, GROW|ALL, 2)
@my_panel_sizer.add(@my_button, 0, GROW|ALL, 2)

See the Sizer.Add documentation for an explanation of the various parameters.

Our last line of code in the initialize method makes the frame visible:

show()

Don't forget to add the my_button_click method that is called by the button click:

def my_button_click(event)
# Your code here
end

That concludes our MyFrame class. Now we want to create a MyApp class that will call our MyFrame class:

class MyApp < App
def on_init
MyFrame.new
end
end

Finally, we create a new instance of our MyApp class and call its main_loop method:

MyApp.new.main_loop()

View the complete code here.

There you have it. A complete, but simple, wxRuby form. It's not much to look at...



...but it demonstrates the basics and we'll enhance it shortly.

Soon, we'll cover related topics: dressing up our GUI a little, using the wxSugar extensions, using a Forms designer such as wxFormBuilder.

Questions? Comments? Suggestions? Post a comment here or send me an email message.

Thanks for stopping by!

Digg my article

21 comments:

Allan said...

Don't forget to keep all your objects as global or (preferably) instance variables or the app will crash unexpectedly - like mine did!

David Mullet said...

Good point, Allan!

I have learned to always use instance variables in my form classes, but had not mentioned this. I've added it to this post. Thanks!

David

Anonymous said...

Any idea why I get a 404 when I try to install wxruby using cygwin?

$ gem install wxruby
Select which gem to install for your platform (i386-cygwin)
1. wxruby 1.9.2 (x86-mswin32)
2. wxruby 1.9.2 (powerpc-darwin-8)
3. wxruby 1.9.2 (x86-linux)
4. wxruby 1.9.2 (x86_64-linux)
5. wxruby 1.9.2 (x86-darwin-8)
6. Skip this gem
7. Cancel installation
> 1
ERROR: While executing gem ... (OpenURI::HTTPError)
404 Not Found

Sam. said...

Je rencontre le même problème d'erreur 404, pour la version windows ainsi que la x86-linux.

Je pense qu'il faut attendre un peu (à défaut se savoir à qui signaler le pb)

Sam. said...

Oups !

Sorry, to write in french here.

I said I have the same problem with gems for windows and also for linux. And We just have to wait something to be fixed ;-)

Dongkyu Kim said...

I had a same problem, so I download a gem file from http://rubyforge.org/frs/?group_id=35 and installed.

Matthijs said...

I just downloaded the gem manually, don't know what's wrong with it.

I'ld also like to mention that there is a typo referring to a MainFrame instead of a MyFrame class.

Great article though! Gives me a head start when I want to write a little GUI app.

Anonymous said...

Thanks for a really nice tutorial. Just to mention that in recent wxRuby versions, it's possible to use shorter and more expressive syntax in a few cases.

1) When writing constructors for widgets, you can use named parameters and skip where the default is wanted (eg Wx:DEFAULT_POSITION). So the StaticText constructors could be written:

Wx::StaticText.new(@my_panel,
:label => "My Label Text",
:style => Wx::ALIGN_CENTRE)

similarly for the other widgets

2) In the common case of handling events with a method, there's no need to use a block - a symbol (since 1.9.2) will do; you also don't need to call get_id. The example could be re-written:

evt_button @my_button, :my_button_click

PS - on the downloading problem, a RubyGems update on Rubyforge broke all the wxRuby downloads, but the gems developers are looking into it.

Anonymous said...

Starting from wxRuby 1.9.2, you can even shorten the application startup code :

Wx::App.run do
MyFrame.new
end

No need for defining an Application class and no need for calling main_loop.

Alex F said...

@ David/Allan - I don't think you should need to use instance/global variable scope to avoid crashes, at least in any version in the last six months or so.

If you could send a small sample on the wxruby m.l. or bug trackers, that reproduces a crash when local variables are used for widgets, it would be much appreciated.

Roberto Scaccia said...

Is it an IDE like BOA for python?

Thanks

Vincent said...

When I run this program it tells me no such file to load - wx

I did 'gem install wxruby' and I successfully installed wxruby-1.9.2-x86-mswin32

I am using ruby 1.8.6

can anyone help me?

Miguel said...

Vincent,

You can put in the first line of your program:

require 'rubygems'

or, include de path to the wx library in the RUBYOPT variable.

Matti said...

I found this wxruby tutorial very useful and already played a little with the wxruby. Thanks! When you will discuss in detail wxSugar or wxFormBuilder?

Steve said...

Just this weekend I've decided to look at Ruby again from a GUI development point of view. Good article showing the basics.

Diva said...

nice info for me, just go on with your blog..

Anonymous said...

When I typed in the WX ruby code for the GUI window I get the following errors;

Using Eclipse Ganymede

C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require': 126: The specified module could not be found. - C:/Ruby193/lib/ruby/gems/1.9.1/gems/wxruby-2.0.1-x86-mingw32/lib/wxruby2.so (LoadError)
from C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
from C:/Ruby193/lib/ruby/gems/1.9.1/gems/wxruby-2.0.1-x86-mingw32/lib/wx.rb:12:in `'
from C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:59:in `require'
from C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:59:in `rescue in require'
from C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:35:in `require'
from C:/Documents and Settings/Ruby/workspace/Hello World/Hello/Hello.rb:2:in `'

Not sure why I have a Ruby 1.9.1 sub directory, I intstalled 1.9.3

lago said...

hey I've got a problem when I run it It gives me this error:
C:/Ruby192/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require':
126: No se puede encontrar el m¾dulo especificado. - C:/Ruby192/lib/ruby/gems/
1.9.1/gems/wxruby-2.0.1-x86-mswin32-60/lib/wxruby2.so (LoadError)
from C:/Ruby192/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:i
n `require'
from C:/Ruby192/lib/ruby/gems/1.9.1/gems/wxruby-2.0.1-x86-mswin32-60/lib
/wx.rb:12:in `'
from :33:in `require'
from :33:in `rescue in require'
from :29:in `require'
from giu_test.rb:1:in `'

could someone help me cus I found all those files just where they were suposed to be

Anonymous said...

Ok, I may be being extremely dim here, new to programing, but I can't seem to pass the size parameters into the new MyFrame class without errors. Could somebody show me an example of how to do this?

Thanks!

David Mullet said...

@Anonymous:

Actually, you need to pass the super() method a Size object. The following code excerpt creates a Point object (for position on screen) and a Size object (for width and height), then passes them to the super() method:

x = 50
y = 100
w = 300
h = 400
point1 = Point.new(x, y)
size1 = Size.new(w, h)
super(nil, -1, "My Window Title", point1, size1)

-David

Swapnil - My words in my fashion !! said...

Thanks for breaking it down step-by-step. I spent days testing different ways to create a simple form gui but nothing helped, until I found this.
Please, keep up the great work.