Namespaces

Variants
Actions

Please note that as of October 24, 2014, the Nokia Developer Wiki will no longer be accepting user contributions, including new entries, edits and comments, as we begin transitioning to our new home, in the Windows Phone Development Wiki. We plan to move over the majority of the existing entries over the next few weeks. Thanks for all your past and future contributions.

Archived:Customized listbox on canvas in PySymbian

From Wiki
Jump to: navigation, search

Archived.pngAquivado: Este artigo foi arquivado, pois o conteúdo não é mais considerado relevante para se criar soluções comerciais atuais. Se você achar que este artigo ainda é importante, inclua o template {{ForArchiveReview|escreva a sua justificativa}}.

All PySymbian articles have been archived. PySymbian is no longer maintained by Nokia and is not guaranteed to work on more recent Symbian devices. It is not possible to submit apps to Nokia Store.

Article Metadata
Tested with
Devices(s): Nokia E71
Compatibility
Platform(s): S60 3rd Edition
Article
Keywords: appuifw, listbox, canvas
Created: marcelobarrosalmeida (04 Apr 2009)
Last edited: hamishwillee (31 May 2013)

Contents

Introduction

Special needs require special programs. I was needing a special listbox, with variable number of lines per row. Even I have found some listboxes that accepted more than two lines per row, they didn´t allow to mix rows with different number of lines.

So, I decided to create my own listbox, drawing it on canvas and using PySymbian. It may be a good starting point for other listboxes that require canvas, like image browsing, multi-selection or for programs like twitter clients. The code was tested only in Nokia E71 and Python 1.4.5. It is not prepared yet for screen rotation.

Usage

CanvasListBox is similar to original PySymbian Listbox, with methods set_list and current and generating a callback when an item is selected. Several parameters may be changed at listbox creation or later, just calling reconfigure. For instance:

  • position: Where your list will be placed, in screen coordinates like (xa,ya,xb,yb). You may use just part of your screen.
  • scrollbar_width: Scrollbar width.
  • margins: The distance from text to listbox outlines, indicated as (dxa,dya,dxb,dyb).
  • font_name: Font to be used in listbox.
  • font_color: Font color.
  • line_space: Space between consecutive lines
  • line_break_chars: For multiple line entries, line breaks are done using this set of characters. This way, it is possible to break a line at characters like '-' or '/', for instance. This is an evolution of TextWrapper example class). The default set is " .;:\\/-".
  • scrollbar_color: Scrollbar color
  • selection_font_color: Font color of selected row
  • selection_fill_color: Background color of selected row
  • selection_border_color: Border color of selected row
  • odd_fill_color: Background color of odd rows
  • even_fill_color: Background color of even rows

Screenshots

See the YouTube Demo Video

Code and demo

# -*- coding: cp1252 -*-
# CanvasListBox 1.0.1
# (c) Marcelo Barros de Almeida
# marcelobarrosalmeida@gmail.com
# License: GPL3
 
from appuifw import *
import e32
import graphics
import key_codes
import os
from math import ceil, floor
import sysinfo
 
class CanvasListBox(Canvas):
"""
This classes creates a listbox with variable row size on canvas.
"""

def __init__(self,items,cbk=lambda:None,**attrs):
""" Creates a list box on canvas. Just set callback
function and possible attributes.
"""

Canvas.__init__(self,
redraw_callback = self.redraw_list,
event_callback = self.event_list)
self.check_default_values(attrs)
self.cbk = cbk
self.set_list(items)
self.bind(key_codes.EKeyUpArrow, self.up_key)
self.bind(key_codes.EKeyDownArrow, self.down_key)
self.bind(key_codes.EKeySelect, self.cbk)
 
def get_config(self):
return self.attrs
 
