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.


ticker.py

#! /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()

conf.py

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

tickurl.py

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] + "&")

pytickerc

[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 = &

Jednotlivé soubory naleznete i tady: http://penguin.cz/~vanous/pyticker