×
Namespaces

Variants
Actions

Archived:PySymbian basic user interface app framework

From Nokia Developer Wiki
Jump to: navigation, search

Archived.pngArchived: This article is archived because it is not considered relevant for third-party developers creating commercial solutions today. If you think this article is still relevant, let us know by adding the template {{ReviewForRemovalFromArchive|user=~~~~|write your reason here}}.

Featured Article
22 Feb
2009
Article Metadata
Code ExampleTested with
Devices(s): Nokia E71, Nokia N95
Compatibility
Platform(s): S60 3rd Edition
S60 3rd Edition (initial release)
Platform Security
Signing Required: Self-Signed
Capabilities: None
Article
Keywords: appuifw
Created: marcelobarrosalmeida (27 Jan 2009)
Last edited: hamishwillee (28 Jun 2012)


Contents

Introduction

The API for writing applications using Python for S60 is very straightforward. Once you have decided what kind of body your application will use (canvas, listbox, or text), you need to define your main menu, application title, and default callback handler for exiting. The user interface (UI) is based on events, and there is a special mechanism that relies on a semaphore for indicating if the application is closing. The user must obtain the semaphore and wait for any signal on it. Once signaled, the application may close properly. The UI allows tabs as well, but in this case a body must be defined for each tab. A routine for handling tabs changing is required in such a situation.

However, if your application has more than one window, meaning more than one set of body, menu, and exit handler, you will need to change these elements each time a new window is displayed, giving the user the impression that s/he is using a multiple-window application. Although this solution is possible, some problems arise:

  • The strategy for exiting, based on semaphores, must be unique across your application.
  • You cannot make any mistakes when switching the UI, that is, the set body+menu+exit handler must be consistently changed.
  • There must be a unified strategy for blocking the UI when a time-consuming operation is pending. For instance, when downloading a file, you may want to disable all menu options, otherwise they will be available for the user during the download operation.


For unifying this process, three additional classes are suggested in this article. The first, called Window, is responsible for holding UI contents such as menu, body, and exit handler, and properly exchanging all UI elements for the derived classes. Moreover, it may lock/unlock the UI when necessary. The second class is named Application. It represents the running application itself and is responsible for handling the termination semaphore. Only one Application class must be instantiated per application. The third class is called Dialog. As its name suggests, it is in charge of showing/hiding dialogues when necessary. Many dialogues are allowed, each with its own set of body+menu+exit handler.

Application and Dialog inherit from Window the content handler ability, while each has different ways of finishing itself (finishing the application or just the dialogue).

Code examples

All behaviours described in the previous section are implemented for the script window.py, given below, where Window, Application, and Dialog are depicted.

# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
 
from appuifw import *
import e32
import key_codes
 
__all__ = [ "Application", "Dialog" ]
 
class Window(object):
""" This class is responsible for holding UI contents like menu, body
and exit handler and exchanging properly all UI elements for
the derived classes. Moreover, it may lock/unlock the UI when necessary.
"""

__ui_lock = False
 
def __init__(self, title, body, global_menu = None, exit_handler = None):
""" Creates a new window given a title, body or a set of bodies (for tabbed
window) and optional global_menu and exit handler. Global menu is
available for all tabs at bottom or it is used as the default
menu for non tabbed window
 
The list of bodies must have the following format:
[(tab_text, body, menu),(tab_text, body, menu),...]
 
where:
tab_text: unicode string used in tab
body: a valid body (Listbox, Text or Canvas)
menu: typical menu
"""

self.title = title
 
if isinstance(body,list):
self.tabbed = True
self.bodies = body
self.body = None
else:
self.tabbed = False
self.bodies = None
self.body = body
 
if global_menu is None:
global_menu = [(LABELS.loc.wi_info_exit, self.close_app)]#"LABELS.loc.wi_info_exit" may should be u"Exit" or something else
 
if exit_handler is None:
exit_handler = self.close_app
 
self.global_menu = global_menu
self.exit_handler = exit_handler
self.last_tab = 0
 
def set_title(self,title):
" Sets the current application title "
app.title = self.title = title
 
def get_title(self):
" Returns the current application title "
return self.title
 
def bind(self, key, cbk):
" Bind a key to the body. A callback must be provided."
self.body.bind(key,cbk)
 
def refresh(self):
" Update the application itens (menu, body and exit handler) "
if self.tabbed:
app.set_tabs([b[0] for b in self.bodies],self.tab_handler)
app.activate_tab(self.last_tab)
self.tab_handler(self.last_tab)
else:
app.set_tabs([], None)
app.menu = self.global_menu
app.body = self.body
app.title = self.title
app.exit_key_handler = self.exit_handler
 