def check_default_values(self,attrs):
self.attrs = {}
self.def_attrs = {'position':(0,0,self.size[0],self.size[1]),
'scrollbar_width':5,
'margins':(2,2,2,2),
'font_name':'dense',
'font_color':(255,255,255),
'font_background_color':(0,0,0),
'line_space': 0,
'line_break_chars':u" .;:\\/-",
'scrollbar_color':(255,255,255),
'selection_font_color':(255,255,102),
'selection_fill_color':(124,104,238),
'selection_border_color':(255,255,102),
'odd_fill_color':(0,0,0),
'even_fill_color':(50,50,50)}
 
for k in self.def_attrs:
if k in attrs:
self.attrs[k] = attrs[k]
else:
self.attrs[k] = self.def_attrs[k]
# fixing spacing
fh = -(graphics.Image.new((1,1)).measure_text("[qg_|^y",font=self.attrs['font_name'])[0][1])
self.attrs['font_height'] = fh
self.attrs['line_space'] = max(3,fh/4,self.attrs['line_space'])
self.position = (0,
0,
self.attrs['position'][2] - self.attrs['position'][0],
self.attrs['position'][3] - self.attrs['position'][1])
self.lstbox_xa = self.position[0] + self.attrs['margins'][0]
self.lstbox_ya = self.position[1] + self.attrs['margins'][1]
self.lstbox_xb = self.position[2] - self.attrs['margins'][2] - \
self.attrs['scrollbar_width']
self.lstbox_yb = self.position[3] - self.attrs['margins'][3]
 
self.scrbar_xa = self.position[2] - self.attrs['scrollbar_width']
self.scrbar_ya = self.position[1] #+ self.attrs['margins'][1]
self.scrbar_xb = self.position[2]
self.scrbar_yb = self.position[3] #- self.attrs['margins'][3]
 
self.selbox_xa = self.position[0]
self.selbox_xb = self.position[2] - self.attrs['scrollbar_width']
 
self.lstbox_size = (self.position[2]-self.position[0],
self.position[3]-self.position[1])
self._screen = graphics.Image.new(self.lstbox_size)
 
def reconfigure(self,attrs={}):
self.check_default_values(attrs)
self.set_list(self._items)
 
def redraw_list(self,rect=None):
self.clear_list()
self.draw_scroll_bar()
self.redraw_items()
self.blit(self._screen,
target=(self.attrs['position'][0],self.attrs['position'][1]),
source=((0,0),self.lstbox_size))
 
def draw_scroll_bar(self):
self._screen.rectangle((self.scrbar_xa,
self.scrbar_ya,
self.scrbar_xb,
self.scrbar_yb),
outline = self.attrs['scrollbar_color'])
list_size = len(self.lstbox_items)
if list_size:
pos = self.scrbar_ya + self._current_sel*(self.scrbar_yb-
self.scrbar_ya)/float(list_size)
pos = int(pos)
pos_ya = max(self.scrbar_ya,pos-10)
pos_yb = min(self.scrbar_yb,pos+10)
self._screen.rectangle((self.scrbar_xa, pos_ya, self.scrbar_xb, pos_yb),
outline = self.attrs['scrollbar_color'],
fill = self.attrs['scrollbar_color'])
 
def redraw_items(self):
xa = self.lstbox_xa
xb = self.lstbox_xb
y = self.lstbox_ya + self.attrs['font_height']
ysa = self.lstbox_ya
n = self._selection_view[0]
while y < self.lstbox_yb and n < len(self.lstbox_items):
row = self.lstbox_items[n]
# select fill color
ysb = ysa + row['height']
font_color = self.attrs['font_color']
if n == self._current_sel:
font_color = self.attrs['selection_font_color']
# selection at center
pos = (self.selbox_xa,ysa-int(ceil(self.attrs['line_space']/2)),
self.selbox_xb,ysb + 1 -int(floor(self.attrs['line_space']/2)))
outline = self.attrs['selection_border_color']
fill = fill = self.attrs['selection_fill_color']
elif n % 2:
pos = (self.selbox_xa,ysa,self.selbox_xb,ysb)
outline = self.attrs['odd_fill_color']
fill = self.attrs['odd_fill_color']
else:
pos = (self.selbox_xa,ysa,self.selbox_xb,ysb)
outline = self.attrs['even_fill_color']
fill = self.attrs['even_fill_color']
self._screen.rectangle(pos,outline = outline,fill = fill)
ysa = ysb
for line in row['text']:
self._screen.text((xa,y),
line,fill=font_color,
font=self.attrs['font_name'])
y += self.attrs['font_height'] + self.attrs['line_space']
n += 1
 
