WxGUI Programming Howto

From GRASS-Wiki
Jump to navigation Jump to search

The basic rules how to write wxPython-based wxGUI are at the Submitting page in GRASS source codes. This page contains more advanced tips how to write wxGUI source code.

Note that this page should be moved to wxGUI documentation and may be split into more pages.

List of other resources

Isolating different behavior by using wx-like styles

Both wxWidgets and wxPython use styles to set appearance and behavior of widgets and windows.

Different behavior or different sub-widgets of one widget can be distinguished by wx-like styles.

Using signals or events to reduce dependencies

An event tells us that something happened

If you are writing code where two classes (class A and B) interact with each other, you may tend to write code into an inappropriate class. For example, class A (ModuleSearchWidget, in this case) contains this code:

if self.cmdPrompt:
    self.cmdPrompt.SetText(text)
    self.cmdPrompt.SetSelectionStart(pos)
    self.cmdPrompt.SetCurrentPos(pos)
    self.cmdPrompt.SetFocus()

This code is specific to class B (GPrompt). Other users of class A do not specify cmdPrompt variable and do not need this code. However, when changing the class B you must modify also A. Similarly, when reusing the class A for the class C, you need to add a C specific code into the class A. The solution is to replace the code depending on other classes by emitting an event. Here is the example code (the event name tell us what is actually happening):

moduleEvent = gModuleSelected(name = cmd)
wx.PostEvent(self, moduleEvent)

Now, when using class A with B or C, you don't need to modify class A. You just connect A with B or C by binding a handler in B or C to an A's event.

Do not call event handler directly

If you are calling event handler directly (without an event), you should create a method which does the actual work, has proper name and does not require an event. The following caller's code:

self.OnTextChanged(event=None)

would be replaced by:

self.UpdateTextLabel()

Event handler can be a lambda function

If you are using event handlers which only call a function to do the actual work, you will probably realize that your event handlers are very short, probably only one line. To avoid creating of these short functions, you can use lambda functions. The following code:

self.Bind(EVT_TEXT_CHANGED, self.OnTextChanged)
# ...
def OnTextChanged(self, event):
    UpdateTextLabel(event.text)

def UpdateTextLabel(text):
    # ... actual work here

can be replaced by:

self.Bind(EVT_TEXT_CHANGED,
          lambda event: UpdateTextLabel(event.text))
# ...
def UpdateTextLabel(text):
    # ... actual work here

Signals are better than events

Signals (an GRASS API for PyDispatcher library) are usually more flexible and easier to use than wxPython events. However, general ideas about the code design which uses signals is the same. Use them if you don't need to use wxPython events.

Signals and events emitted by a class are part of its interface

Signals and events are part of class' interface.

Signals should be defined in the class' constructor (__init__ function) and properly documented including parameters and the occasion of emit.

If defining new events, define them before the class definition. Newly defined events should be before a class definition (not at the file beginning or in separate file). Note that this does not apply to CommandEvents.

Splitting GUI and logic

You can achieve re-usability by splitting GUI (windows, buttons, colors, ...) and logic (running commands, writing files, data handling, ...). Note, that there also is MVC concept which has three parts logic, data and GUI.

Single responsibility principle

Use single responsibility principle. One class, function or module should provide only one kind of functionality. Beside other positive consequences, this allows definition of clear interfaces. This applies also generally, not only to GUI and logic.

Enhance existing classes if appropriate

If you lack functionality in some class interface, consider to enhance its interface instead of writing new function somewhere. This applies to the cases when the new functionality is more connected to the old class itself than to your new code. Similarly, when you have similar or same code to control some class, you should consider moving this code to the class or to some new general control class.

If you are modifying a class to provide some functionality for you, modify it in a general way which would allow other in similar situation just use some functions/services without modifying a class.

For example, there is a notebook controller class. It controls different kinds of notebooks. User is notified about the change in one notebook page by switching the page or by adding dots into the name of the notebook page. This was formerly done on different places. Now this functionality is provided by notebook controller. So it is on one place and thus the way of highlighting can be changed easily (e.g., according to notebook capabilities).

The GrassInterface concept

There are many methods of different classes which are widely used through the whole wxGUI. The new GrassInterface is a concept which provides commonly needed functionality.

The term interface is a set of method definitions which the object (class) follows in order to cooperate with other objects (classes).

Currently, classes in wxGUI do not have clearly defined interfaces. Also the structure of classes is unclear and may differ because of variability of wxGUI. To address these issues, new concept was introduced. The GrassInterface is a layer between objects (classes) which defines the set of methods to be used to communicate between objects.

Thanks to clearly defined interface it is possible to change the application behavior by changing the class which implements the interface. For example, some functionality in the main GUI such as querying can be used also for wx-based d.mon if the Layer Manager can be replaced by some other class.

The term GrassInterface denotes an interface which contains basic wxGUI functionality. In addition, it denotes concrete classes which either wraps the functionality of existing classes (LayerManagerGrassInterface) or provides their own implementation (StandAloneGrassInterface). In the latter case, the term interface is used more generally.

Alternatively, some classes such as Layer Manager could simply define methods from an interface. However, considering state of wxGUI and Python language, this approach would not actually help in defining clear interface because non-interface methods can be still used by other classes. On the other hand, the GrassInterface classes define only methods from an interface and ensure that other methods will not be called. Moreover, the GrassInterface classes explicitly show what purpose of the interface and this approach creates an intermediate layer which provides a good starting point for creation of other classes which can then more easily replace classes such as Layer Manager on occasions like standalone modules.

In fact, both approaches are combined. The usage of GrassInterface classes may change in the future. For example, Layer Manager can itself have GrassInterface methods. However, the clearly defined interface and the way to test whether a class uses only this defined interface is crucial to any other development.

There are other advantages of GrassInterface, namely it is a way to split application windows and application logic and is is a necessary step for creating interface for plugins.

Other tips

Executing some code later

Sometimes it is needed to do something like show some text or a window for some time and than hide it or close it. To do this time.sleep() function is often used. However, this function prevents Python from doing anything, so the GUI may be unresponsive and in fact, it will be slower. WxWidgets provide convenient way how to call a function after some time --- wx.FutureCall().

Use wx.FutureCall() instead of time.sleep().

Setting environmental variables

Do not use os.environ to set environment variables for one command. Use env parameter of subprocess.Popen() instead.

GUI guidelines

Showing text in statusbar

Dialog shall not show messages in parent frame's statusbar. It is quite uncommon for text in status bar to be updated according to dialog.

Disabling buttons

This is not clear since there are many things to consider: what is comfortable to the user, how difficult is to implement the solution, the visual style of error messages (the red or yellow does not have to be an ideal color for all window themes).

Buttons should not by disabled (grayed out) even if the contents of dialog is invalid. The reason is that user have no clue what to do to enable the button. The better way how to let user know about the not valid content is the message dialog after clicking on the button (e.g., OK button). It is important that message actually tells user which field is incorrect and why.

However, the preferred way is to validate dialog after clicking on the button. If some of the fields is invalid the message must show up somewhere (besides the field or at the top or at the bottom of the dialog). It seems that the best ways is to show the message in some notification area in the top of the dialog.

Alternatively, fields can be validated on-the-fly and some message must show up somewhere. This can be combined with the message dialog but also with the disabled buttons.

The possible exceptions when buttons can be disabled are wizards when it may be clear that user have to fill all the fields. However, there could be some other validation needed such a the length of text in some field and again user does not know what to change.