Sunday, November 8, 2009

Keeping wxRuby GUI Working with Threads

A big problem with GUI applications is the unresponsive interface, which happens when the program is busy working on a complex task, leaving no time for updating the graphical interface. All graphical programs performing significant work will require at least two working threads: one thread to look after the graphical interface, redrawing the windows and buttons; and one thread to look after the task.

The following program illustrates the problem. The display is a simple menu with a 'start' item on it. Selecting 'start' will begin a thread displaying ten messages into the text control; the task is just to add up a lot of numbers. Although this works, none of the messages appears until the entire 'busy_task' method is complete.
require 'wx'

class MyFrame < Wx::Frame
def initialize
super(nil, :title => "Thread example")

set_menu_bar menubar

@text = Wx::TextCtrl.new(self, :style => Wx::TE_MULTILINE)
@tasks = 0
end

def menubar
menubar = Wx::MenuBar.new
file_menu = Wx::Menu.new
about_item = Wx::MenuItem.new(file_menu, Wx::ID_ANY, "About")
evt_menu(about_item) { @text.append_text "About\n" }
file_menu.append_item about_item
start_item = Wx::MenuItem.new(file_menu, Wx::ID_ANY, "Start")
evt_menu(start_item) { busy_task}

file_menu.append_item start_item

menubar.append(file_menu, "File")
menubar
end

def busy_task
@tasks += 1
tasks = @tasks
10.times do
@text.append_text "running #{tasks} ... #{Time.new}\n"
(1...1000_000).inject :+
end
@text.append_text "Thread #{tasks} done\n"
end
end

Wx::App.run do
MyFrame.new.show
end

Threads and Timers

We can improve on the above code by placing the work within the 'busy_task' method into a thread:

def busy_task
# start task in a separate thread
Thread.new do
@tasks += 1
tasks = @tasks
10.times do
@text.append_text "running #{tasks} ... #{Time.new}\n"
(1...1_00_000).inject :+
end
@text.append_text "Thread #{tasks} done\n"
end
end
end

This will indeed work. The GUI stays responsive, and more than one thread can be active, by selecting the 'start' menu item again. However, you may find the working thread seems to stop doing anything at times; this happens if I switch focus to another window, so the ruby application is running in the background.

The second improvement is to add a Timer. This is created in the initialize method for the frame. The Timer's role is to wake up after a short period of time, and pass processing control to the next thread. In this case, I make the Timer wake up after 10 milliseconds.

def initialize
super(nil, :title => "Thread example")

set_menu_bar menubar

# -- this timer interrupts every 100ms and gets next thread active
# -- if you don't have the timer, the GUI does not update if window
# in background
timer = Wx::Timer.new(self, Wx::ID_ANY)
evt_timer(timer.id) {Thread.pass}
timer.start(10)

@text = Wx::TextCtrl.new(self, :style => Wx::TE_MULTILINE)
@tasks = 0
end

Summary

Worker Threads and a Timer to make sure the Threads are kept active is probably the key to keeping wxRuby's GUI active under large-running tasks, although I expect to have to do some experimenting in future. For better information, Mario Steele has described the principles here and in a tutorial.

1 comments:

  1. Many thanks for this post.... you've solved my problem :P

    Bertini Carlo [WaYdotNET]
    www.waydotnet.com

    ReplyDelete