def tab_handler(self,idx):
" Update tab and its related contents "
self.last_tab = idx
self.body = self.bodies[idx][1]
self.menu = self.bodies[idx][2] + self.global_menu
app.title = self.title
app.menu = self.menu
app.body = self.body
 
def run(self):
" Show the dialog/application "
self.refresh()
 
def lock_ui(self,title = u""):
""" Lock UI (menu, body and exit handler are disabled).
You may set a string to be shown in the title area.
"""

Window.__ui_lock = True
app.menu = []
app.set_tabs([], None)
app.exit_key_handler = lambda: None
if title:
app.title = title
 
def unlock_ui(self):
"Unlock UI. Call refresh() for restoring menu, body and exit handler."
Window.__ui_lock = False
 
def ui_is_locked(self):
"Chech if UI is locked or not, return True or False"
return Window.__ui_lock
 
class Application(Window):
""" This class represents the running application itself
and it is responsible for handling the termination semaphore.
Only one Application class must be instantiated per application.
"""

__highlander = None
__lock = None
 
def __init__(self, title, body, menu = None, exit_handler = None):
""" Only one application is allowed. It is responsible for starting
and finishing the program.
run() is override for controlling this behavior.
"""

if Application.__highlander:
raise "Only one Application() allowed"
Application.__highlander = self
 
if not Application.__lock:
Application.__lock = e32.Ao_lock()
 
Window.__init__(self, title, body, menu, exit_handler)
 
def close_app(self):
""" Signalize the application lock, allowing run() to terminate the application.
"""

Application.__lock.signal()
 
def run(self):
""" Show the the application and wait until application lock is
signalized. After that, make all necessary cleanup.
"""

old_title = app.title
self.refresh()
Application.__lock.wait()
# restore everything !
app.set_tabs( [], None )
app.title = old_title
app.menu = []
app.body = None
app.set_exit()
 
class Dialog(Window):
""" This class is in the charge of showing/hiding dialogs when necessary.
Many dialogs are allowed, each one with their own set of body+menu+exit
handler.
"""

def __init__(self, cbk, title, body, menu = None, exit_handler = None):
""" Create a dialog. cbk is called when dialog is closing.
Dialog contents, like title and body need
to be specified. If menu or exit_handler are not specified,
defautl values for dialog class are used.
"""

self.cbk = cbk
self.cancel = False
Window.__init__(self, title, body, menu, exit_handler)
 
def close_app(self):
""" When closing the dialog, a call do self.cbk() is done.
If this function returns True the dialog is not refreshed
and the latest dialog/window takes control. If something fails
during callback execution, callback function should return False
and does not call refresh(). Using self.cancel it is possible
to determine when the dialog was canceled or not.
"""

if self.cbk() == False:
self.refresh()
 
def cancel_app(self):
""" Close the dialog but turn the cancel flag on.
"""

self.cancel = True
self.close_app()

Usage examples

Using this framework, user attention is focused on the application and not on appuifw issues. Consider this first example, with only one window (no dialogues). Since your program inherits from Application, default actions for some specific appuifw are already defined:

  • The termination semaphore is controlled by the Application class, being properly initialized inside its constructor (Application.__lock = e32.Ao_lock()). Only one Application object per program is allowed.
  • exit_default_handler is set to Application.close_app(), where the termination semaphore is set (Application.__lock.signal()).
  • Application.run() initializes the application, calling refresh() for updating the UI and waits for the termination semaphore. When signalized, all necessary cleanup is done and the application is closed.


We can see one simple example in action:

# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
 
from window import Application
from appuifw import Listbox, note
 
