Saturday, July 5, 2014

44. Baby Names App

ListView adapters are used with data from files.




On this webpage, you can download a zip file. The particular file used was the first one, for national data. Once the file is downloaded, it should be extracted into a folder, say data.




A python file, will be written, to read the data, in the folder, with that name. All data files have the form of 'yob', year, '.txt'. To see the structure of the file, you can open one of the files in a text editor. The data is comma formatted, which is the reason we split at comma. For Female and Male, we create two lists, F and M, which are lists of tuples.




Next, we change list of tuples, to list of strings, with same names. This way, we do not have to write code, for argument conversion, in the ListAdapter. Finally, there is some test code, which shows how the function can be used.


# bname.py

def rlists(year):
    M=[]
    F=[]
    with open('data/yob'+str(year)+'.txt') as f:
        A=f.readlines()
    for a in A:
        if ',F,' in a:
            F.append((a.split(',')[0],int(a.split(',')[-1])))
        else:
            M.append((a.split(',')[0],int(a.split(',')[-1])))
    F=[f[0]+" : " + str(f[1]) for f in F]
    M=[m[0]+" : " + str(m[1]) for m in M]
    return M[:100],F[:100]

if __name__=='__main__':
    m,f=rlists(2001)
    print(m)



In the application's python file, these are the imports. The root is based on BoxLayout. The ListItemButton, is imported, to create subclasses, for the 3 ListViews. Each ListView, will be associated, with a ListProperty. Finally, the function rlists, in the module bname is imported.




The three ListItemButton subclasses, for the three ListViews, are created.




In the root, we have three ListProperties, for the 3 ListViews. The first one, d1, will have, all the years, in the data, from 1880, to 2013. The Python range function, goes from first number, to last number, minus 1. The d2 and d3, are initialized, as 100 empty strings. The reason, we initialized them, with empty strings, is so that, empty buttons, are visible, at startup, for Lists 2 and 3.




From the kv file, this function is called, whenever a year is selected in ListView1. We try to read, male, and female data, in to lists, d2, and d3. If we can not, an error is written to the screen. The most probable reason, is data, is not stored, in the correct directory, and thus we write, the current directory name. Once you put data in that directory, the program should be rerun.




These functions are called, when an item in Listview2 or Listview3, is selected. It just prints, the text of the selected item.




The app code must exist for the main python file.


# ex44.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.listview import ListItemButton
from kivy.properties import ListProperty
from bname import rlists

class FirstListItemButton(ListItemButton):
    pass

class SecondListItemButton(ListItemButton):
    pass

class ThirdListItemButton(ListItemButton):
    pass

class Ex44(BoxLayout):
    d1 = ListProperty(
        [str(i) for i in range(1880,2014)] )
    d2 = ListProperty(['']*100)
    d3 = ListProperty(['']*100)
    def change(self,c):
        try: self.d2,self.d3 = rlists(int(c.text))
        except:
            import os
            CurDir = os.getcwd()
            print('Can not find data in ' + CurDir) 
    def change1(self,c):
        print('M => '+c.text)
    def change2(self,c):
        print('F => '+c.text)

class Ex44App(App):
    def build(self):
        return Ex44()

if __name__ == '__main__':
    Ex44App().run()



In the kv file, we import the ListAdapter, and the main python file. In the Last tutorial, we imported the ListItemButton. But now we want to import the subclasses of ListItemButton, which are in the main python file.




We select the appropriate change function, depending on which item is selected.




ListView1 uses the List d1. Now, cls points to the correct subclass.




ListView2 and 3, use Lists d2 and d3. cls points to the appropriate ListItemButton subclass.


# ex44.kv

#: import ListAdapter kivy.adapters.listadapter.ListAdapter
#: import ex44 ex44

<FirstListItemButton>:
    on_press: app.root.change(*args)

<SecondListItemButton>:
    on_press: app.root.change1(*args)

<ThirdListItemButton>:
    on_press: app.root.change2(*args)

 
<Ex44>:
    ListView:
        adapter:
            ListAdapter(data=root.d1,
            selection_mode='single',
            cls=ex44.FirstListItemButton)
    ListView:
        adapter:
            ListAdapter(data=root.d2,
            selection_mode='single',
            cls=ex44.SecondListItemButton)
    ListView:
        adapter:
            ListAdapter(data=root.d3,
            selection_mode='single',
            cls=ex44.ThirdListItemButton)
        



