Skip to main content

Ralsina.Me — Roberto Alsina's website

Rapid Application development using PyQt and Eric3 ... in realtime!

Hel­lo, I am Rober­to Alsi­na and I will be your host for this evening's demon­stra­tion. I will de­vel­op a use­ful ap­pli­ca­tion us­ing PyQt and Er­ic3, and doc­u­ment the process here. In re­al­time.

Right now, it's 12:49, Jan­u­ary 9, 2004, and I will start.... now!

The application

I will de­vel­op a hi­er­ar­chi­cal note-­tak­ing ap­pli­ca­tion. Some­thing some­what like KJot­s, on­ly pret­ti­er ;-) I will call it Not­ty.

First, I will start by cre­at­ing a project in Er­ic3, the amaz­ing Python IDE I am us­ing.

13

The main script

The main scrip­t, not­ty.py is just some boil­er­plate , in fac­t, I am copy­ing it from an­oth­er pro­jec­t!

#!/usr/bin/env python

# This file is in the public domain
# Written by Roberto Alsina <ralsina@kde.org>


import sys
from qt import *
from window import Window
from optik import OptionParser


def main(args):

        parser=OptionParser()
        (options,args)=parser.parse_args(sys.argv)

        app=QApplication(args)
        win=Window()
        app.setMainWidget(win)
        win.show()
        app.connect(app, SIGNAL("lastWindowClosed()")
                , app
                , SLOT("quit()"))
        app.exec_loop()

if __name__=="__main__":
        main(sys.argv)

As you can see, it sim­ply cre­ates a QAp­pli­ca­tion and shows a win­dow, then en­ters the event loop un­til the win­dow is closed, then dies. It al­so pars­es its op­tion­s, but does noth­ing with them. That's for lat­er.

But what is that Win­dow() there? Well, it's cre­at­ing an in­stance of the Win­dow class. Which I haven't de­fined yet.

Cur­rent time: 12:57

The Window class

To cre­ate the win­dow, I will use Qt de­sign­er. Here's the idea: a tree on the left side, a tabbed wid­get on the right side that al­ter­nates be­tween HTML and re­Struc­tured­Text ver­sions of the notes, and a search thingie at the bot­tom.

Of course, al­so the usu­al tool­bar and menu.

Us­ing De­sign­er, this is most­ly drag drop and stretch.

What I do is cre­ate a .ui file us­ing de­sign­er called win­dow­base.ui, which er­ic will lat­er com­pile in­to a win­dow­base.py python mod­ule.

In that file, the form cre­ates a Win­dow­Base class. Then, I write win­dow.py, where I de­fine the Win­dow class, in­her­it­ing Win­dow­Base and im­ple­ment­ing the func­tion­al­i­ty I wan­t.

In fac­t, right now it al­ready works with on­ly the fol­low­ing as win­dow.py. Does­n't do any­thing, though :-)

from windowbase import WindowBase

class Window (WindowBase):
        def __init__(self):
                WindowBase.__init__(self)
14

Cur­rent time: 13:14

Doing stuff

First, let's look at the right side of the win­dow. I want to be able to ed­it the notes us­ing re­Struc­tured­Tex­t, which is a very nice sort of markup, very nat­u­ral, and see the re­sult as HTML (al­most) on a QTextBrows­er wid­get.

So, I want that, when the us­er choos­es the "HTM­L" tab, it will parse the text of the "Tex­t" tab, and dis­play it.

Piece of cake. Just cre­ate a func­tion Win­dow.changedTab, and con­nect it to the right sig­nal!

This is the class right now:

class Window (WindowBase):
        def __init__(self):
                WindowBase.__init__(self)
                self.connect(self.tabs,SIGNAL("currentChanged(QWidget *)"),self.changedTab)

        def changedTab(self,widget):
                if self.tabs.currentPageIndex()==0:  #HTML tab
                        self.viewer.setText(publish_string(str(self.editor.text()),writer_name='html'))
                elif self.tabs.currentPageIndex()==1:  #Text tab
                        pass

And this is how the app look­s:

1516

Cur­rent time: 13:37 Man, I'm slow­ing down! Most­ly be­cause I had no idea how to use re­Struc­tured­Tex­t, though ;-)

The tree

This is the tricky part. I in­tend to use the tree to hold the da­ta.

I will cre­ate my own noteIt­em class in­her­it­ing the usu­al QListviewItem, and store the note's ren­dered and raw ver­sion in there. Al­so, it will be drag&­drop en­abled, so you can move them around in the tree.

Here's the class:

class noteItem(QListViewItem):
        def __init__(self,parent):
                        QListViewItem.__init__(self,parent)
                        self.setDragEnabled(True)
                        self.setDropEnabled(True)
                        self.setRenameEnabled(0,True)
                        self.setText(0,"New Note")
                        self.rest=""
                        self.html=""

        def setRest(self,text):
                self.html=publish_string(str(text),writer_name='html')
                self.rest=str(text)

Now, I need to be able to in­sert these guys in the tree. I will have to add a new ac­tion in the main win­dow. Let's call it newNote, and put it in the menu. I al­so bound it to Ctr­l-N, and con­nect it to a brand new newNoteS­lot().

