Genotrance

Icon

Random thoughts, ideas and experiences

wxPython Widgets – Part II

So here’s part II of wxPython Widgets. Let’s continue where we left off.

Button

Buttons don’t need much of an introduction. They are everywhere and do a simple task on click. In AppSnap, I have two buttons, one for install and one for uninstall. So let’s create a button.

# Create a button on an existing panel widget
install = wx.Button(parent=panel, label=’Install’, pos=(160, 10))

Next, we bind the button event to a method where we can perform some action when the button is clicked.

# Bind method to button click
wx.EVT_BUTTON(frame, install.GetId(), self.DoInstall)

In my previous post, I mentioned how AppSnap will install all checked applications when the install button is clicked. We already know how to get the list of checked applications from the checklist box. So the method executed on clicking install would get all checked applications and perform the install function on each application. In general, button methods interact more with other widgets in an application than the button itself. In this example, the button method interacts with the checklist box. I’ll skip an example of the button method since there’s nothing much to show anyway.

In AppSnap, I have separate methods for install and uninstall so the button events are bound to different methods. It is possible to bind all buttons to the same method and then find out which button was clicked as follows:-

# Button click method
def OnClick(self, event):

# Get the button object
button_object = event.GetEventObject()

# Get the label of the button object
button_label = button_object.GetLabel()

Now that we have the button label, we can perform a different action for each button. The GetEventObject() method helps us here by giving us the widget object that invoked the event. This allows us to access relevant object properties. Another method of interest is the GetEventType() which gives us the type of event invoked. This method can be used in a method associated with multiple event types and perform different actions based on type. Both these methods are available in all event objects so this is not specific just to buttons.

It is also possible to change a button’s label as follows:-

# Change a button label
install.SetLabel(label=’Done’)

Static Text

Static text widgets are used to display text on the GUI. In AppSnap, I need to display an application’s details such as name, version, brief description, etc when it is checked or selected in the checklist box. I use a bunch of static text widgets in order to achieve this. These are simple to implement and manage. Let’s create one.

# Create static text on an existing panel widget
version = wx.StaticText(parent=panel, pos=(160, 30))

Adding and modifying the string displayed is done as follows:-

# Clear label
version.SetLabel(label=”)

# Set label to Hello
version.SetLabel(label=’Hello’)

In my experiments, I’ve found that wx.StaticText only allows a single line of text. This is kind of painful because you have to create multiple such objects when you want to display a chunk of text. Perhaps there’s a better way to do that but I’ve not gone that far yet.

Hyperlinks

AppSnap displays the website of an application amongst other details. It would be ideal to open the link in the default browser when clicked. Luckily for us, wxPython has a widget just for this.

Let’s create a hyperlink:

# Create hyperlink on an existing panel widget
link = wx.lib.hyperlink.HyperLinkCtrl(parent=panel, pos=(225, 60))

For starters, let’s initialize the link.

# Initialize the hyperlink
link.SetURL(URL=”)
link.SetLabel(label=”)
link.SetToolTipString(tip=”)

The SetURL() method sets the URL that is invoked when the widget is clicked. The actual text displayed can be set using SetLabel(). The SetToolTipString() method controls the string displayed when the mouse is hovered over the widget. In AppSnap, I use all three of these methods to control the display. First, I use SetURL() to set the application URL. I then use SetLabel() to display a trimmed down version of the URL if it is too long. This is because long links do not fit on the panel. If the label has been trimmed, I use SetToolTipString() to display the entire URL when the mouse is hovered over the widget. This is so that the user can see the entire URL that will be opened on click.