def calculate_sel_view(self):
n = self._selection_view[0]
y = self.lstbox_ya
while y < self.lstbox_yb and n < len(self.lstbox_items):
y += self.lstbox_items[n]['height']
n += 1
if y >= self.lstbox_yb:
# ensure all items in view are visible
n -= 1
# base index is 0
self._selection_view[1] = n - 1
 
def up_key(self):
if self._current_sel <= 0:
return
n = self._current_sel - 1
if n < self._selection_view[0]:
self._selection_view[0] -= 1
self.calculate_sel_view()
else:
self._current_sel_in_view -= 1
 
self._current_sel = self._current_sel_in_view + self._selection_view[0]
self.redraw_list()
 
def down_key(self):
if self._current_sel >= len(self.lstbox_items) - 1:
return
n = self._current_sel + 1
if n > self._selection_view[1]:
# ensure that selected item in inside the view,
# increasing the begining until it fits
while n > self._selection_view[1]:
self._selection_view[0] += 1
self.calculate_sel_view()
self._current_sel_in_view = n - self._selection_view[0]
else:
self._current_sel_in_view += 1
 
self._current_sel = n
self.redraw_list()
 
def build_list(self,items):
self.lstbox_items = []
width = self.lstbox_xb - self.lstbox_xa
for item in items:
# text: array with all lines for the current text, already splitted
# num_line: len of array
# height: how much height is necessary for displaying
# this text including line space
reg = {}
lines = item.split(u'\n')
reg['text'] = []
reg['num_lines'] = 0
reg['height'] = 0
for line in lines:
splt_lines = self.split_text(line,width)
reg['text'] += splt_lines
num_lines = len(splt_lines)
reg['num_lines'] += num_lines
reg['height'] += num_lines*(self.attrs['font_height'] + \
self.attrs['line_space'])
self.lstbox_items.append(reg)
 
# modified version of TextRenderer.chop
# http://www.developer.nokia.com/Community/Discussion/showthread.php?124666-TextWrapper-example-class
def split_text(self, text, width):
lines = []
text_left = text
while len(text_left) > 0:
bounding, to_right, fits = self.measure_text(text_left,
font=self.attrs['font_name'],
maxwidth=width,
maxadvance=width)
if fits <= 0:
lines.append(text_left)
break
 
slice = text_left[0:fits]
adjust = 0 # (preserve or not whitespaces at the end of the row)
 
if len(slice) < len(text_left):
# find the separator character closest to the right
rindex = -1
for sep in self.attrs['line_break_chars']:
idx = slice.rfind(sep)
if idx > rindex:
rindex = idx
if rindex > 0:
if slice[rindex] == u' ':
adjust = 1
slice = slice[0:rindex]
 
lines.append(slice)
text_left = text_left[len(slice)+adjust:]
 
return lines
 
def event_list(self,ev):
pass
 
def clear_list(self):
self._screen.clear(self.attrs['font_background_color'])
self.blit(self._screen,
target=(self.attrs['position'][0],self.attrs['position'][1]),
source=((0,0),self.lstbox_size))
 
def current(self):
return self._current_sel
 
def set_list(self,items):
# selected item. It is relative to 0.
self._current_sel = 0
# current selection inside view. It is relative
# to the view (self._selection_view[0]).
self._current_sel_in_view = 0
# current items in the view. It is relative to 0
self._selection_view = [0,0]
# save original data
self._items = items
self.build_list(items)
self.calculate_sel_view()
self.redraw_list()
 
