Monday, December 3, 2012

Easily Create Graphical User Interfaces in Rhino Python


I have been doing some work for the Taubman College of Architecture Fabrication Lab. I've been working on some Graphical User Interface helper classes for use in Rhino Python. The idea behind these classes is they allow you to create custom, floating dialog user interfaces without having to use a UI creation tool like SharpDevelop.

I thought these would be generally useful so I made them free to download and use. Use the link below to download a ZIP file of everything you'll need.

Download the Code

Getting Started

Simply unzip the file into a directory of your choice. Run the Rhino Python Editor, load one of the example .py files (for instance Circle UI Example.py) and choose "Run Script (no debugging)" from the python editor.

Here are a few examples of UIs you can easily make. These examples are all included in the ZIP file.

Circle UI Example.py

This is the most basic example. The UI is a single numeric control which allows the user to enter the radius for a circle. After the dialog is closed the circle is drawn in the Rhino viewport. This demonstrates the basic setup pattern and how a single variable can be changed in the UI and then used by your script.

Relative Prime UI Example.py

This demonstrates using two numeric input controls and a read-only label field which is updated based on the values in the two spinners. There are also OK, Cancel and Help buttons. This shows how the controls can interact with one another.

All Controls UI Example.py

This sample shows every control that's available and demonstrates some basic techniques to process the controls as the user interacts with them. It shows how to create and use labels, URL link labels, text boxes, read-only text fields, check boxes, buttons, combo boxes, separators, numeric up/dn controls, trackbars, and picture boxes.


Torus Knot UI Example.py

This example goes beyond being educational and is actually somewhat useful! This script creates torus knot curves and polysurfaces and provides an interactive preview in the viewport as the user changes the control values.
The basic idea for this example came from Steve Baer's excellent Starmaker sample. I used it as the pattern for the interactive redraw. However I'm not using the SharpDevelop IDE as Starmaker does.

Basic Steps - An Overview

To make a UI for your script there are just a few things you need to do:
  1. Import the necessary modules into your code. 
  2. Define the variables the UI controls will change.
  3. Write the code to create the UI form. 
  4. Write the code to process the UI controls.
  5. Display the form to the user. 
  6. Work with the updated variable values the UI has altered. 
The idea is that every one of those steps should be easy! Let's start with the simplest examply and see specifically how to accomplish each step. Load Circle UI Example.py into the Python Editor.

The Circle UI Example


This example is trivially simple. Here's the complete script:

Circle Example Details


1. Import the necessary modules into your code. 
First you need to import the Meier_UI_Utility module. This file contains the code to create the form, methods to add controls, and a method to arrange them on the form. You can see the import on line 3, in the code above.


2. Define the variables the UI controls will change.
The next thing to do is define a class which creates and manages the UI variables. That's class CircleUI above (line 16). Everything happens in the __init__ method. Line 19 defines a variable to hold the radius value the UI will change.


3. Write the code to create the UI form. 
Line 21 creates the UIForm object using the Meier_UI_Utility module. You can think of the form as the dialog window (the frame, the title bar, and the close button). This line also creates a panel and docks that into the form. You can think of the panel as an empty surface that will hold the controls. When the form is displayed the panel's controls are what appears inside the window.

From the ui variable you access the form using this code:
ui.form
You access the panel on the form using this code:
ui.form.panel

The next step in building the UI is to add controls to the form's panel. There are methods provided such as addLabel, addTextBox, addPictureBox, etc. It is the parameters which you pass to these methods which describe the control. In the Circle example only two controls are needed - a label control and a numeric up/dn control. The label just shows the name "Radius:". The numeric up/dn allows the user to type in a value and use the up and down arrows to increment or decrement the value.

Here are the two calls to add them:

It is worth carefully looking at each parameter to understand them. First the label - it has the following parameters as defined in the addLabel method of the Meier_UI_Utility.py file.

Note: You should use Meier_UI_Utility.py as your reference for the parameters. Each is fully documented in the file itself as shown below.
Several of these parameters apply to every control. For example name and breakFlowAfter. Others are unique to each control type.

Every control has a name associated with it. You can use the name so your code can "find" the control later. We'll see examples of this in a more complex example. If you don't need to find the control you can simply pass "" for the name. The Circle example doesn't need to find it.