# Setup the hyperlink for a short URL
link.SetURL(URL=’http://www.google.com’)
link.SetLabel(label=’http://www.google.com’)
link.SetToolTipString(tip=”)

# Setup the hyperlink for a long URL
link.SetURL(URL=’http://blog.genotrance.com/2006/08/19/wxpython-widgets-part-i/’)
link.SetLabel(label=’http://blog.genotrance.com/…’)
link.SetToolTipString(tip=’http://blog.genotrance.com/2006/08/19/wxpython-widgets-part-i/’)

On clicking the widget, the link is automatically opened in the default browser and the link color changes from blue to purple. Considering I use the same hyperlink widget for all the applications, once a user clicks on the URL for one application, the URLs for all applications show up as visited. Since I didn’t care about marking links as visited, I used the following method to ensure that links always show up in blue.

# Set the visited color as blue
link.SetColours(visited=wx.Colour(0, 0, 255))

Alternatively, one could use the SetVisited() method every time a new application link is to be displayed.

# Set the hyperlink as not visited
link.SetVisited(Visited=False)

Gauge

Nothing beats having a progress bar to inform the user about the current status of an application performing a list of tasks. AppSnap had to have one. If the user selects multiple applications to install or uninstall, the progress bar gives the user an idea of how much still needs to be done. There are two widgets in wxPython for this: wx.Gauge which displays a progress bar on any widget such as a panel and wx.ProgressDialog which is a dialog widget with an embedded gauge and a text message. I used wx.Gauge since I had enough place on the main panel to embed it.

Let’s create a gauge with a maximum value of 1000.

# Create a gauge on an existing panel
gauge = wx.Gauge(parent=panel, range=1000, pos=(175, 120), size=(200, 15))

For starters, I don’t want to show the progress bar. I’ll display it when the user clicks the install or uninstall button. Let’s hide it for now.

# Hide the gauge
gauge.Hide()

When the user clicks install or uninstall, we show the gauge and increment the value of the gauge counter step by step so that it displays the progress as needed. Figuring out the step size is relevant. The step size will be the range (1000) divided by the number of steps to be performed. In AppSnap, an install is two steps: downloading the application installer and running it silently. If the user selects 4 applications, you have a total of 8 steps. So your step size is 1000/8 = 125. After each step, you increment the gauge counter by 125.

# Show the gauge
gauge.Show()

# Increment the gauge value
gauge.SetValue(pos=gauge.GetValue() + stepsize)

Suppose you have 13 steps. In this case, the step size becomes 1000/13 = 76.9 which gets rounded to 76. But 76 * 13 = 988 which is less than 1000. Just to make sure that the progress bar gets to 1000, at the end of all the steps, I call SetValue() again with pos=1000. After a few seconds, I then reset the gauge to pos=0 and hide it so that I can reuse it later.

Some Tricks

Yielding

In wxPython, the main application loop detects all widget events and invokes the corresponding event method. You might make all sorts of modifications to the GUI in your event method. But nothing will get updated until the main loop gets a chance to implement these modifications. Normally, these updates will happen when the event method exits and returns control to the main loop. This may not be feasible in a method that wants to update the GUI, do some processing and then make further changes to the GUI. For this situation, the Yield() method for the application object provides the solution.

On clicking the install button in AppSnap, the gauge is displayed and the first selected application’s details are populated. The application is then downloaded and installed. On completion, the gauge progress is updated. After this, the next application is displayed, downloaded and installed and so forth until all applications are done. The gauge is then hidden and reset. For this to work as expcted, the Yield() method is invoked after every GUI update so that the main loop can apply these updates. Without these Yield() calls, none of the updates would be displayed on the GUI until the method exits.

Calling the method is straight forward.

# Yield for GUI updates to occur
application.Yield()

Using Threads

Once an event method is invoked, the GUI stops responding to further events until the method exits. For some events, this may not be the best user experience. In this case, using Python threads is warranted.

In AppSnap, an application’s details are displayed on selecting it in the listbox. This includes the application version which needs to be obtained from the web. This call can take a while and hanging the GUI until it completes is not acceptable. As a result, a thread is created in the event method so that it can do all the processing in the background. The GUI continues to respond to further events. This can be done as follows:-

# Event method
def OnEvent(self, event):

# Create the thread that will execute the processing method
child = threading.Thread(target=processing_method, args=[arg1, arg2])

# Set thread as a daemon so that we don’t have to wait for it
child.setDaemon(True)

# Start the thread
child.start()

Resizing the Application

One challenge I faced with GUI applications was the resize event. How do you redo your widget layout when the user changes the window size of the application? Setting a fixed the size is one option but it may not work for all applications. The approach I follow is to use simple algebra to do my resizing for me.

Some widgets can be ignored through resize events. For example, the drop down box in AppSnap is always at (10, 10). The criteria for widgets that can be ignored through resize events are:-

  • They have a fixed width and height
  • There’s only widgets that can be ignored through resize events or no widgets at all to the left and on top of the widget

AppSnap does not have many widgets so resizing it is simple. The code below demonstrates how it is done. This method may not scale for complex applications but is useful nonetheless.

# Bind method to frame resize
wx.EVT_SIZE(frame, self.OnResize)

# Frame resize event method
def OnResize(self, event):

# Get the new frame size
frame_size = event.GetSize()

# Resize the top level panel
panel.SetSize(frame_size)

# Resize the checklist box
list.SetSize(size=(150, frame_size.y – 70))

# Resize the outline
outline.SetSize(size=(230, frame_size.y – 63))

Done for Now

Ok, I’m done. This took some time to get together so I hope it was worth it. If you like this approach, do let me know and I can post more wxPython Widget goodness.

Advertisements

Filed under: Programming

4 Responses

  1. simon says:

    Hi,

    With the wx.StaticText labels, I have found that you can implement multiple lines by using the line break character. For example:

    label.SetLabel(“Line 1\nLine 2”)

    Good luck.

  2. Rezwan says:

    How can I reset gauge widget? In my application, the user will download some real time modem data and analyze those data once download completed. I have 3 buttons.

    button1: Start ( to capture some real time data)
    button2: Stop (to stop capturing data)
    button3: Refresh( the user needs to click this button if he/she wants to start whole process again. what I want is to use some kinda progress bar during downloading. Is there any way to reset gauge widget?

    Thanks!

  3. genotrance says:

    As I mentioned in the post, you can set pos=0 to reset the Gauge.

  4. Thanks so for the time you spent putting this together, I was able to look at your examples and get my first ever program to work.
    http://abbottdavid.com/wx_python/wx_ogg_avi.py

    comments, tips very welcome 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Twitter Updates

%d bloggers like this: