Starting Point
I begin with a simple application, which opens up a frame and displays a menu. I have added an 'open' menu item, which will be responsible for opening an image and displaying it in the frame.
require 'wx'
class ImageFrame < Wx::Frame
def initialize
super(nil, :title => "Image Displayer")
set_menu_bar make_menu_bar
create_status_bar 2
end
def make_menu_bar
# construct menu
menu_bar = Wx::MenuBar.new
# -- menu
image_menu = Wx::Menu.new
image_menu.append(Wx::ID_ABOUT)
image_menu.append(Wx::ID_OPEN, "&Open", "Open an image file")
image_menu.append(Wx::ID_EXIT)
evt_menu(Wx::ID_ABOUT) {about_program}
evt_menu(Wx::ID_OPEN) {puts "open_image"}
evt_menu(Wx::ID_EXIT) {exit_program}
menu_bar.append(image_menu, "&Images")
menu_bar
end
def about_program
Wx::MessageDialog.new(self,
"This program lets you choose an image file\nto display",
"About Program",
Wx::ICON_INFORMATION | Wx::OK).show_modal
end
def exit_program
close
end
end
Wx::App.run do
ImageFrame.new.show
end
File Dialog
The standard dialogs within a graphical library make it easy to add functionality to your application in a format your users will expect. The file dialog is the way to get a filename from the user. Here, the user must identify an image file to open and display.
The general pattern is first to create an instance of the FileDialog object. Second, show the dialog, which returns the ID number of the button the user clicked: we will only act if the dialog returns 'Wx::ID_OK', to indicate that the user clicked the 'OK' button. Finally, we ask the dialog instance for the name of the filename. Here is the code, which should be placed inside the open_image method; for now, it will only display the name of the selected file:
def open_image
fd = Wx::FileDialog.new(self, "Open an image file",
:style => Wx::FD_FILE_MUST_EXIST)
if fd.show_modal == Wx::ID_OK
puts "Selected #{fd.filename}"
end
end
The 'style' keyword tells the file dialog to only accept existing files, so the user cannot type in an unknown filename. For this method to be called, we must make a change to the make_menu_bar method: evt_menu(Wx::ID_OPEN) {open_image}
Displaying an Image
Once we have the filename of an image, we can open and display the image in the frame. The image object is known as a Bitmap, and is created from the path of the image to load. A call to is_ok checks that nothing went wrong, and then the Bitmap can be passed to our display widget. We replace the puts call above with:
begin
image = Wx::Bitmap.new(fd.get_path)
if image.is_ok
@display.set_image image
set_status_text(fd.get_filename, 1)
end
rescue Exception # catch any problems in setting up the bitmap
return
end
The display widget is responsible for showing the image inside our frame. As the image may be larger than the frame size, we will need the widget to have scrollbars: we construct a subclass of ScrolledWindow. Whenever we open an image, we tell the display widget about the new image by calling set_image. Then, the widget updates its scrollbars to the image's width and height, and refreshes its display.
The last bit of code is to handle the actual drawing of the bitmap. This is managed through an on_draw method, which is called everytime the widget must update its display. The on_draw method is called with a pointer to the 'device context' on which it must draw. Our method is very simple: we clear the device, and then draw the bitmap, if we have one.
class ImageDisplay < Wx::ScrolledWindow
def initialize parent
super parent
@image = nil
end
def set_image image
@image = image
set_scrollbars(1, 1, @image.width, @image.height)
refresh
end
def on_draw dc
dc.set_background Wx::WHITE_BRUSH
dc.clear
return if @image == nil
dc.draw_bitmap(@image, 0, 0, true)
end
end
Telling the Frame to Display the Widget
Apart from learning about individual widgets, the user of a GUI library like wxRuby must learn how to display widgets within a frame in the intended places. The way to do this is with 'Sizers'. Each widget has a sizer to manage the widgets within it. The simplest sizers are the BoxSizers, which lay out their children in either a horizontal or a vertical direction. Widgets are usually constructed and added to sizers in the initialize method of their parent.
For the program here, we made a vertical sizer. The widget to display is then added to the sizer. The call sizer.add(@display, 1, Wx::GROW) tells the sizer which widget to add, that it should expand the widget to fill the space, and that the widget should grow as the frame grows around it. Finally, the sizer is set as the frame's main sizer. The final initialize method for ImageFrame is:
def initialize
super(nil, :title => "Image Displayer")
set_menu_bar make_menu_bar
create_status_bar 2
sizer = Wx::BoxSizer.new Wx::VERTICAL
@display = ImageDisplay.new self
sizer.add(@display, 1, Wx::GROW)
set_sizer sizer
end