class DemoCanvasListBox(object):
items = [ u"Beautiful\nis better\nthan ugly.",
u"Explicit\nis better\nthan implicit.",
u"Simple is better than complex.",
u"Complex is better than complicated.",
u"Flat is better than nested.",
u"Sparse is better than dense.",
u"Readability counts.",
u"Special cases aren't special enough to break the rules.",
u"Although practicality beats purity.",
u"Errors should never pass silently.",
u"Unless explicitly silenced.",
u"In the face of ambiguity, refuse the temptation to guess.",
u"There should be one-- and preferably only one --obvious way to do it.",
u"Although that way may not be obvious at first unless you're Dutch.",
u"Now is better than never.",
u"Although never is often better than *right* now.",
u"If the implementation is hard to explain, it's a bad idea.",
u"If the implementation is easy to explain, it may be a good idea.",
u"Namespaces are one honking great idea -- let's do more of those!" ]
 
colors = {u'Black':(0, 0, 0),
u'White':(255, 255, 255),
u'Red':(255, 0, 0),
u'Green':(0, 255, 0),
u'Blue':(0, 0, 255),
u'Yellow':(255, 255, 0),
u'Cyan':(0, 255, 255),
u'Magenta':(255, 0, 255),
u'Light gray':(200,200,200),
u'Dark Gray':(50,50,50),
u'Gray':(150,150,150)}
 
def __init__(self):
self.lock = e32.Ao_lock()
app.screen = "full"
app.menu = [(u"Change font",self.change_font),
(u"Change position",self.change_position),
(u"Change line space",self.change_line_space),
(u"Change margins",self.change_margins),
(u"Change colors",(
(u"Text",lambda:self.change_colors('font_color')),
(u"Selection Text",lambda:self.change_colors('selection_font_color')),
(u"Selection background",lambda:self.change_colors('selection_fill_color')),
(u"Selection border",lambda:self.change_colors('selection_border_color')),
(u"Odd lines",lambda:self.change_colors('odd_fill_color')),
(u"Even lines",lambda:self.change_colors('even_fill_color')),
(u"Background",lambda:self.change_colors('font_background_color')))),
(u"Default",lambda:self.listbox.reconfigure()),
(u"Quit", self.close_app)]
pos = (0,0) + sysinfo.display_pixels()
self.listbox = CanvasListBox(self.items,self.item_selected,position=pos)
app.body = self.listbox
self.lock.wait()
 
def change_font(self):
attrs = self.listbox.get_config()
fonts = available_fonts() + [u"normal", u"dense", u"title", u"symbol", u"legend", u"annotation"]
op = popup_menu(fonts,u"Font:")
if op is not None:
attrs['font_name']= fonts[op]
self.listbox.reconfigure(attrs)
 
def change_position(self):
attrs = self.listbox.get_config()
pos = query(u"Position","text", unicode(attrs['position']))
if pos is not None:
try:
pos = eval(pos)
except:
note(u"Invalid position","info")
else:
attrs['position'] = pos
self.listbox.reconfigure(attrs)
 
def change_line_space(self):
attrs = self.listbox.get_config()
pos = query(u"Line space", "number", attrs['line_space'])
if pos is not None:
attrs['line_space'] = pos
self.listbox.reconfigure(attrs)
 
def change_margins(self):
attrs = self.listbox.get_config()
pos = query(u"Margins","text", unicode(attrs['margins']))
if pos is not None:
try:
pos = eval(pos)
except:
note(u"Invalid margins","info")
else:
attrs['margins'] = pos
self.listbox.reconfigure(attrs)
 
def change_colors(self,element):
attrs = self.listbox.get_config()
colors = self.colors.keys()
colors.sort()
op = popup_menu(colors,u"Color:")
if op is not None:
attrs[element]= self.colors[colors[op]]
self.listbox.reconfigure(attrs)
 
def item_selected(self):
note(u"Item %d selected !" % self.listbox.current(),"info")
 
def close_app(self):
self.lock.signal()
 
DemoCanvasListBox()

References

This page was last modified on 31 May 2013, at 01:03.
83 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.

×