Al­so, I re­moved the fileOpen and file­New ac­tion­s, be­cause they make no sense in this ap­p.

Note that all the above is done via de­sign­er. But, I will have to im­ple­ment Win­dow.newNoteS­lot() if I want it to do some­thing use­ful.

Not that it's spe­cial­ly hard, though!

def newNoteSlot(self):
        noteItem(self.tree)

In or­der to make it hi­er­ar­chi­cal , though, some ex­tra work is need­ed. The QlistView (our tree wid­get) needs to co­op­er­ate in the drag­ging and drop­ping of item­s.

Now, this is some­what black mag­ic, but take it from me, it's hard­er in C++ ;-)

The main prob­lem is that QListViewItem it­self does­n't im­ple­ment the meth­ods that han­dle drag­ging and drop­ping, so you re­al­ly need to sub­class it.

But, if you sub­class, you can't just put the wid­get there us­ing de­sign­er. You would need to add a "cus­tom wid­get" in de­sign­er, and it's some­what of a mess.

So... en­ter python's cute dy­nam­ic na­ture.

I de­fine a func­tion that han­dles the drop­ping, one for the drag­ging, and an event han­dler. Then I over­ride the class :-)

This will go in­to the Win­dow con­struc­tor:

self.w.tree.__class__.dragObject=dragNote
self.w.tree.__class__.dropEvent=dropNote
self.w.tree.__class__.dragEnterEvent=dragNoteEnterEvent

And here are the func­tion­s:

#Function that returns what we use to drag a note
def dragNote(self):
        if not self.currentItem().__class__==noteItem:
                return None #Nothing to do here
        o=QTextDrag(str(self.currentItem().text(0))+'\n'+self.currentItem().rest,self)
        return o

def dragNoteEnterEvent(self,event):
        event.accept()

#Function handling a drop in the note tree
def dropNote(self,event):
        if event.provides("text/plain"):
                npos=self.viewport().mapFrom(self,event.pos())
                data=str(event.encodedData("text/plain"))
                lines=data.split('\n')
                title=lines[0]
                parent=self.itemAt(npos)
                item=noteItem(parent)
                item.setText(0,title)
                item.setRest(string.join(lines[1:],'\n'))
                event.accept(True)
        else:
                event.accept(False)

While this is far from ob­vi­ous, at least it is the same when­ev­er you want to do a tree wid­get that drags and drop­s.

On­ly prob­lem is it on­ly copy­-­drags, while move-­drag would seem more nat­u­ral. No idea how to fix it.

No­tice how the note is pick­led very sim­ply: first line is the note name, the rest is the re­Struc­tured­Text con­tents.

17

Cur­rent time: 14:48 Took a while, most­ly be­cause I for­got the sec­ond ar­gu­ment to QTextDrag and was caus­ing a seg­fault.

Binding the tree

Now, we can cre­ate notes, but how can we make the ed­i­tor ed­it their con­tents? Piece o'­cake.

Just con­nect the tree's se­lecte­dItem sig­nal to a slot that up­dates the view­er and ed­i­tor wid­get­s, and we have half of it done. De­sign­er takes care of that, this is the slot:

def noteChanged(self,item):
        if item==0:
                self.viewer.setText('')
                self.editor.setText('')
        else:
                self.editor.setText(item.rest)
                self.viewer.setText(item.html)

The stuff about item==0 is be­cause some­times you can have no item se­lect­ed, for ex­am­ple if you delete all item­s.

While this work­s, it's on­ly half of it. It shows, on the view­er/ed­i­tor, the con­tents of the note. But it does­n't save the changes you make in­to the note. Since all our notes are emp­ty, this does pret­ty much noth­ing ;-)

So, we al­so need to save changes made to the note in the ed­i­tor in­to the noteIt­em's rest field.

But how? This is some­what trick­y. Since us­ing noteIt­em.se­tRest() pars­es the data, it's slow. So, we will save the da­ta when­ev­er one of the fol­low­ing things hap­pen:

  1. The writ­er switch­es to the view­er. Since we have to parse any­way, it's a good mo­­men­t.

  2. When­ev­er a note has been mod­­i­­fied and los­es fo­­cus.

The first is sim­ple. We on­ly need to mod­i­fy Win­dow.changedTab as fol­lows:

def changedTab(self,widget):
        if self.tabs.currentPageIndex()==0:  #HTML tab
                if self.tree.currentItem():
                        self.tree.currentItem().setRest(str(self.editor.text()))
                        self.viewer.setText(self.tree.currentItem().html)
        elif self.tabs.currentPageIndex()==1:  #Text tab
                pass

The sec­ond is a bit hard­er, but not much.

We need to make the ed­i­tor re­mem­ber what noteIt­em the note be­longs to, then make it save the da­ta there on noteChanged.

So, this is how it look­s:

def noteChanged(self,item):
        if item==0:
                self.viewer.setText('')
                self.editor.setText('')
                self.editor.item=None
        else:
                if self.editor.item and self.editor.isModified():
                        self.editor.item.setRest(self.editor.text())
                self.editor.setText(item.rest)
                self.viewer.setText(item.html)
                self.editor.item=item

