Dia de la Llibertat del Programari 2014

Dia de la Llibertat del Programari - Software Freedom Day - 2014

Com cada any arriba el dia de la Llibertat del Programari, el Software Freedom Day.

A casa nostra, els actes tenen tres centres d’activitat: ahir, a la Palma de Cervelló, organitzada per la Konfraria del Pingüí, i avui al centre cívic de les Corts, pel Caliu. A més, l’associació per a joves del Raval, El Teb, que participa a la xarxa de telecentres Punt TIC de la Generalitat, també s’ha sumat a la iniciativa.

L’objectiu d’aquest dia és (extret de la Wiki del Caliu):

Visió i Objectius
La nostra visió és la de potenciar que tothom es connecti lliurement, crei i comparteixi en un món digital que sigui participatiu, transparent, i sostenible.
Objectius
1. celebrar la llibertat del programari i la gent que hi ha al seu voltant
2. fomentar una comprensió general de la llibertat del programari i encoratjar l’adopció del programari lliure i els estàndards oberts
3. crear un accés més equitatiu a les oportunitats mitjançant l’ús de tecnologies participatives
4. promoure el diàleg constructiu de responsabilitats i drets a la societat de la informació
5. ser inclusius d’organitzacions i individus que comparteixen la nostra Visió
6. ser pragmàtics, transparents i responsables com a organització

Les activitats previstes per avui al Centre Cívic de les Corts (de la Wiki Caliu):

Dissabte 20 de setembre

Hora            Taller	
10:00-10:10	Introducció a la Viquipèdia
10:15-10:55	FreeBSD
11:00-11:55	Nano Arduino
12:00-12:55	Sessió de dubtes d'edició de la Viquipèdia
13:00-13:55	Web2Py i PyNuke
16:00-16:55	F-Droid
17:00-17:55	Open Data
18:00-18:30	Conclusions del taller d'edició de la Viquipèdia
18:30-19:30	Fòrum obert

Durant tot el dia es portarà a terme un taller d'edició de la Viquipèdia per millorar i afegir-hi articles relacionats amb el programari lliure.

Python. Progressbar amb tkinter.

Aquest estiu he aprofitat per practicar una mica amb Python. La idea, no gaire original, ho reconec, ha estat desenvolupar una aplicació de biblioteca per gestionar el contingut de les dotzenes i dotzenes de DVDs acumulats durant els anys.

Evidentment que hi ha programari fet que serveix per això mateix -com ja he dit, no he estat gaire original- però l’interès principal era practicar amb Python.

L’aplicació llegeix els continguts d’un DVD, o d’un pendrive o targeta SD, i emmagatzema la informació sobre contingut del media a una base de dades SQLite. Després, es pot consultar, modificar, esborrar la informació. L’aplicació fa servir el mòdul tkinter per a fer la interfície gràfica d’usuari.

Coses que he posat en pràctica? tkinter, per la GUI, el mòdul os per llegir el contingut del media (os.walk), el mòdul sqlite per a tractar amb la BD.

potser en un post posterior presentaré l’aplicació completa. Avui només vull comentar un dels aspectes del desenvolupament.

Un indicador de progrés

Potser és una mica sorprenent, però la part que m’ha portat més feina ha estat aquesta:

La lectura del media és un procés que, potencialment, pot ser llarg. Tinc alguns DVDs amb milers de pàgines html de javadoc. O amb milers de fotografies i vídeos familiars acumulats. La lectura d’aquest DVDs és llarga. Quan passa això: processos pesats que consumeixen temps, la solució professional és oferir a l’usuari alguna mena de feedback indicant-li que hi ha un procés en marxa i que esperi. Una forma habitual de fer-ho és amb finestres popup que mostrin alguna mena d’indicador de progrés.

Dit i fet, he estat buscant com es podia fer això amb Tkinter (una progressbar és un widget de interfície gràfica i això vol dir que la implementació concreta depèn de la llibreria de GUI triada.)

Ha calgut fer algunes proves, però al final he arribat al següent esquema per a fer un popup d’indicació de progrés.

main_thread.py

