Ova serija uputstava je inspirisana dvema stvarima:
Ovo drugo može da zvuči pomalo ratoborno, ali nije tako. Ne kažem da su druga uputstva pogrešna, ili loša, samo kažem da ne funkcionišu na način na koji ja volim da radim.
Lično ne verujem u pristup da se nešto objašnjava, a zatim se kaže “Sada ću vam pokazati kako se to stvarno radi”. Ne verujem u “dečije” primere. Ja verujem u to da ste dovoljno pametni da stvari učite jednom, i da odmah učite pravu stvar.
Dakle, u ovoj seriji uputstava ću razvijati malu TODO aplikaciju (“to do” – za uraditi), koristeći alate i procedure koje koristim kada stvarno programiram, osim razvojnog okruženja Eric IDE. To je zato što su razvojna okruženja stvar ličnih sklonosti, i za ovde razmatrani projekat zaista nisu puno bitna.
Jedna druga stvar, koju neću dodati, je testiranje jedinica. Iako je to veoma važno, mislim da bi odvraćalo pažnju od stvarnog rada. Ukoliko je to problem, ono se može dodati u kasnijoj verziji ovog uputstva.
Morate imati instalirane sledeće programe:
Ovo uputstvo ne podrazumeva poznavanje Elixir-a, PyQt-a, ili baza podataka, ali podrazumeva radno znanje Python-a. Ukoliko još uvek ne znate python, ovo nije još pravo uputstvo za vas.
Potpun kod za ovu sesiju možete dobiti ovde: Izvorni kodovi (kliknite na dugme “Preuzimanje”).
Pošto se ovo uputstvo u potpunosti hostuje na GitHub-u, slobodni ste da dodajete unapređenja, modifikacije, pa čak i cele nove sesije ili opcije!
Najnovija verzija ove sesije (u RST formatu) je uvek dostupna na GitHub-ovom glavnom stablu, kao tut1.txt
Pošto razvijamo TODO aplikaciju, potreban nam je pozadinski mehanizam koji obavlja skladištenje, povraćaj i generalno upravljanje TODO zadacima.
Da bih to uradio na najjednostavniji mogući način, uradiću to koristeći Elixir, “Deklarativni sloj nad SQLAlchemy objektno-relacionim maperom”.
Ako je ovo zvučalo zastrašujuće, nemojte biti zabrinuti. To znači “način kreiranja objekata koji se automatski pohranjuju u bazu podataka”.
Ovde se nalazi kod, sa komentarima, za naš pozadinski mehanizam, nazvan todo.py. Nadajmo se da nećemo morati ponovo da ga gledamo, sve do mnogo kasnijeg mesta u ovom uputstvu!
%2d # -*- kodni raspored: utf-8 -*-
%2d
%2d """Jednostavan pozadinski mehanizam za TODO aplikaciju, koristeci Elixir"""
%2d
%2d import os
%2d from elixir import *
%2d
%2d dbdir=os.path.join(os.path.expanduser("~"),".pyqtodo")
%2d dbfile=os.path.join(dbdir,"tasks.sqlite")
%2d
%2d class Task(Entity):
%2d """
%2d Zadatak za vasu TODO listu.
%2d """
%2d using_options(tablename='tasks')
%2d text = Field(Unicode,required=True)
%2d date = Field(DateTime,default=None,required=False)
%2d done = Field(Boolean,default=False,required=True)
%2d tags = ManyToMany("Tag")
%2d
%2d def __repr__(self):
%2d return "Task: "+self.text
%2d
%2d
%2d class Tag(Entity):
%2d """
%2d Oznaka koju mozemo da primenimo na zadatak.
%2d """
%2d using_options(tablename='tags')
%2d name = Field(Unicode,required=True)
%2d tasks = ManyToMany("Task")
%2d
%2d def __repr__(self):
%2d return "Tag: "+self.name
%2d
%2d
%2d saveData=None
%2d
%2d def initDB():
%2d if not os.path.isdir(dbdir):
%2d os.mkdir(dbdir)
%2d metadata.bind = "sqlite:///%s"%dbfile
%2d setup_all()
%2d if not os.path.exists(dbfile):
%2d create_all()
%2d
%2d # Ovo je kako Elixir 0.5.x i 0.6.x radi
%2d # Da, donekle je ruzno, ali je potrebno za Debian
%2d # i Ubuntu i druge distribucije.
%2d
%2d global saveData
%2d import elixir
%2d if elixir.__version__ < "0.6":
%2d saveData=session.flush
%2d else:
%2d saveData=session.commit
%2d
%2d
%2d
%2d def main():
%2d
%2d # Inicijalizacija baze podataka
%2d initDB()
%2d
%2d # Kreiranje dve oznake
%2d green=Tag(name=u"green")
%2d red=Tag(name=u"red")
%2d
%2d #Kreiranje novih oznaka i njihovo "kacenje"
%2d tarea1=Task(text=u"Buy tomatos",tags=[red])
%2d tarea2=Task(text=u"Buy chili",tags=[red])
%2d tarea3=Task(text=u"Buy lettuce",tags=[green])
%2d tarea4=Task(text=u"Buy strawberries",tags=[red,green])
%2d saveData()
%2d
%2d print "Green Tasks:"
%2d print green.tasks
%2d print
%2d print "Red Tasks:"
%2d print red.tasks
%2d print
%2d print "Tasks with l:"
%2d print [(t.id,t.text,t.done) for t in Task.query.filter(Task.text.like(ur'%l%')).all()]
%2d
%2d if __name__ == "__main__":
%2d main()
Počnimo sada sa zabavnim delom: PyQt!
Preporučujem korišćenje dizajnera (softvera) za kreiranje vaših grafičkih interfejsa. Da, neki ljudi se žale na interfejs dizajnere. Ja kažem da svoje vreme treba da trošite pišući kod za one delove za koje ne postoje dobri alati.
I, ovde se nalazi Qt Designer fajl za naš glavni prozor: window.ui. Ne brinite o svom tom XML-u, samo otvorite fajl u svom dizajneru ;-)
Evo kako prozor izgleda u dizajneru:
Glavni prozor, u dizajneru.
Ono što vidite je “Glavni prozor”. Ova vrsta prozora vam daje da imate meni, trake sa alatima, statusne trake, i to je tipičan prozor za standardnu aplikaciju.
Tekst na vrhu, “Type Here” (upišite ovde), je tu jer je meni još uvek prazan, i poziva vas da dodate nešto u njega.
Velika pravougaona stvar sa natpisima “Task” (zadatak), “Date” (datum) i “Tags” (tagovi – oznake), je grafički objekat (widget), zvani QTreeView, koji je zgodan za prikazivanje stvari sa ikonama, nekoliko kolona i, možda, hijerarhijskom strukturom (otud ime “tree” – stablo). Mi ćemo ga upotrebiti za prikaz naše liste zadataka.
Možete videti kako ovaj prozor izgleda koristeći opciju “Form” -> “Preview”, u dizajneru. Ovo je ono šta ćete dobiti:
Pogled na glavni prozor, sa prikazom liste zadataka.
Možete probati da promenite veličinu prozora, i ovaj grafički objekat će upotrebiti sav raspoloživi prostor i razvući prozor unutar njega. Ovo je važno: prozori koji ne “podnose” pravilno promenu veličine izgledaju neprofesionalno i nisu odgovarajući.
U Qt-u se ovo radi korišćenjem rasporeda (layout). U ovom konkretnom slučaju, pošto imamo samo jedan grafički objekat, ono što radimo je da kliknemo na pozadinu forme i izaberemo “Layout” -> “Layout Horizontally” (Vertikalno bi ovde imalo potpuno isti efekat).
Kada budemo radili konfiguracioni okvir za dijalog, naučićemo više o rasporedima.
Sada se slobodno igrajte sa dizajnerom i ovom formom. Možete probati menjanje rasporeda, dodati nove stvari, menjati svojstva grafičkih objekata, eksperimentisati kako vam volja, učenje dizajnera je vredno uloženog truda!
Sada ćemo ovaj prozor, koji smo napravili, učiniti delom stvarnog programa, tako da možemo početi da da ga teramo da radi.
Prvo moramo da kompajliramo naš .ui fajl u python kod. To možete uraditi sa ovom komandom:
pyuic4 window.ui -o windowUi.py
Hajde sada da pogledamo main.py, glavni fajl naše aplikacije:
%2d # -*- kodni raspored: utf-8 -*-
%2d
%2d """Korisnički interfejs za vašu aplikaciju"""
%2d
%2d import os,sys
%2d
%2d # Uvoženje Qt modula
%2d from PyQt4 import QtCore,QtGui
%2d
%2d # Uvoženje kompajliranog UI modula
%2d from windowUi import Ui_MainWindow
%2d
%2d # Kreiranje klase za naš glavni prozor
%2d class Main(QtGui.QMainWindow):
%2d def __init__(self):
%2d QtGui.QMainWindow.__init__(self)
%2d
%2d # Ovo je uvek isto
%2d self.ui=Ui_MainWindow()
%2d self.ui.setupUi(self)
%2d
%2d def main():
%2d # Još jednom, ovo je opšte namene, biće isto u
%2d # skoro svakoj aplikaciji koju napišete
%2d app = QtGui.QApplication(sys.argv)
%2d window=Main()
%2d window.show()
%2d # Ovo je exec_ jer je exec rezervisana reč u Python-u
%2d sys.exit(app.exec_())
%2d
%2d
%2d if __name__ == "__main__":
%2d main()
Kao što možete videti, ovo uopšte nije specifično za našu TODO aplikaciju. Šta god bilo u tom .ui fajlu, radilo bi sasvim fino sa ovim!
Jedini interesantan deo je klasa Main. Ta klasa koristi kompajlirani ui fajl i tu ćemo smestiti logiku korisničkog interfejsa naše aplikacije. Nikada nemojte ručno uređivati .ui fajl, ili generisani python fajl!
Hajde da to kažem ovim rečima: AKO UREĐUJETE UI FAJL (BEZ KORIŠĆENJA DIZAJNERA), ILI GENERISANI PYTHON FAJL, RADITE TO POGREŠNO! GREŠITE! EPSKA GREŠKA!. Nadam se da je ovo doprlo do vas, jer postoji najmanje jedan vodič (uputstvo) koji vam govori da to radite. NE RADITE TO!!!,
Samo stavite vaš kod u ovu klasu i sve će biti dobro.
Tako, ako pokrenete main.py, učinićete da aplikacija radi. Ona neće raditi nšta interesantno, pošto moramo našem korisničkom interfejsu da prikačimo pozadinski mehanizam, ali to je već sesija 2.
Ukoliko još niste, pogledajte prvo Sesiju 1.
Svi fajlovi za ovu sesiju nalaze se ovde: Sesija 2 na GitHub-u
U Sesiji 1 smo napravili glavni prozor naše aplikacije, skeletnu aplikaciju koja prikazuje pomenuti prozor, i jednostavan pozadinski mehanizam, zasnovan na Elixir-u.
Nismo, međutim, povezali obe stvari. A povezivanje stvari je važan korak koji preduzimamo u ovoj sesiji.
Prvo ćemo raditi na našem main.py.
Prva stvar je da moramo da koristimo naš pozadinski mehanizam, pa moramo da uvezemo todo.py.
To radimo u liniji 13
Zatim, u liniji 25 radimo prvi pravi novi posao: uzimamo listu zadataka i stavljamo ih u našu listu.
Ispitajmo to u detalje. Evo koda od linija 25 do 35:
%2d # Uradimo nešto interesantno: učitavanje sadržaja baze podataka
%2d # u naš grafički objekat sa listom zadataka
%2d for task in todo.Task.query().all():
%2d tags=','.join([t.name for t in task.tags])
%2d item=QtGui.QTreeWidgetItem([task.text,str(task.date),tags])
%2d item.task=task
%2d if task.done:
%2d item.setCheckState(0,QtCore.Qt.Checked)
%2d else:
%2d item.setCheckState(0,QtCore.Qt.Unchecked)
%2d self.ui.list.addTopLevelItem(item)
Sećate se našeg pozadinskog mehanizma, todo.py? U njemu smo definisali Task (zadatak) objekte koji se smeštaju u bazu podataka. U slučaju da ne znate Elixir, todo.Task.query().all()
uzima listu svih Task objekata iz baze podataka.
Zatim oznakama (tags) dodeljujemo oznake, odvojene zapetama, koje će izgledati poput “home,important”.
I sada, vidimo naš prvi Qt-vezani kod.
Prvo: self.ui.list je grafički objekat (widget). Sve što stavimo u prozor, koristeći dizajner, je dostupno korišćenjem self.ui.object_name, a object_name je podešeno korišćenjem dizajnera. Ako kliknete desnim dugmetom na naš prozor možete videti da je ime grafičkog objekta list:
Ime objekta je list.
To takođe možete videti (i promeniti ga) u Object Inspector-u i u Property Editor-u.
To nije samo neki grafički objekat. To je QTreeWidget, koristan za prikazivanje kitnjastih listi sa više kolona i stabala.
Možete pročitati dosta toga o ovom grafičkom objektu, u njegovom uputstvu, ali hajde da damo vrlo skraćenu verziju:
Zatim, ako je zadatak završen (task.done==True
) stavljamo “kvačicu” (checkmark) pored njega. Ako nije završen, ne stavljamo je.
Za mnoge jednostavne programe, ovo je sve što treba da znate o QtreeWidget-u. To je prilično kompleksan grafički objekat, i vrlo moćan, ali ovo će biti dovoljno za naš posao. Istražujte!
Dakle, šta ovo radi? Pokrenimo python main.py i saznajmo!
Lista zadataka sa našim zadacima - primerima.
Ako je vaš prozor prazan, probajte prvo sa pokretanjem python todo.py first, to će kreirati neke zadatke - primere.
Možete čak i “čekirati” zadatak, da ga označite kao završen! To je zato što smo primenili (delimično) snimanje izmenjenih zadataka...
Dakle, ono što želimo je da, onda kada korisnik klikne na “kućicu” (checkbox), zadatak bude izmenjen u skladu sa tim, i sačuvan u bazi podataka.
U većini alata biste tražili povratne pozive. Ovde su stvari nešto drugačije. Kada neki Qt grafički objekat želi da vas o nečemu obavesti , kao na primer “Korisnik je kliknuo na ovo dugme”, ili “Meni za pomoć (Help menu) je aktiviran”, ili šta god bilo, oni odašilju signale (na trenutak idu uporedo).
Konkretno, ovde nas interesuje itemChanged signal QtreeWidget-a:
QTreeWidget.itemChanged ( stavka, kolona ) [signal]
Ovaj signal se odašilje onda kada se sadržaj kolone u navedenoj stavki menja.
I, kad god kliknete na ”kućicu”, ovaj signal se odašilje. Zašto je to važno? Zato što možete povezati sopstveni kod za neki signal, tako da se vaš kod izvrši svaki put kada se taj signal emituje! Taj vaš kod se u Qt slengu naziva slot (utičnica).
Ovo je poput povratnih poziva, osim što:
Ovo pomaže da vaš kod držite slobodno povezanim.
Mogli bismo definisati metod u Main klasi, i povezati ga sa signalom itemChanged, ali nema potrebe jer možemo da upotrebimo AutoConnect. Ukoliko u Main dodate metod sa specifičnim imenom, on će biti povezan sa tim signalom. Ime je oblika on_objectname_signalname.
Ovde je kod (linije 37 do 42):
%1d def on_list_itemChanged(self,item,column):
%1d if item.checkState(0):
%1d item.task.done=True
%1d else:
%1d item.task.done=False
%1d todo.saveData()
Vidite li kako koristimo item.task da odrazimo checkState stavke (bilo da je “čekirana”, ili ne) u stanju zadatka?
Naredba todo.saveData(), na kraju, osigurava da svi podaci budu snimljeni u bazu podataka, putem Elixir-a.
AutoConnect je ono što ćete koristiti 90% vremena da dodate ponašanje vašoj aplikaciji. Većinu vremena ćete samo praviti prozore, dodavati grafičke objekte, i kačiti signale preko AutoConnect-a.
U nekim prilikama ovo neće biti dovoljno. Ali dotle još nismo stigli.
Ovo je bila prilično kratka sesija, ali spremite se za sledeću: radićemo Važne stvari sa Dizajnerom (Important Stuff with Designer (TM))!
U međuvremenu, možda budete želeli da pogledate ove strane:
Ovde možete videti šta se promenilo između starih i novih verzija:
Izmenjene linije: | 34 | Generisano pomoću by diff2html © Yves Bailly, MandrakeSoft S.A. 2001 diff2html je zaštićen licencom . |
Dodata linija: | 13, 14, 15, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48 | |
Uklonjena linija: | Ni jedna |
session1/main.py | session2/main.py | |||
---|---|---|---|---|
34 linije 754 bajta Poslednja izmena: Pon 2. mart 2009. 01:29:24 |
60 linija 1557 bajtova Poslednja izmena: Čet 5. mart 2009. 02:03:03 |
|||
1 |
# -*- kodni raspored: utf-8 -*- |
1 |
# -*- kodni raspored: utf-8 -*- |
|
2 |
2 |
|||
3 |
"""Korisnički interfejs za našu aplikaciju""" |
3 |
"""Korisnički interfejs za našu aplikaciju""" |
|
4 |
4 |
|||
5 |
import os,sys |
5 |
import os,sys |
|
6 |
6 |
|||
7 |
# Uvoženje Qt modula |
7 |
# Uvoženje Qt modula |
|
8 |
from PyQt4 import QtCore,QtGui |
8 |
from PyQt4 import QtCore,QtGui |
|
9 |
9 |
|||
10 |
# Uvoženje kompajliranog UI modula |
10 |
# Uvoženje kompajliranog UI modula |
|
11 |
from windowUi import Ui_MainWindow |
11 |
from windowUi import Ui_MainWindow |
|
12 |
12 |
|||
13 |
# Uvoženje našeg pozadinskog mehanizma |
|||
14 |
import todo |
|||
15 |
||||
13 |
# Kreiranje klase za naš glavni prozor |
16 |
# Kreiranje klase za naš glavni prozor |
|
14 |
class Main(QtGui.QMainWindow): |
17 |
class Main(QtGui.QMainWindow): |
|
15 |
def __init__(self): |
18 |
def __init__(self): |
|
16 |
QtGui.QMainWindow.__init__(self) |
19 |
QtGui.QMainWindow.__init__(self) |
|
17 |
20 |
|||
18 |
# Ovo je uvek isto |
21 |
# Ovo je uvek isto |
|
19 |
self.ui=Ui_MainWindow() |
22 |
self.ui=Ui_MainWindow() |
|
20 |
self.ui.setupUi(self) |
23 |
self.ui.setupUi(self) |
|
21 |
24 |
|||
25 |
# Uradimo nešto interesantno: učitavanje sadržaja baze podataka |
|||
26 |
# u naš grafički objekat sa listom zadataka |
|||
27 |
for task in todo.Task.query().all(): |
|||
28 |
tags=','.join([t.name for t in task.tags]) |
|||
29 |
item=QtGui.QTreeWidgetItem([task.text,str(task.date),tags]) |
|||
30 |
item.task=task |
|||
31 |
if task.done: |
|||
32 |
item.setCheckState(0,QtCore.Qt.Checked) |
|||
33 |
else: |
|||
34 |
item.setCheckState(0,QtCore.Qt.Unchecked) |
|||
35 |
self.ui.list.addTopLevelItem(item) |
|||
36 |
||||
37 |
def on_list_itemChanged(self,item,column): |
|||
38 |
if item.checkState(0): |
|||
39 |
item.task.done=True |
|||
40 |
else: |
|||
41 |
item.task.done=False |
|||
42 |
todo.saveData() |
|||
43 |
||||
44 |
||||
22 |
def main(): |
45 |
def main(): |
|
46 |
# Inicijalizacija baze podataka, pre rađenja bilo čega drugog |
|||
47 |
todo.initDB() |
|||
48 |
||||
23 |
# Još jednom, ovo je opšte namene, biće isto u |
49 |
# Još jednom, ovo je opšte namene, biće isto u |
|
24 |
# skoro svakoj aplikaciji koju napišete |
50 |
# skoro svakoj aplikaciji koju napišete |
|
25 |
app = QtGui.QApplication(sys.argv) |
51 |
app = QtGui.QApplication(sys.argv) |
|
26 |
window=Main() |
52 |
window=Main() |
|
27 |
window.show() |
53 |
window.show() |
|
28 |
# Ovo je exec_ jer je exec rezervisana reč u Python-u |
54 |
# Ovo je exec_ jer je exec rezervisana reč u Python-u |
|
29 |
sys.exit(app.exec_()) |
55 |
sys.exit(app.exec_()) |
|
30 |
56 |
|||
31 |
57 |
|||
32 |
if __name__ == "__main__": |
58 |
if __name__ == "__main__": |
|
33 |
main() |
59 |
main() |
|
34 |
60 |
Generisano pomoću diff2html-a, u petak 6. marta 2009. u 00:58:30
Komandna linija: /home/ralsina/bin/diff2html session1/main.py session2/main.py