Orange

Settings and Controls

In the previous section of our tutorial we have just built a simple sampling widget. Let us now make this widget a bit more useful, by allowing a user to set the proportion of data instances to be retained in the sample. Say we want to design a widget that looks something like this:

What we added is an Options box, with a spin entry box to set the sample size, and a check box and button to commit (send out) any change we made in setting. If the check box with "Commit data on selection change" is checked, than any change in the sample size will make the widget send out the sampled data set. If data sets are large (say of several thousands or more) instances, we may want to send out the sample data only after we are done setting the sample size, hence we left the commit check box unchecked and press "Commit" when we are ready for it.

This is a very simple interface, but there is something more to it. We want the settings (the sample size and the state of the commit button) to be saved. That is, any setting we made, after closing our widget (or after going out of Orange application that includes this widget, or after closing Orange Canvas), we want to save so that the next time we open the widget the settings is there as we have left it. There is some complication to it, as widget can be part of an application, or part of some schema in the Canvas, and we would like to have the settings application- or schema-specific.

Widgets Settings

Luckily, since we use the base class OWWidget, the settings will be handled just fine. We only need to tell which variables we will use for the settings. For Python inspired readers: these variables can store any complex object, as long as it is picklable. In our widget, we will use two settings variables, and we declare this just after the widget class definition.

class OWDataSamplerB(OWWidget): settingsList = ['proportion', 'commitOnChange'] def __init__(self, parent=None, signalManager=None): ...

Any setting has to be initialized, and then we need to call loadSettings() to override defaults in case we have used the widget before and the settings have been saved:

self.proportion = 50 self.commitOnChange = 0 self.loadSettings()

Now anything we do with the two variables (self.proportion and self.commitOnChange) will be saved upon exiting our widget. In our widget, we won't be setting these variables directly, but will instead use them in conjunction with GUI controls.

Controls and OWGUI

Now we could tell you how to put different Qt controls on the widgets and write callback functions that set our settings appropriately. This is what we have done before we got bored with it, since the GUI part spanned over much of the widget's code. Instead, we wrote a library called OWGUI (I never liked the name, but could never come up with something better). With this library, the GUI definition part of the options box is a bit dense but rather very short:

box = OWGUI.widgetBox(self.controlArea, "Info") self.infoa = OWGUI.widgetLabel(box, 'No data on input yet, waiting to get something.') self.infob = OWGUI.widgetLabel(box, '') OWGUI.separator(self.controlArea) self.optionsBox = OWGUI.widgetBox(self.controlArea, "Options") OWGUI.spin(self.optionsBox, self, 'proportion', min=10, max=90, step=10, label='Sample Size [%]:', callback=[self.selection, self.checkCommit]) OWGUI.checkBox(self.optionsBox, self, 'commitOnChange', 'Commit data on selection change') OWGUI.button(self.optionsBox, self, "Commit", callback=self.commit) self.optionsBox.setDisabled(1)

We are already familiar with the first part - the Info group box. To make widget nicer, we put a separator between this and Options box. After defining the option box, here is our first serious OWGUI control. Called a spin, we give it place where it is drawn (self.optionsBox), and we give it the widget object (self) so that it knows where the settings and some other variables of our widget are.

Next, we tell the spin box to be associated with a variable called proportion. This simply means that any change in the value the spin box holds will be directly translated to a change of the variable self.proportion. No need for a callback! But there's more: any change in variable self.proportion will be reflected in the look of this GUI control. Say if there would be a line self.proportion = 70 in your code, our spin box control would get updated as well. (I must admit I do not know if you appreciate this feature, but trust me, it may really help prototyping widgets with some more complex GUI.

The rest of the OWGUI spin box call gives some parameters for the control (minimum and maximum value and the step size), tells about the label which will be placed on the top, and tells it which functions to call when the value in the spin box is changed. We need the first callback to make a data sample and report in the Info box what is the size of the sample, and a second callback to check if we can send this data out. In OWGUI, callbacks are either references to functions, or a list with references, just like in our case.

With all of the above, the parameters for the call of OWGUI.checkBox should be clear as well. Notice that this and a call to OWGUI.spin do not need a parameter which would tell the control the value for initialization: upon construction, both controls will be set to the value that is pertained in the associated setting variable.

That's it. Notice though that we have, as a default, disabled all the controls in the Options box. This is because at the start of the widget, there is no data to sample from. But this also means that when process the input tokens, we should take care for enabling and disabling. The data processing and token sending part of our widget now is:

def data(self, dataset): if dataset: self.dataset = dataset self.infoa.setText('%d instances in input data set' % len(dataset)) self.optionsBox.setDisabled(0) self.selection() self.commit() else: self.send("Sampled Data", None) self.optionsBox.setDisabled(1) self.infoa.setText('No data on input yet, waiting to get something.') self.infob.setText('') def selection(self): indices = orange.MakeRandomIndices2(p0=self.proportion / 100.) ind = indices(self.dataset) self.sample = self.dataset.select(ind, 0) self.infob.setText('%d sampled instances' % len(self.sample)) def commit(self): self.send("Sampled Data", self.sample) def checkCommit(self): if self.commitOnChange: self.commit()

You can now also inspect the complete code of this widget. To distinguish it with a widget we have developed in the previous section, we have designed a special icon for it. If you wish to test is widget in the Orange Canvas, put its code in the Test directory we have created for the previous widget, update the Canvas registry, and try it out using a schema with a File and Data Table widget.