class MyApp(Application):
def __init__(self):
items = [ u"Option A",
u"Option B",
u"Option C" ]
menu = [ (u"Menu A", self.option_a),
(u"Menu B", self.option_b),
(u"Menu C", self.option_c) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
 
def check_items(self):
idx = self.body.current()
( self.option_a, self.option_b, self.option_c )[idx]()
 
def option_a(self): note(u"A","info")
def option_b(self): note(u"B","info")
def option_c(self): note(u"C","info")
 
if __name__ == "__main__":
 
app = MyApp()
app.run()

First example in action:

MBA BFUI Example1.jpg

Of course, if you need special actions, such as a confirmation when exiting, this can be implemented simply by overriding close_app():

# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
 
from window import Application
from appuifw import Listbox, note, popup_menu
 
class MyApp(Application):
def __init__(self):
items = [ u"Option A",
u"Option B",
u"Option C" ]
menu = [ (u"Menu A", self.option_a),
(u"Menu B", self.option_b),
(u"Menu C", self.option_c) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
 
def check_items(self):
idx = self.body.current()
( self.option_a, self.option_b, self.option_c )[idx]()
 
def option_a(self): note(u"A","info")
def option_b(self): note(u"B","info")
def option_c(self): note(u"C","info")
 
def close_app(self):
ny = popup_menu( [u"No", u"Yes"], u"Exit ?")
if ny is not None:
if ny == 1:
Application.close_app(self)
 
if __name__ == "__main__":
 
app = MyApp()
app.run()

Second example, with customized exit function:

MBA BFUI Example2.jpg

Dialogues

Suppose that the user wants to add a dialogue call. Here is a simple solution that also hides all appuifw issues.

# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
 
from window import Application, Dialog
from appuifw import Listbox, note, popup_menu, Text
 
class Notepad(Dialog):
def __init__(self, cbk, txt=u""):
menu = [(u"Save", self.close_app),
(u"Discard", self.cancel_app)]
Dialog.__init__(self, cbk, u"MyDlg title", Text(txt), menu)
 
class MyApp(Application):
def __init__(self):
self.txt = u""
items = [ u"Text editor",
u"Option B",
u"Option C" ]
menu = [ (u"Text editor", self.text_editor),
(u"Menu B", self.option_b),
(u"Menu C", self.option_c) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
 
def check_items(self):
idx = self.body.current()
( self.text_editor, self.option_b, self.option_c )[idx]()
 
def text_editor(self):
def cbk():
if not self.dlg.cancel:
self.txt = self.dlg.body.get()
note(self.txt, "info")
self.refresh()
return True
 
self.dlg = Notepad(cbk, self.txt)
self.dlg.run()
 
def option_b(self): note(u"B","info")
def option_c(self): note(u"C","info")
 
def close_app(self):
ny = popup_menu( [u"No", u"Yes"], u"Exit ?")
if ny is not None:
if ny == 1:
Application.close_app(self)
 
if __name__ == "__main__":
 
app = MyApp()
app.run()

Notepad added to menu:

MBA BFUI Example31.jpg

Notepad in action:

MBA BFUI Example32.jpg

Notepad data:

MBA BFUI Example33.jpg

When a dialogue is created, a callback function needs to be defined. This callback is called when the user cancels or closes the dialogue. Inside the callback body, it is possible to check if the dialogue was canceled just by verifying the cancel variable. Dialogue variables may be accessed in a similar way as in any other Python object and this is the way of retrieving dialogue data.

The callback function must return either True or False, before finishing. If it returns True, self.refresh() must be called before, inside callback body. This way, the menu, body and exit handler will be updated using the context of the dialogue caller (MyApp, in this case). If it returns False, self.refresh() is called inside dialogue context and the dialogue is restored. This is an excellent way to check dialogue data and to avoid data loss. A better example with this feature is given below (see the number_sel() method).

# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
 
from window import Application, Dialog
from appuifw import Listbox, note, popup_menu, Text
 
class Notepad(Dialog):
def __init__(self, cbk, txt=u""):
menu = [(u"Save", self.close_app),
(u"Discard", self.cancel_app)]
Dialog.__init__(self, cbk, u"MyDlg title", Text(txt), menu)
 
class NumSel(Dialog):
def __init__(self, cbk):
self.items = [ u"1", u"2", u"a", u"b" ]
Dialog.__init__(self, cbk,
u"Select a number",
Listbox(self.items, self.close_app))
 
class MyApp(Application):
def __init__(self):
self.txt = u""
items = [ u"Text editor",
u"Number selection",
u"Option C" ]
menu = [ (u"Text editor", self.text_editor),
(u"Number selection", self.number_sel),
(u"Menu C", self.option_c) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
 
def check_items(self):
idx = self.body.current()
( self.text_editor, self.number_sel, self.option_c )[idx]()
 
def text_editor(self):
def cbk():
if not self.dlg.cancel:
self.txt = self.dlg.body.get()
note(self.txt, "info")
self.refresh()
return True
 
self.dlg = Notepad(cbk, self.txt)
self.dlg.run()
 
def number_sel(self):
def cbk():
if not self.dlg.cancel:
val = self.dlg.items[self.dlg.body.current()]
try:
n = int(val)
except:
note(u"Invalid number. Try again.", "info")
return False
note(u"Valid number", "info")
self.refresh()
return True
 
self.dlg = NumSel(cbk)
self.dlg.run()
 
def option_c(self): note(u"C","info")
 
def close_app(self):
ny = popup_menu( [u"No", u"Yes"], u"Exit ?")
if ny is not None:
if ny == 1:
Application.close_app(self)
 
if __name__ == "__main__":
 
app = MyApp()
app.run()

Number selection added to menu:

MBA BFUI Example41.jpg

Number selection in action:

MBA BFUI Example42.jpg

Handling invalid entries (return False and do not call refresh() inside dialogue caller context):

MBA BFUI Example43.jpg

Valid entry accepted (return True and call refresh() inside dialogue caller context):

MBA BFUI Example44.jpg

Finally, what about changing the menu and body dynamically? This can be accomplished simply by overriding refresh(). refresh() is responsible for all UI updates, assigning desired values for menu, body, and exit_handler. If you made changes to a UI element, it is necessary to redraw it. In the next example, the menu and body contents of the NameList() dialogue are changed dynamically.

# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
 
from window import Application, Dialog
from appuifw import Listbox, note, popup_menu, Text, query, app
 
class Notepad(Dialog):
def __init__(self, cbk, txt=u""):
menu = [(u"Save", self.close_app),
(u"Discard", self.cancel_app)]
Dialog.__init__(self, cbk, u"MyDlg title", Text(txt), menu)
 
class NumSel(Dialog):
def __init__(self, cbk):
self.items = [ u"1", u"2", u"a", u"b" ]
Dialog.__init__(self, cbk,
u"Select a number",
Listbox(self.items, self.close_app))
 
class NameList(Dialog):
def __init__(self, cbk, names=[]):
self.names = names
self.body = Listbox([(u"")],self.options)
# Do not populate Listbox() ! Refresh will do this.
Dialog.__init__(self, cbk, u"Name list", self.body)
 
def options(self):
op = popup_menu( [u"Insert", u"Del"] , u"Names:")
if op is not None:
if op == 0:
name = query(u"New name:", "text", u"" )
if name is not None:
self.names.append(name)
print self.names
elif self.names:
del self.names[self.body.current()]
# Menu and body are changing !
# You need to refresh the interface.
self.refresh()
 
def refresh(self):
menu = []
if self.names:
menu = map(lambda x: (x, lambda: None), self.names)
items = self.names
else:
items = [u"<empty>"]
self.menu = menu + [(u"Exit", self.close_app)]
self.body.set_list(items,0)
# Since your self.menu and self.body have already defined their
# new values, call base class refresh()
# PSZY:Another way to refresh the body,in this way ,you can even change the body to another type
#appuifw.app.body = None
#del self.body
#self.body = appuifw.Listbox(self.items,self.options)
#Dialog.refresh(self)
 
Dialog.refresh(self)
 
class MyApp(Application):
def __init__(self):
self.txt = u""
self.names = []
items = [ u"Text editor",
u"Number selection",
u"Name list" ]
menu = [ (u"Text editor", self.text_editor),
(u"Number selection", self.number_sel),
(u"Name list", self.name_list) ]
body = Listbox(items, self.check_items )
Application.__init__(self,
u"MyApp title",
body,
menu)
 
def check_items(self):
idx = self.body.current()
( self.text_editor, self.number_sel, self.name_list )[idx]()
 
def text_editor(self):
def cbk():
if not self.dlg.cancel:
self.txt = self.dlg.body.get()
note(self.txt, "info")
self.refresh()
return True
 
self.dlg = Notepad(cbk, self.txt)
self.dlg.run()
 
def number_sel(self):
def cbk():
if not self.dlg.cancel:
val = self.dlg.items[self.dlg.body.current()]
try:
n = int(val)
except:
note(u"Invalid number", "info")
return False
note(u"Valid number", "info")
self.refresh()
return True
 
self.dlg = NumSel(cbk)
self.dlg.run()
 
def name_list(self):
def cbk():
if not self.dlg.cancel:
self.names = self.dlg.names
self.refresh()
return True
 
self.dlg = NameList(cbk, self.names)
self.dlg.run()
 
def close_app(self):
ny = popup_menu( [u"No", u"Yes"], u"Exit ?")
if ny is not None:
if ny == 1:
Application.close_app(self)
 
if __name__ == "__main__":
 
app = MyApp()
app.run()

Name list added to menu:

MBA BFUI Example51.jpg

Names added to menu and body:

MBA BFUI Example52.jpg . MBA BFUI Example53.jpg . MBA BFUI Example54.jpg

Download source code: Media:MBA ui demo.zip

Go to another Dialog from a Dialog

# -*- coding: utf-8 -*-
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# Edited by Zhang Yu (PSZY)
# cn.psz.zhangy*gmail.com(Replace * by @)
# LICENSE: GPL3
 
from window import *
import appuifw
 
class Notepad(Dialog):
def __init__(self,cbk,txt=u''):
menu = [(u'Go to Dlg2',self.GotoList),\
(u'Save',self.close_app),\
(u'Discard',self.cancel_app)]
Dialog.__init__(self,cbk,u'MyDlg title',appuifw.Text(txt),menu)
def GotoList(self):
def cbk():
self.refresh()
self.dlg = List(cbk)
self.dlg.run()
 
class List(Dialog):
def __init__(self,cbk,title=u'Second Dlg'):
self.ListExample = [u'P',u'S',u'Z',u'Z']
self.body2 = appuifw.Listbox(self.ListExample,self.sayHello)
menu = [(u'Back to Dlg1',self.close_app)]
Dialog.__init__(self,cbk,title,self.body2,menu)
def sayHello(self):
num = self.body.current()
if num == 0:
appuifw.note(u'Peng','conf')
if num == 1:
appuifw.note(u'Su','conf')
if num == 2 or num ==3 :
appuifw.note(u'Zhang','conf')
class MyApp(Application):
def __init__(self):
self.txt = u''
items = [ u'Text editor',u'Option B',u'Option C']
menu = [(u'Text editor',self.text_editor),\
(u'Menu B',self.option_b),\
(u'Menu C',self.option_c)]
body = appuifw.Listbox(items,self.check_items)
Application.__init__(self,u'MyApp title',body,menu)
def check_items(self):
idx = self.body.current()
( self.text_editor,self.option_b,self.option_c)[idx]()
def text_editor(self):
def cbk():
if not self.dlg.cancel:
self.txt = self.dlg.body.get()
appuifw.note(self.txt,'info')
self.refresh()
return True
self.dlg = Notepad(cbk,self.txt)
self.dlg.run()
def option_b(self):appuifw.note(u'B','conf')
def option_c(self):appuifw.note(u'C','conf')
 
def close_app(self):
ny = appuifw.popup_menu([u'No',u'Yes'],u'Exit?')
if ny is not None:
if ny == 1:
Application.close_app(self)
if __name__ == '__main__':
app = MyApp()
app.run()


Tabbed applications

Tabbed applications are supporting as well. In this case, the body must be replaced by a list of bodies with the following format:

[(tab_text, body, menu),(tab_text, body, menu),...]

where:

* tab_text: unicode string used in tab
* body: a valid body (Listbox, Text or Canvas)
* menu: menu for that body

Each entry in this list will be displayed in a tab. You can specify a global menu to be added to the bottom of each tab menu. This way, it is simple to share common function (like exit) among all tabs. Just specify this menu when calling Dialog() or Application.

# -*- coding: utf-8 -*-
#
# Marcelo Barros de Almeida
# marcelobarrosalmeida (at) gmail.com
# License: GPL3
 
from window import Application
from appuifw import *
 
class MyApp(Application):
def __init__(self):
# defining menus for each tab/body
ma=[(u"menu a",lambda:self.msg(u"menu a"))]
mb=[(u"menu b",lambda:self.msg(u"menu b"))]
mc=[(u"menu c",lambda:self.msg(u"menu c"))]
md=[(u"menu d",lambda:self.msg(u"menu d"))]
# common menu for all tabs
common_menu=[(u"common menu",lambda:self.msg(u"common menu"))]
# bodies
ba=Listbox([u"a",u"b",u"c"])
bb=Canvas()
bc=Text(u"Text")
bd=Listbox([u"1",u"2",u"3"])
 
Application.__init__(self,
u"MyApp title",
[(u"Tab a",ba,ma),(u"Tab b",bb,mb),
(u"Tab c",bc,mc),(u"Tab d",bd,md)],
common_menu)
 
def msg(self,m):
note(m,"info")
 
if __name__ == "__main__":
app = MyApp()
app.run()

Tabbed application in action:

MBA BFUI Screenshot0067.png . MBA BFUI Screenshot0068.png

Locking UI

For time-consuming operations, such as network connections, one interesting option is to lock the user interface to avoid undesired user actions. Two methods are used in such a situation: lock_ui() and unlock_ui(). Simply lock the UI, do whatever you want to do, and unlock the UI. If you wish, change the application title during this locking to indicate some operation status, and do not forget to call refresh() just after unlocking the UI.

self.lock_ui(u"Connecting...")
#
# your stuff here
#
self.unlock_ui()
self.set_title(u"My app")

Conclusion

The framework presented here, although simple, is powerful and easy to use, allowing rapid prototyping of applications with multiple dialogues. It is used in the project Wordmobi, where more use cases can be found.

Related links

This page was last modified on 28 June 2012, at 09:20.
177 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×