Jednoduché programování multiplatformních grafických aplikací - News Ticker Pro všechny, kteří pod Linuxem postrádají jednoduché grafické programování pod GPL tady mám okomentovaný zdrojový kód malé grafické aplikace News Ticker. News Ticker je prográmek, který zpracovává a zobrazuje XML data z exportních RSS souborů zpravodajských serverů. Napsaný je Pythonu s použitím wxWindows (wxPython) a python-xml. Nejprve samozřejmě varování:
- nejsem programátor a vím, že zcela vědomě používám hrozné programovací (nebo spíše amatérské) obraty, techniky a konstrukce (na programování mám pár minut denně, na studování dokumentace nezbývá čas). Toto není návod na programování, toto je konstatování že to jde a je to jednoduché.
- stoprocentně vím, že News Ticker není stoprocentně multiplatformní - bude potřeba upravit určité rutiny, například určení cesty konfiguračního souboru a pak také spouštění browseru ;-), ale jinak by multiplatformní být měl.
Teď k věci:
Už dávno jsem si chtěl napsat tento malý prográmek, abych měl možnost rychle vidět, jaké nové články jsou na news serverech k dispozici. O tom, že programovacím jazykem bude Python jsem nepochyboval, jsem rozhodnut se jej _nějakým_způsobem_naučit_, otázka ale byla jaké grafické prostředí zvolit. Otestoval jsem Tck/Tk, ale nějak mi nepřirostlo k srdci a pak se mi také zdálo, že nemá dostatek widgetů (mřížka a podobně...). S obavou jsem sáhl po wxWindows - s obavou proto, že se mi zdálo, že se jedná o komplikované multiplatformní knihovny, byl jsem ale příjemně překvapen a všichni, kteří mají rádi Visual Basic by měli zajásat. Je to velmi podobné. Hledal jsem a testoval i nějaká vývojová prostředí (měl jsem jich pár pro Tck/Tk), ale můj počítač je moc pomalý na takové testy... V době hledání jsem měl navíc pouze zhruba dvacet mega volného prostoru na disku, takže jsem se do instalace nových programů nijak nehrnul. Teď mám místa více, ale zase nemám čas hledat...pokud máte nějaké zkušenosti s IDE pro Python/wxPython, dejte vědět v komentářích. Ještě k rychlosti - na mém Pentiu 100MHz/32MB RAM je všechno hned vidět ;-( , takže i nabíhání News Tickeru je pomalé. Doufám ale, že na normálních strojích nebude načítání programu nikoho zdržovat.
Program je rozdělěn do tří částí - modulů. Na vrcholu stojí ticker.py , který je hlavní částí, která slouží ke spouštění. Má také naimportovány dva pomocné moduly - soubor conf.py, který slouží k naštení a uložení dat do konfiguračního souboru a pak druhý soubor tickurl.py , který se stará o stažení a rozparsování zadaného RSS souboru. Důležitý je také čtvrtý soubor - .pytickerc , který již obsahuje pár nastavených serverů a nastavený browser. Program si pamatuje poslední nastavenou velikost okna, jak je v .pytickerc také vidět.
#! /usr/bin/env python
from wxPython.wx import * # import modulů použitých funkcí
ID_ABOUT=wxNewId()
ID_EXIT=wxNewId()
ID_REFRESH=wxNewId()
ID_OPEN=wxNewId()
#vytvoření identifikátorů položek v menu
verze='0.5-alpha '
jmeno="News Ticker " + verze
vsechno=[]
#další globální pole ;-)
import tickurl, conf
#import modulů pro stažení a parsování rss plus modulu pro získání a ukládání konfigurace
UlozenaSize=conf.GetSize()
browser=conf.GetBrowser()
browser2=conf.GetBrowser2()
#získáme uložené údaje z konfiguračního souboru
class MainWindow(wxFrame):
#hlavní okno
def __init__(self,parent,id):
self.dirname="."
wxFrame.__init__(self,parent,-4, jmeno, size = UlozenaSize, style=wxDEFAULT_FRAME_STYLE|wxNO_FULL_REPAINT_ON_RESIZE)
EVT_SIZE(self, self.OnSize) #událost při změně velikosti
self.control = wxListBox(self, 60, wxPoint(80, 50), wxSize(80, 100),['Tento program byl vytvořen','pro vyzkoušení si','Pythonu, XML a wxWindow.'], wxLB_SINGLE) #vytvoření listboxu
# EVT_LISTBOX(self, 60, self.EvtListBoxClick)
EVT_LISTBOX_DCLICK(self, 60, self.EvtListBoxDClick) #událost při kliknutí na listbox
EVT_BUTTON(self, ID_OPEN, self.EvtListBoxDClick) #událost při kliknutí na button _open_
## EVT_BUTTON(self, ID_REFRESH, self.MakeMenu) #událost při kliknutí na button _refresh_
# EVT_LISTBOX_RCLICKED(self, 60, self.EvtListRClick)
self.bar=self.CreateStatusBar() # A Statusbar in the bottom of the window
self.bar.SetFieldsCount(3) #nastavení vlastností status baru
self.bar.SetStatusWidths([-1, 80])
## self.but = wxButton(self.bar, ID_REFRESH, "Obnovit") #vložení tlačítka do status baru
self.but2 = wxButton(self.bar, ID_OPEN, "Otevřít") #vložení tlačítka do status baru
# self.OnSize(None)
self.MakeMenu(self) #vytvoření menu
def MakeMenu(self,e): #funkce vytvoreni menu
# Setting up the menu
filemenu= wxMenu() #inicializace
filemenu.Append(ID_ABOUT, "&O programu"," Informace o tomto programu") #přidání položky do menu
filemenu.AppendSeparator() #přidání oddělovače (šedá čára)
global servery_def # globální proměnná pro data
servery_def=conf.GetServer() #načtení parametrů o serverech z konf. souboru
for server in servery_def: # smyčka
nId=wxNewId() #vytvoření nového ID pro jednotlivé položky menu
filemenu.Append(nId, server[0]," Načti data " + server[0] + " (" + server[1] + ")") #přidání položky do menu
EVT_MENU(self, nId, self.mojo) #přidání události položky menu
filemenu.AppendSeparator() # přidání oddělovače
filemenu.Append(ID_EXIT,"&Konec"," Ukončení programu") #přidání další položky menu
# Creating the menubar.
self.menuBar = wxMenuBar()
self.menuBar.Append(filemenu,"&Servery") # Adding the "filemenu" to the MenuBar
self.SetMenuBar(self.menuBar) # Adding the MenuBar to the Frame content.
EVT_MENU(self, ID_ABOUT, self.OnAbout) # attach the menu-event ID_ABOUT to the method self.OnAbout
EVT_MENU(self, ID_EXIT, self.OnExit) # attach the menu-event ID_EXIT to the method self.OnExit
self.Show(true) # zobrazit
def mojo(self,e): #funkce události kliknutí na položku v menu
u = self.menuBar.GetLabel(e.GetId()) #zjištění položky menu
for i in servery_def:
if i[0]==u: #nalezení odpovídající položky v poli
self.nacti(i[1],i[2]) #stažení odpovídajícího rss souboru
def OnSize(self, event): # zpracování události při změně velikosti okna
import conf
self.cSize=self.GetSize() #načtení rozměrů z konf. souboru
conf.SetSize(self.cSize.width, self.cSize.height) #nastavení zjištěných údajů
event.Skip()
rect=self.bar.GetFieldRect(1) # tady se zpracovává velikost pole ve status baru,
rect2=self.bar.GetFieldRect(2) # podle něj se pak vypočte a umístí tlačítko
## self.but.SetPosition(wxPoint(rect.x+2, rect.y+2))
## self.but.SetSize(wxSize(rect.width-4, rect.height-4))
self.but2.SetPosition(wxPoint(rect2.x+2, rect2.y+2)) #samotný výpočet a umístění tlačítka
self.but2.SetSize(wxSize(rect2.width-4, rect2.height-4))
def OnAbout(self,e): # o programu ;-)
d= wxMessageDialog( self, jmeno +
"\nnapsaný ve wxPython.\n"
"\n"
"Petr Vaněk\n"
"vanous@penguin.cz","O programu", wxOK) # Create a message dialog box o programu
d.ShowModal() # Shows it
d.Destroy() # finally destroy it when finished.
def OnExit(self,e):
self.Close(true) # Close the frame.
# def EvtListBoxClick(self, event):
# self.control.GetItemText(self.currentItem)
# self.SetStatusText("%s " % (vsechno[self.control.GetSelection()][0]))
# self.SetStatusText(self.control.GetSelection())
# self.control.GetItemText(self.currentItem)
# self.SetStatusText("%s " % (vsechno[self.control.GetSelection()][0]))
# self.SetToolTip(wxToolTip('toooool'))
# self.SetStatusText(self.control.GetSelection())
# d= wxMessageDialog( self,"%s " % (vsechno[self.control.GetSelection()][1]), wxOK)
## d= wxMessageDialog( self,"%s " % (vsechno[self.control.GetSelection()][2]),"Popisek:",wxOK)
# Create a message dialog box
## d.ShowModal() # Shows it
## d.Destroy() # finally destroy it when finished.
# def EvtListBoxRClick(self, event):
# self.control.GetItemText(self.currentItem)
# self.SetStatusText("Pravy klik")
# self.SetStatusText("%s " % (vsechno[self.control.GetSelection()][0]))
# self.SetToolTip(wxToolTip('toooool'))
# self.SetStatusText(self.control.GetSelection())
def EvtListBoxDClick(self, event): #dvojklik - otevření v browseru
import os
os.system(browser + " \"" + vsechno[self.control.GetSelection()][0] + "\"" + browser2) # spuštění příkazu s parametry
# self.control.Delete(self.control.GetSelection()) # po přečtení se položka vymaže z menu, je ale potřeba projít i pole s daty a provést tam změny, odloženo na neurčito ;-)
def nacti(self,url,koding): #stažení dat z požadovaného serveru
global vsechno # oblíbené globální položky
if vsechno != 0:
vsechno[:]=[] # vytvoření prázdného pole (tuple)
try:
vsechno=tickurl.convert(url,koding) # stažení a parsing. tady by to chtělo ošetřit timeout, popřípadě dát možnost ručního přerušení.
except:
chyba=wxMessageDialog(self, "Naslala chyba při stahování z adresy\n" + url, "Soubor RSS nestažen",wxICON_HAND, wxDefaultPosition) # ošetření chyby
chyba.ShowModal()
chyba.Destroy()
# print vsechno
sampleList=[]
for i in vsechno: # rozsekání na kousky
pavel=i[1]
petr=pavel
petr=pavel.encode(koding)
sampleList.append(petr)
self.control.Set(sampleList) # naplnění list boxu
self.SetTitle(jmeno + sampleList[0]) # nastavení title okna
app = wxPySimpleApp() # spuštění
frame = MainWindow(None, -1)
frame.Show(1)
app.MainLoop()
app.MainLoop()
import ConfigParser,os # natažení modulů použitých funkcí
file=os.path.expanduser('~/.pytickerc') # umístění a jméno konfiguračního souboru
config=ConfigParser.ConfigParser() # inicializace parseru
config.read(file)
def SetSize(x,y): # uložení velikosti okna
soubor=open(file,'w') # otevření souboru pro zápis
if config.has_section('size') !=1 : # pokud neexistuje sekce size
config.add_section('size') # tak ji vytvoříme
config.set('size','x',x) # uložíme x-ovou velikost do sekce size jako proměnnou x
config.set('size','y',y) # a totéž pro y
config.write(soubor) # zapíšeme do souboru
def GetSize(): # načtení uložené velikosti
try:
x=config.getint('size','x')
except:
x=240 # ošetření výjimky - velikost ještě není zapsána v konf. souboru
try:
y=config.getint('size','y')
except:
y=120
return x,y # návratová hodnota funkce
def GetBrowser(): # načtení parametru programu browseru
try:
x=config.get('browsers','browser')
except:
print "browser nenastaven"
x=''
return x
def GetBrowser2(): # načtení parametru pro browse program (na unixu je to & pro spuštění detachnutí)
try:
x=config.get('browsers','browser2')
except:
print "parametr browseru nenastaven"
x=''
return x
def GetServer(): # načtení parametrů rss serverů
try:
x=config.options('servers')
except:
print "servers nenastaveny"
x=''
y=[] # pomocné pole
for i in x: # rosparsování parametrů
pom=config.get('servers',i) # načtení parametru
pom2=pom.split(',') # rozsekání na dílky (podle čárek)
y.append(pom2) # vložení do pomocného pole
z=tuple(y) # konverze na tuple
return z # funkce vrací z
import sys,codecs,os,urllib
from xml.parsers import expat
#import modulů použitých funkcí
moje=[]
#vynulování globální proměnné, která se naplní daty z xml
class RSS2HTML:
#konvertor samotný
def __init__(self):
# self._out = []
# nastavení proměnných
self._data = ""
self._first_item = 1
self._title = None
self._link = None
self._descr = None
def start_tag(self, name, attrs):
self._data = ""
if name == "item":
self._descr = None # vynulování
if self._first_item:
self._first_item = 0
def end_tag(self, name):
# rosparsování položek rss
if name == "title":
self._title = self._data
elif name == "link":
self._link = self._data
elif name == "description":
self._descr = self._data
elif name == "language":
self._out=([self._link, self._title,self._descr or ""])
global moje
moje.append(self._out)
#připojení do globálního pole
elif name == "item":
self._out=([self._link,self._title,self._descr or ""])
global moje
moje.append(self._out)
# print self._out
def data_handler(self, data):
self._data = self._data + data
# --- The driver
def convert(url,koding):
sysid = urllib.urlopen(url)
# sysid = urllib.urlopen('http://www.root.cz/rss/')
app = RSS2HTML()
p = expat.ParserCreate(koding)
p.StartElementHandler = app.start_tag
p.EndElementHandler = app.end_tag
p.CharacterDataHandler = app.data_handler
error = 0
inf = sysid
buf = inf.read(16384)
while buf != "":
if p.Parse(buf, 0) != 1:
error = 1
break
buf = inf.read(16384)
print buf
inf.close()
global moje
return moje
if error or p.Parse("", 1) != 1:
print "ERROR: %s in %s:%s:%s" % (expat.ErrorString(p.ErrorCode),
sysid, p.ErrorLineNumber,
p.ErrorColumnNumber)
# --- The main program
#servery=['http://penguin.cz/cgi-bin/toISO-8859-2.en/rss.php3','http://www.root.cz/rss/']
#servery=['http://penguin.cz/cgi-bin/toISO-8859-2.en/rss.php3']
#out = codecs.open(sys.argv[1], "w", "iso-8859-2")
#for i in servery:
# sysid = urllib.urlopen(i)
# convert(sysid)
#print len(moje)
#convert(sysid, out)
#out.close()
#os.system("dillo file:" + sys.argv[1] + "&")
[servers] penguin = Penguin.cz,http://penguin.cz/rss.php3,iso8859-2 root = Root.cz,http://www.root.cz/rss/,iso8859-2 [size] x = 367 y = 151 [browsers] browser = dillo browser2 = &