This is the result, when the year, 2013, was selected. The Male and Female Lists have the top 100 names. You can scroll up, and down, the list. There are many ways to optimize reading of the files and make it independent of filesystem. We could easily create a new Python file to hold the lists. One particular implementation is shown next.


# dataTopy.py

def rlists(year):
    M=[]
    F=[]
    with open('data/yob'+str(year)+'.txt') as f:
        A=f.readlines()
    for a in A:
        if ',F,' in a:
            F.append((a.split(',')[0],int(a.split(',')[-1])))
        else:
            M.append((a.split(',')[0],int(a.split(',')[-1])))
    F=[f[0]+" : " + str(f[1]) for f in F]
    M=[m[0]+" : " + str(m[1]) for m in M]
    return M[:100],F[:100]


if __name__=='__main__':
    wout = []
    wout.append('# data.py\n')
    wout.append('dtM={}\n')
    wout.append('dtF={}\n')
    for year in range(1880,2014):
        a,b=rlists(year)
        wout.append('dtM[{}]={}\n'.format(year,a))
        wout.append('dtF[{}]={}\n'.format(year,b))
    wout.append('def rlists(year):\n')
    wout.append(' '*4 + 'return dtM[year],dtF[year]\n')
    fout=open('data.py','w')
    fout.writelines(wout)
    fout.close()
    # In ex44.py, only 1 change
    # from bname import rlist
    # -> from data import rlist  



Thursday, July 3, 2014

43. ListView

ListView allows us, to view lists of data. As in most cases, here, the individual list items are selectable.




The root widget is based on BoxLayout. We use ListProperty, to hold the ListView data, to be displayed. Whenever, a particular list item is selected, its text will be placed in the StringProperty.




In the root, the list to be displayed by ListView, is in the variable, my_data. Initially, it is empty and thus the ListView, would be empty at startup. The StringProperty called selected_value, holds the individual list item button's text. The change() function will be called, from the kv file, whenever a particular list item is selected.




The app class has to return the root widget. It should be noted that, inside the kv file, root always refers to top of current tree, and not necessarily this class, unless rules are being defined, for the root widget.


# ex43.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ListProperty, StringProperty

class Ex43(BoxLayout):
    my_data = ListProperty([])
    selected_value = StringProperty('Select a button')
    def change(self,change):
        self.selected_value = 'Selected: {}'.format(change.text)

class Ex43App(App):
    def build(self):
        return Ex43()

if __name__ == '__main__':
    Ex43App().run()



In the kv file, we need two imports. The first is for the ListAdapter, which is needed to connect to the Python List. The second import is required since we want each of the list items to be selectable, and based on the Button. A rule is defined for ListItemButton to go to the change() function when a press is detected.




The root is based on BoxLayout. It will have a 'vertical' orientation. It will have three rows, the first is BoxLayout, and is shown here. The inner BoxLayout defines three items which will be arranged 'horizontally', the default orientation. The first item is a Label. Next, we have a TextInput with an id of add_box. The button is the last item and will add one string to the list, my_data.




ListView takes 80% of the height. We define the adapter as pointing to my_data. The canvas.before is used to make this item an orange color.




The bluish label indicates the selected value.


# ex43.kv

#: import ListAdapter kivy.adapters.listadapter.ListAdapter
#: import ListItemButton kivy.uix.listview.ListItemButton

<ListItemButton>:
    on_press: app.root.change(*args)
    
<Ex43>:
    orientation: 'vertical'
    BoxLayout:
        size_hint_y: .1
        Label:
            text: 'Enter String'
        TextInput:
            id: add_box
            multiline: False
            size_hint_x: 5
        Button:
            text: 'Add'
            on_press: root.my_data.append(add_box.text)
    ListView:
        size_hint_y: .8
        adapter:
            ListAdapter(data=root.my_data,
            selection_mode='single',
            allow_empty_selection=False,
            cls=ListItemButton)
        canvas.before:
            Color:
                rgb: 1,.5,0
            Rectangle:
                pos: self.pos
                size: self.size
    Label:
        size_hint_y: .1
        canvas.before:
            Color:
                rgb: 0,.5,1
            Rectangle:
                pos: self.pos
                size: self.size
        text: root.selected_value
        
            



In this result, we entered 7 lines. Then the last line was selected, and the label in the bottom shows the status. In this example, the items were added by typing. In a real application, they could be data, from a database, or a website.