Em calen dues classes, una que representa el fil principal d’execució, amb la GUI, i que rep el nom de MainThread, que deso al fitxer main_thread.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from secondary_thread import BackgroundJob
from tkinter import *
import queue

class MainThread():
    root = None
    queue = None
    strVar = None
    value = 0
    popup = None
    
    def onButtonClick(self):
        self.queue = queue.Queue()
        self.popupWindow()
        
        backgroundJob = BackgroundJob(self.queue)
        backgroundJob.start()

    def __init__(self):
        print("Inici")
        print("Aquest és el fil principal")

        self.root = Tk()
        self.root.title("Main window")
        self.root.geometry("320x240")

        label = Label(self.root, text="Prem el botó per executar el fil")
        label.pack(expand = True)

        button = Button(text="Execute", command=self.onButtonClick)
        button.pack()
        # self.root.wait_window()
        self.root.mainloop()
        
    def popupWindow(self):
        self.popup = Toplevel(self.root)
        self.popup.title("Progress window")
        self.popup.geometry("200x50")  
        self.popup.focus()
        self.strVar = StringVar()
        self.strVar.set("Valor inicial")
        label = Label(self.popup, textvariable = self.strVar)
        label.pack(expand=True)
        
        self.root.after(100, self.read_queue)
        
    def setValue(self, newValue):
        self.strVar.set("Valor actual (%d)" % newValue)
    
    def read_queue(self):
        try:
            self.value = self.queue.get_nowait()
            print("read from queue: %d" % (self.value))
            self.setValue(self.value)
        except Queue.Empty:
            pass
  
        if self.value < 1000000:
            self.root.after(100, self.read_queue)
        else:
            print("job done")
            self.popup.destroy()

# main
if __name__ == "__main__":
    app = MainThread()

secondary_thread.py

I una segona classe BackgroundJob que hereta de threading.Thread i que executa el job en background. Al fitxer de nom secondary_thread.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import threading
import queue

class BackgroundJob(threading.Thread):
    queue = None

    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.queue = queue

    def run(self):
        value = 0
        while value < 1000000:
            value = value + 1
            if value % 100000 == 0:
                self.queue.put(value)
                print("value %d" % (value))
             

Remarques al codi
He fet servir Python3. La recomanació per a nou codi és fer servir aquesta versió del llenguatge. la primera línia de tots dos fitxers és la que estableix l’execució amb la versió Python3. En el neu cas, que tinc fins a tres versions de Python a algun ordinador, aquesta discriminació inicial és important.

Com que algun text que apareix fa servir caràcters accentuats, he especificat l’encoding amb la segona línia dels comentaqris.

No he fet servir el widget Progressbar de ttk perquè he preferit presentar primer una solució general. És molt senzill modificar el codi per a fer servir aquest widget, com mostraré després.

El fil principal és el que mou la GUI. Aquest és un requeriment de Tkinter. El job pesat, en el cas de l’exemple, un comptador de 0 fins 1.000.000 ha d’executar-se en el fil secundari.

La classe principal, per tant, és la que obre la finestra principal, amb el botó que llença l’execució del job, per una banda, i obre la finestra popup amb un label que mostra l’evolució del job.

El job pesat s’executa dins de la classe derivada de threading.Thread, al mètode run. El mètode run és el que s’invoca en un fil apart quan des del fil principal es fa backgroundJob.start()

Quan s’instancia la classe BackgroundJob, se li passa una cua (en aquest cas senzill, també s’hauria pogut fer servir una pipe). El fil secundari posa (put) a la cua, cada 100.000 iteracions, el nombre actual d’iteració.

El fil principal, per la seva banda executa cada 100 milisegons (fent servir el mètode after del toplevel widget) el mètode read_queue. Aquest mètode llegeix (get) la cua i actualitza el popup de progressbar.

Quan read_queue del fil principal llegeix a la cua que el procés pesat del fil secundari ha acabat, destrueix el popup.

Es tracta d’un parell de classes nomès però és un exemple molt bonic ja que amaga una considerable riquesa de conceptes: GUI amb tkinter, programació multifil amb threadind.Thread, comunicació entre processos mitjançant cues. Déu n’hi do.