The text parameter is simply the label text - what shows up in the UI. In our case that's passed as "Radius:".
The label text can be any color you like. If you pass None for the color parameter you'll get black text. Otherwise pass an (R,G,B) list like (0, 255, 0) to get a custom color.

The breakFlowAfter parameter controls the layout. This one is important to understand.

Controls are laid out on the form from left to right, top to bottom.

You can think of them as "flowing" onto the form.

If a control does not "break the flow after itself" then the next control will appear directly to its right.

For example, the numeric up/dn control appears directly to the right of the label control. To get that we pass False as the label's breakFlowAfter parameter. Here is our call to addLine and the result:

If instead we passed True to breakFlowAfter the UI would appear like this:
The layout "flow" is broken and the next control begins again on a new line. This is the essence of how you manage the layout on the form. The entire UI for the Torus Knot example was done that way.

Next let's see the code to add the numeric up/dn control. Here's the definition from the Meier_UI_Utility.py file for the addNumericUpDown method - the comments describe the parameters:

The Circle example call to this method looks like this:

The name is passed as "". The next to arguments define the lower and upper limits for allowable values. So this control will allow values between 1 and 50. The next argument is the increment to use when the up and down arrows are clicked. Here the increment is set to 1. The next argument is the number of decimal places to display. This is set to 2. If you want integers to appear you'd pass 0. The next argument is the default value to use - that is what value appears in the control initially. Here we pass the self.radius variable. The next argument is the width - the horizontal size for the control in pixels. Next is breakFlowAfter and True is passed. If there were any additional control they would appear on a new line. In this example it really doesn't matter since there are no more controls.

4. Write the code to process the UI controls. 
The last parameter is an important one. It is called a delegate. A delegate is a method that you write with a specific signature (set of parameters). This method will be called as the user operates the control.

Some control don't have a delegate - because they need no processing. For example the label. Other controls, like the numeric up/dn do.

In this example the delegate is self.Radius_OnValueChange. As the name suggests this is what's called when the radius value changes. Here is the code we provide as that delegate:

The parameters to the method form the standard signature - (self, sender, e). The one we are most concerned with is sender. This is the "sending" control itself, e.g. the radius numeric up/dn control. We use the Value property of that control to get the current value and set our radius variable equal to it.
In this way, as the user operates the control our method gets called and we can store the variable or do any other work we like based on the change. This is a very simple example so the only thing we do is store the value. In more complex user interfaces you'll often do things like enable or disable other controls, or ask the viewports to redraw to reflect the changed value. The other examples - Relatively Prime, All Controls, and Torus Knot all demonstrates this.

The next step is to get the controls onto the form. What's actually happened during these addXYZ... methods is the controls are just accumulated. In order to place them on the form you need to call a method of the form named layoutControls().

That's all we need to create the UI and handle processing the controls.

5. Display the form to the user. 
What's next is displaying it to the user and then processing the results. In this Circle example that's very easy. Here's the code:

First we create the CircleUI object which we defined and store it in a variable named ui. Then we present the UI to the user by calling the Rhino method ShowSemiModal(). We pass in the form from our UI object (ui.form).
That method runs and puts up the dialog. The user can change the controls as well as do some simple work in Rhino itself. For example they can change layers, and create objects. However many operations are restricted while the dialog is up - for example they can't change the selection.

6. Work with the updated variable values the UI has altered.
When the user exits the dialog by pressing the [X] button in the title bar control returns to the script. In this example we simply add the circle to the drawing.

rs.AddCircle(rs.WorldXYPlane(), ui.radius)

Note that we pass the value from the ui object - ui.radius.

That's it. Those basic steps are all that's required to make a GUI and use the results. One you get used to this pattern it's easy to copy/paste code to create more complex UIs.

Other Examples

In this next section we'll look at a few more code snippets from some of the other examples.
The Relative Prime UI Example has two numeric input controls and a read-only label field which is updated based on the values in the two spinners. There are also OK, Cancel and Help buttons.

This example is a little larger and demonstrates compartmentalizing the code a bit more.

