source: orange/docs/extend-widgets/rst/basics.rst @ 11593:6edc44eb9655

Revision 11593:6edc44eb9655, 12.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Updated Widget development tutorial.

Line 
1###############
2Getting Started
3###############
4
5
6The tutorial on these pages is meant for those who are interested in
7developing widgets in Orange. Orange Widgets are components in
8Orange's visual programming environment. They are wrappers around some
9data analysis code that provide graphical user interface
10(GUI). Widgets communicate, and pass tokens through communication
11channels to interact with other widgets. While simplest widgets
12consist of even less than 100 lines of code, those more complex that
13often implement some fancy graphical display of data and allow for
14some really nice interaction may be over 1000 lines long.
15
16On this page, we will start with some simple essentials, and then
17show how to build a simple widget that will be ready to run within
18Orange Canvas, our visual programming environment.
19
20*************
21Prerequisites
22*************
23
24Each Orange widget belongs to a category and within a
25category has an associated priority. Opening Orange Canvas, a visual
26programming environment that comes with Orange, widgets are listed in
27a toolbox on the left:
28
29.. image:: widgettoolbox.png
30
31The widgets and categories to which they belong are discovered at Orange
32Canvas startup leveraging setuptools/distribute and it's `entry points
33<http://pythonhosted.org/distribute/setuptools.html#dynamic-discovery-of-services-and-plugins>`_
34protocol. In particular Orange Canvas looks for widgets using a
35`orange.widgets` entry point.
36
37
38First we will examine an existing widget in Orange. The Test Learners
39widget which is implemented in `OWTestLearners.py
40<http://orange.biolab.si/trac/browser/orange/Orange/OrangeWidgets/Evaluate/OWTestLearners.py>`_.
41
42Here is its header::
43
44    """
45    <name>Test Learners</name>
46    <description>Estimates the predictive performance of learners on a data set.</description>
47    <icon>icons/TestLearners1.svg</icon>
48    <priority>200</priority>
49    """
50
51OWTestLearners is a Python module, so the header information we
52show about lies within the comment block, with triple quote opening
53and closing the comment. Header defines the name of the widget, its
54description, the name of the picture file the widget will use for an
55icon, and a number expressing the priority of the widget. The name of
56the widget as given in the header will be the one that will be used
57throughout in Orange Canvas. The description of the widget is shown
58once mouse rests on an toolbox icon representing the widget. And for
59the priority: this determines the order in which widgets appear in the
60toolbox within a category.
61
62Widgets communicate. They use typed channels, and exchange
63tokens. Each widget would define its input and output channels in
64something like::
65
66    self.inputs = [("Test Data Set", ExampleTable, self.cdata),
67                   ("Learner", orange.Learner, self.learner, 0)]
68    self.outputs = [("Evaluation Results", orngTest.ExperimentResults)]
69
70
71We will go over the syntax of channel definitions later, but for
72now the following is important:
73
74   - Widgets are defined in a Python files.
75   - Widgets are registered through entry points and are discovered at
76     runtime.
77   - A python module implementing a widget starts with a header. This, given
78     in sort of XMLish style, tells about the name, short description,
79     location of an icon and priority of the widget.
80   - The sole role of priority is to specify the placement (order) of widgets
81     in the Orange Canvas toolbox.
82   - Somewhere in the code (where we will learn later) there are two lines
83     which tell which channels the widgets uses for communication. These,
84     together with the header information, completely specify the widget as it
85     is seen from the outside.
86
87.. note::
88   Orange caches widget descriptions to achieve a faster startup,
89   but this cache is automatically refreshed at startup if any change
90   is detected in widgets' file.
91
92***********
93Let's Start
94***********
95
96Now that we went through some of the more boring stuff, let us now
97have some fun and write a widget. We will start with a very simple
98one, that will receive a data set on the input and will output a data
99set with 10% of the data instances. We will call this widget
100`OWDataSamplerA.py` (OW for Orange Widget, DataSampler since this is what
101widget will be doing, and A since we prototype a number of this widgets
102in our tutorial).
103
104But first we must create a simple `python project`_ layout called *Demo*,
105that we will use in the rest of this tutorial.
106
107.. _`python project`: http://docs.python.org/2/distutils/examples.html#pure-python-distribution-by-package
108
109The layout should be::
110
111   Demo/
112         setup.py
113         orangedemo/
114                     __init__.py
115                     OWDataSamplerA.py
116
117and the :download:`setup.py` should contain
118
119.. literalinclude:: setup.py
120
121Note that we declare our *orangedemo* package as containing widgets
122from an ad hoc defined category *Demo*.
123
124Following the previous example of OWTestLearners, our module defining
125the OWDataSamplerA widget starts with a following header::
126
127    <name>Data Sampler</name>
128    <description>Randomly selects a subset of instances from the data set</description>
129    <icon>icons/DataSamplerA.svg</icon>
130    <priority>10</priority>
131
132This should all be clear now, perhaps just a remark on an icon. We
133can put any name here, and if Orange Canvas won't find the
134corresponding file, it will use a file called Unknown.png (an icon
135with a question mark).
136
137Orange Widgets are all derived from the class OWWidget. The name of
138the class should match the file name, so the lines following the
139header in our Data Sampler widget should look something like::
140
141    import Orange
142    from OWWidget import *
143    import OWGUI
144
145    class OWDataSamplerA(OWWidget):
146
147        def __init__(self, parent=None, signalManager=None):
148            OWWidget.__init__(self, parent, signalManager)
149
150            self.inputs = [("Data", Orange.data.Table, self.data)]
151            self.outputs = [("Sampled Data", Orange.data.Table)]
152
153            # GUI
154            box = OWGUI.widgetBox(self.controlArea, "Info")
155            self.infoa = OWGUI.widgetLabel(box, 'No data on input yet, waiting to get something.')
156            self.infob = OWGUI.widgetLabel(box, '')
157            self.resize(100,50)
158
159In initialization, the widget calls the :func:`__init__` method
160of a base class. Widget then defines inputs and outputs. For input,
161this is a *Data* channel, accepting tokens of the type
162:class:`Orange.data.Table` and specifying that :func:`data` method will
163be used to handle them. For now, we will use a single output channel
164called "Sampled Data", which will be of the same type
165(Orange.data.Table).
166
167Notice that the types of the channels are specified by a class;
168you can use any class here, but if your widgets need to talk with
169other widgets in Orange, you will need to check which classes are
170used there. Luckily, and as one of the main design principles,
171there are just a few channel types that current Orange widgets are
172using.
173
174The next four lines specify the GUI of our widget. This will be
175simple, and will include only two lines of text of which, if nothing
176will happen, the first line will report on "no data yet", and second
177line will be empty. By (another) design principles, in an interface
178Orange widgets are most often split to control and main area. Control
179area appears on the left and should include any controls for settings
180or options that your widget will use. Main area would most often
181include a graph, table or some drawing that will be based on the
182inputs to the widget and current options/setting in the control
183area. OWWidget make these two areas available through its attributes
184:obj:`self.controlArea` and :obj:`self.mainArea`. Notice
185that while it would be nice for all widgets to have this common visual
186look, you can use these areas in any way you want to, even disregarding one
187and composing your widget completely unlike the others in Orange.
188
189As our widget won't display anything apart from some info, we will
190place the two labels in the control area and surround it with the box
191"Info".
192
193In order to complete our widget, we now need to define how will it
194handle the input data. This is done in a method called :func:`data`
195(remember, we did introduce this name in the specification of the
196input channel)::
197
198    def data(self, dataset):
199        if dataset:
200            self.infoa.setText('%d instances in input data set' % len(dataset))
201            indices = orange.MakeRandomIndices2(p0=0.1)
202            ind = indices(dataset)
203            sample = dataset.select(ind, 0)
204            self.infob.setText('%d sampled instances' % len(sample))
205            self.send("Sampled Data", sample)
206        else:
207            self.infoa.setText('No data on input yet, waiting to get something.')
208            self.infob.setText('')
209            self.send("Sampled Data", None)
210
211The :obj:`dataset` argument is the token sent through the input
212channel which our method needs to handle.
213
214To handle the non-empty token, the widget updates the interface
215reporting on number of data items on the input, then does the data
216sampling using Orange's routines for these, and updates the
217interface reporting on the number of sampled instances. Finally, the
218sampled data is sent as a token to the output channel with a name
219"Sampled Data".
220
221Notice that the token can be empty (``None``), resulting from either
222the sending widget to which we have connected intentionally emptying
223the channel, or when the link between the two widgets is removed.
224In any case, it is important that we always write token handlers
225that appropriately handle the empty tokens. In our implementation,
226we took care of empty input data set by appropriately setting the
227GUI of a widget and sending an empty token to the output channel.
228
229
230Although our widget is now ready to test, for a final touch, let's
231design an icon for our widget. As specified in the widget header, we
232will call it :download:`DataSamplerA.svg <DataSamplerA.svg>` and will
233put it in `icons` subdirectory of `orangedemo` directory.
234
235With this we cen now go ahead and install the orangedemo package. We
236will do this by running :code:`python setup.py develop` command from
237the `Demo` directory.
238
239.. note::
240   Depending on your python installation you might need
241   administrator/superuser privileges.
242
243For a test, we now open Orange Canvas. There should be a new pane in a
244widget toolbox called Demo. If we click on this pane, it displays an
245icon of our widget. Try to hover on it to see if the header and channel
246info was processed correctly:
247
248.. image:: samplewidgetontoolbox.png
249
250Now for the real test. We put the File widget on the schema (from
251Data pane) and load the iris.tab data set. We also put our Data
252Sampler widget on the scheme and open it (double click on the icon,
253or right-click and choose Open):
254
255.. image:: datasamplerAempty.png
256
257Now connect the File and Data Sampler widget (click on an output
258connector of the File widget, and drag the line to the input connector
259of the Data Sampler). If everything is ok, as soon as you release the
260mouse, the connection is established and, the token that was waiting
261on the output of the file widget was sent to the Data Sampler widget,
262which in turn updated its window:
263
264.. image:: datasamplerAupdated.png
265
266To see if the Data Sampler indeed sent some data to the output,
267connect it to the Data Table widget:
268
269.. image:: schemawithdatatable.png
270
271Try opening different data files (the change should propagate
272through your widgets and with Data Table window open, you should
273immediately see the result of sampling). Try also removing the
274connection between File and Data Sampler (right click on the
275connection, choose Remove). What happens to the data displayed in the
276Data Table?
277
278*****************************************
279Testing Your Widget Outside Orange Canvas
280*****************************************
281
282When prototyping a single widget, for a fast test I often get
283bored of running Orange Canvas, setting the schema and clicking on
284icons to get widget windows. There are two options to bypass this. The
285first one is to add a testing script at the end of your widget. To do
286this, we finished Data Sampler with::
287
288    if __name__=="__main__":
289        appl = QApplication(sys.argv)
290        ow = OWDataSamplerA()
291        ow.show()
292        dataset = Orange.data.Table('iris.tab')
293        ow.data(dataset)
294        appl.exec_()
295
296These are essentially some calls to Qt routines that run GUI for our
297widgets. Notice that we call the :func:`data` method directly.
Note: See TracBrowser for help on using the repository browser.