Amb el widget ttk.Progressbar
Finalment, l’adaptació del codi per a fer servir el widget ttk.Progressbar. Hi han un parell d’opcions per a la progressbar: quan se sap exactament en quin estat es troba el procés i es pot expressar numèricament, aleshores es pot fer servir una Progressbar en mode “determinate”, i passar-li un grau de progrés. Aleshores la representació gràfica de la barra de progrés mostra el mateix tant per cent de barra de progrés completat com de procés. Si, en canvi, es desconeix la durada del procés, aleshores es pot fer servir el mode “indeterminate” en que la barra de progrés mostra un indicador oscil·lant.

Tenint en compte l’anterior, l’adaptació per a mostrar una barra de progrés en mode “indeterminate” és directa. Només cal afegir l’import i substituir el widget label pel widget progressbar. És el que he de fer servir a l’aplicació que llegeix els DVD i els pendrive ja que, en principi, no se quin és el volum de contingut de cada DVD i no faig una passada prèvia per a determinar-lo.

Afegeixo l’import del widget progressbar

from secondary_thread import BackgroundJob
from tkinter import *
from tkinter.ttk import Progressbar
import queue

i substitueixo el label. Tan aviat com es mostra el popup amb la progressbar, inicia l’oscil·lació amb pb.start()

    def popupWindow(self):
        self.popup = Toplevel(self.root)
        self.popup.title("Progress window")
        self.popup.geometry("200x50")  
        self.popup.focus()
        self.strVar = StringVar()
        self.strVar.set("Valor inicial")
        #label = Label(self.popup, textvariable = self.strVar)
        #label.pack(expand=True)
        pb = Progressbar(self.popup, mode="indeterminate", orient="horizontal")
        pb.start()    # inicia l'oscil·lació
        pb.pack(expand=True)

Però en el cas del comptador de 0 a 1.000.000 sí que soc capaç de dir en quin punt es troba el procés, per tant, sí que té sentit fer servir el mode “determinate”. El seguent és el codi del fil principal modificat per a fer servir una Progressbar en mode ‘determinate’.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from secondary_thread import BackgroundJob
from tkinter import *
from tkinter.ttk import Progressbar
import queue

class MainThread():
    root = None
    queue = None
    counter = None
    value = 0
    popup = None
    
    def onButtonClick(self):
        self.queue = queue.Queue()
        self.popupWindow()
        
        backgroundJob = BackgroundJob(self.queue)
        backgroundJob.start()

    def __init__(self):
        print("Inici")
        print("Aquest és el fil principal")

        self.root = Tk()
        self.root.title("Main window")
        self.root.geometry("320x240")

        label = Label(self.root, text="Prem el botó per executar el fil")
        label.pack(expand = True)

        button = Button(text="Execute", command=self.onButtonClick)
        button.pack()
        # self.root.wait_window()
        self.root.mainloop()
        
    def popupWindow(self):
        self.popup = Toplevel(self.root)
        self.popup.title("Progress window")
        self.popup.geometry("200x50")  
        self.popup.focus()
        self.counter = IntVar()
        self.counter.set(0)
        #label = Label(self.popup, textvariable = self.strVar)
        #label.pack(expand=True)
        pb = Progressbar(self.popup, mode="determinate", orient="horizontal", variable=self.counter, maximum=1000000)
        pb.start()
        pb.pack(expand=True)
        
        self.root.after(100, self.read_queue)
        
    def setValue(self, newValue):
        self.counter.set(newValue)
    
    def read_queue(self):
        try:
            self.value = self.queue.get_nowait()
            print("read from queue: %d" % (self.value))
            self.setValue(self.value)
        except Queue.Empty:
            pass
  
        if self.value < 1000000:
            self.root.after(100, self.read_queue)
        else:
            print("job done")
            self.popup.destroy()

# main
if __name__ == "__main__":
    app = MainThread()

En negreta he ressaltat el més interessant.

Remarcar com en el progressbar he indicat el valor màxim, i com el “progrés” ve donat per la “variable” counter de tipus IntVar.

Per anar acabant: el video amb l’execució del programa:

i l’enllaç al repositori GitHub

https://github.com/abaranguer/progressbar-tkinter