This example does some demo processing of the dialog return value. It prints "OK" or "Cancel" depending on how the user closes the dialog. Therefore an extra import module is needed. You can see that DialogResult is imported at the top of the code.
Instead of a single value to update as in the Circle example, this code has two: a and b. For the sake of demonstration this is broken out into its own small class, UIData. This objects holds the two value the UI will change.

It is constructed and then passed in the RelativelyPrimeUI object. As we'll see below that's the one that actually creates the form, adds the controls, and lays them out. This is very similar to the Circle example except the data to manipulate comes from another object. Notice how Main() create the data object then passes it to the RelativleyPrimeUI class which stores it.
The form is created as before. The controls are added as before, however they are broken out into their own method for clarity.

Let's see how this example updates the read-only label when the spinners change. As before there is a delegate passed to the AddNumericUpDown method to process the changes. Inside this delegate is where the processing happens.

Note that we have delegates for the A, B and the Help buttons. We also have a method which updates the UI to report is the current numbers are indeed relatively prime.

The A_OnValueChange() and B_OnValueChange() methods each store the value and then call an UpdateUI() method. This "finds" the read-only control and sets the string appropriately. You can see in the code above it searches using the name of the control. So the read-only name field is assigned the name "readOnlyResult". UpdateUI() uses this name to find it with the code shown above. Note the try/except mechanism - this is needed to prevent problems as the form is constructed. When the A and B spinners are added to the form their ValueChange method can be called as the initial value is set. That happens before the read-only field control is added to the form. When that's the case Find method will not succeed - the control isn't there yet and would crash. To prevent that we use try/except. If the try block fails we simply do nothing.

The delegate for the Help button is shown. This is called whenever the button is pressed. You can see it simply brings up a MessageBox.

One other thing to note here. Buttons which have text parameters "OK" or "Cancel" get special treatment. It is assumed these are meant to close the dialog and return the appropriate return value. So - that's how they are set up to work. A Cancel button also allows the Esc key to close the dialog.

Further Examples

Have a look at All Controls UI Example.py. It shows the correct format for adding every UI control that's supported. The delegates are all used and simple processing code is provided.

The Torus Knot UI Example.py provides a larger sample which supports interactive redraw as the user changes the controls.

Final Thoughts

I sincerely hope you enjoy using these tools. However, this code is provided "as-is". If you leave comments I'll try to address them as I have time.

Known Issues

The following issues are known bugs or limitations:
  • These UI controls are based on Winforms from the Microsoft Windows operating system. Therefore these UIs will not work on Mac versions of Rhino. 
  • The PictureBox control has a vertical spacing issue. The layout code for the FlowLayoutPanel which makes them computes the required space incorrectly. The work around for this is to use a blank label before the PictureBox. Make sure to pass False to the breakFlowAfter parameter. Like this:
                       addLabel("", "", None, False)
                       addPictureBox("picbox1", "./SamplePicture.jpg", True)


6 comments:

  1. Thank you so much, man!
    You have no idea how helpful this material was to me.

    ReplyDelete
  2. Thank you so much! These are perfect examples to get me started with forms and probably cover all the functionality I need for my project.

    ReplyDelete
  3. Hi there, this is great and has encouraged me to now move over from RhinoScript to Python - watch this space!
    Just a question - with the Relatively Prime example, if the labels are different lengths the NumbericUpDown are not aligned. Is there a way of setting a tab position of of the NumericUpDown?
    Or a way of creating columns in these forms?

    ReplyDelete
  4. Hi Adrian,

    Not using what I've provided. If you need more advanced alignment you can use a better layout tool (like SharpDevelop). Moving forward (beyond Rhino 5) I believe ETO is going to be what's used to provide UI support.

    Mark

    ReplyDelete
  5. Wow, All of this looks very promising! Thanks.
    Do you know If I could use python to switch to a custom Key-Mousebutton mapping for viewport navigation?
    Could this be done via script?

    ReplyDelete
    Replies
    1. Hi,

      I'd recommend looking at ETO for your user interface needs. If you Google "Rhino Python ETO" you'll find a number of threads related to it. That's the future of UI as it's recommended and used by McNeel.

      I'm not sure about scripting the mouse interaction.

      Delete