Also, I had to add a self.editor.item=None in the Window constructor, or else that would complain about self.editor.item not existing.

Al­so, since pars­ing re­Struc­tured­Text can take a sec­ond or two, and we don't want the app to look frozen, I changed noteIt­em.se­tRest to look like this:

def setRest(self,text):
        if not self.rest==text:
                qApp.setOverrideCursor(QCursor(qApp.WaitCursor))
                try:
                        self.html=publish_string(str(text),writer_name='html')
                        self.rest=str(text)
                except:
                        pass
                qApp.restoreOverrideCursor()

The ex­cep­tion han­dling is to avoid fall­ing out of the func­tion with a wait cur­sor. Of course that's not re­al­ly cor­rect er­ror han­dling ;-)

And voila, the app now work­s.

Of course there are some mi­nor is­sues. Like, say, sav­ing ;-)

Cur­rent time: 15:22

Saviour and Loader

The code­word here is se­ri­al­iza­tion.

Sim­ply put, I need to take the tree struc­ture in the QListView and turn it in­to some sort of text file, and vicev­er­sa.

Well, I hear you all chant­ing XM­L! XM­L! back there. I am clue­less about XM­L, so I will have to go do some re­search in­to the python li­brary. Be right back.

Cur­rent time: 15:27

Ok, I am not go­ing to learn XML in the next 15 min­utes. Let's do it us­ing some­thing else.

Let's try... pick­le.

What I need to do is im­ple­ment the Win­dow.­file­Save slot.

Here's an idea. I will tra­verse the tree, and cre­ate sim­ple python struc­ture that rep­re­sents it. Each node will have a ti­tle, a rest con­tent, an html con­tent, and an ar­ray of chil­dren.

Then I pick­le the root node, and the rest is mag­ic.

Here's the code. While this is prob­a­bly not or­tho­dox, it work­s, so don't com­plain too much ;-)

First two helper class­es to tra­verse the tree:

class savedItem:
        def __init__(self,item):
                self.children=[ ]
                self.title=str(item.text(0))
                self.rest=str(item.rest)
                self.html=str(item.html)
                i=item.firstChild()
                while i:
                        self.children.append(savedItem(i))
                        i=i.nextSibling()

class loadedItem:
        def __init__(self,data,parent):
                item=noteItem(parent)
                item.setText(0,data.title)
                item.rest=data.rest
                item.html=data.html
                for child in data.children:
                        loadedItem(child,item)

Then, the ac­tu­al load and save func­tion­s:

def fileSave(self):
        root=[]
        item=self.tree.firstChild()
        while item:
                root.append(savedItem(item))
                item=item.nextSibling()
        f=open("nottybook", "w")
        p=Pickler(f)
        p.dump(root)

def fileLoad(self):
        f=open("nottybook", "r")
        u=Unpickler(f)
        root=u.load()
        for item in root:
                loadedItem(item,self.tree)

No­tice how you don't choose what file you load. There is on­ly one, and it opens and saves au­to­mat­i­cal­ly.

To make it open au­to­mat­i­cal­ly, I added a self­.­fileLoad­() call in the win­dow con­struc­tor, and to make it save au­to­mat­i­cal­ly, I added a self­.­file­Save on a timer, like this:

class autoSaver:
        def __init__(self,delay,window):
                self.t=QTimer()
                self.t.connect(self.t,SIGNAL("timeout()"),self.event)
                self.t.start(delay*60000)
                self.w=window
        def event(self):
                self.w.fileSave()

Then just cre­ate an in­stance of it in the win­dow con­struc­tor, and it's done (the de­lay is in min­utes, BTW)

Al­so, to make it save on ex­it, im­ple­ment­ed the Win­dow.­file­Ex­it slot in a triv­ial man­ner.

def close(self,alsoDelete):
        self.fileSave()
        WindowBase.close(self,True)

def fileExit(self):
        self.close()

And voilà, the app is now re­al­ly an ap­p, and this is a re­al ar­ti­cle.

Cur­rent time: 16:18

Not bad for three and a half hours of work! In fac­t, Not­ty is good enough to con­tain this whole ar­ti­cle as a note :-)

18

Of course, lots of work re­main to be done, spit&pol­ish. But that's for the next is­sue.

Roberto Alsina / 2006-04-04 16:25:

Comments for this story are here:

http://www.haloscan.com/com...

Fred / 2011-09-13 23:20:

Pretty fascinating post condition I'm not clear in your mind we agree also to a cool extent with reference to nearly all of it. Qualification you execute be in possession of selected heroic points I capacity tote up. Regarding unusual theme reverse phone lookup approximating to facilitate talk with reference to a repeal telephone search search I heard a propos. Condition I'm not clear in your mind if you need that check it out so with the intention of you be capable of complete a swap telephone lookup today with the aim of find absent both kinds of things as regards personality plus anything.

employment background check / 2011-12-27 23:23:


Hi very nice article


Contents © 2000-2023 Roberto Alsina