Hello, I am Roberto Alsina and I will be your host for this evening's demonstration. I will develop a useful application using PyQt and Eric3, and document the process here. In realtime.
Right now, it's 12:49, January 9, 2004, and I will start.... now!
I will develop a hierarchical note-taking application. Something somewhat like KJots, only prettier ;-) I will call it Notty.
First, I will start by creating a project in Eric3, the amazing Python IDE I am using.
The main script
The main script, notty.py is just some boilerplate , in fact, I am copying it from another project!
#!/usr/bin/env python # This file is in the public domain # Written by Roberto Alsina <email@example.com> 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 simply creates a QApplication and shows a window, then enters the event loop until the window is closed, then dies. It also parses its options, but does nothing with them. That's for later.
But what is that Window() there? Well, it's creating an instance of the Window class. Which I haven't defined yet.
Current time: 12:57
The Window class
To create the window, I will use Qt designer. Here's the idea: a tree on the left side, a tabbed widget on the right side that alternates between HTML and reStructuredText versions of the notes, and a search thingie at the bottom.
Of course, also the usual toolbar and menu.
Using Designer, this is mostly drag drop and stretch.
What I do is create a .ui file using designer called windowbase.ui, which eric will later compile into a windowbase.py python module.
In that file, the form creates a WindowBase class. Then, I write window.py, where I define the Window class, inheriting WindowBase and implementing the functionality I want.
In fact, right now it already works with only the following as window.py. Doesn't do anything, though :-)
from windowbase import WindowBase class Window (WindowBase): def __init__(self): WindowBase.__init__(self)
Current time: 13:14
First, let's look at the right side of the window. I want to be able to edit the notes using reStructuredText, which is a very nice sort of markup, very natural, and see the result as HTML (almost) on a QTextBrowser widget.
So, I want that, when the user chooses the "HTML" tab, it will parse the text of the "Text" tab, and display it.
Piece of cake. Just create a function Window.changedTab, and connect it to the right signal!
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 looks:
Current time: 13:37 Man, I'm slowing down! Mostly because I had no idea how to use reStructuredText, though ;-)
This is the tricky part. I intend to use the tree to hold the data.
I will create my own noteItem class inheriting the usual QListviewItem, and store the note's rendered and raw version in there. Also, it will be drag&drop enabled, 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 insert these guys in the tree. I will have to add a new action in the main window. Let's call it newNote, and put it in the menu. I also bound it to Ctrl-N, and connect it to a brand new newNoteSlot().
Also, I removed the fileOpen and fileNew actions, because they make no sense in this app.
Note that all the above is done via designer. But, I will have to implement Window.newNoteSlot() if I want it to do something useful.
Not that it's specially hard, though!
def newNoteSlot(self): noteItem(self.tree)
In order to make it hierarchical , though, some extra work is needed. The QlistView (our tree widget) needs to cooperate in the dragging and dropping of items.
Now, this is somewhat black magic, but take it from me, it's harder in C++ ;-)
The main problem is that QListViewItem itself doesn't implement the methods that handle dragging and dropping, so you really need to subclass it.
But, if you subclass, you can't just put the widget there using designer. You would need to add a "custom widget" in designer, and it's somewhat of a mess.
So... enter python's cute dynamic nature.
I define a function that handles the dropping, one for the dragging, and an event handler. Then I override the class :-)
This will go into the Window constructor:
self.w.tree.__class__.dragObject=dragNote self.w.tree.__class__.dropEvent=dropNote self.w.tree.__class__.dragEnterEvent=dragNoteEnterEvent
And here are the functions:
#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 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 obvious, at least it is the same whenever you want to do a tree widget that drags and drops.
Only problem is it only copy-drags, while move-drag would seem more natural. No idea how to fix it.
Notice how the note is pickled very simply: first line is the note name, the rest is the reStructuredText contents.
Current time: 14:48 Took a while, mostly because I forgot the second argument to QTextDrag and was causing a segfault.
Binding the tree
Now, we can create notes, but how can we make the editor edit their contents? Piece o'cake.
Just connect the tree's selectedItem signal to a slot that updates the viewer and editor widgets, and we have half of it done. Designer 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 because sometimes you can have no item selected, for example if you delete all items.
While this works, it's only half of it. It shows, on the viewer/editor, the contents of the note. But it doesn't save the changes you make into the note. Since all our notes are empty, this does pretty much nothing ;-)
So, we also need to save changes made to the note in the editor into the noteItem's rest field.
But how? This is somewhat tricky. Since using noteItem.setRest() parses the data, it's slow. So, we will save the data whenever one of the following things happen:
The writer switches to the viewer. Since we have to parse anyway, it's a good moment.
Whenever a note has been modified and loses focus.
The first is simple. We only need to modify Window.changedTab as follows:
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 second is a bit harder, but not much.
We need to make the editor remember what noteItem the note belongs to, then make it save the data there on noteChanged.
So, this is how it looks:
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.
Also, since parsing reStructuredText can take a second or two, and we don't want the app to look frozen, I changed noteItem.setRest 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 exception handling is to avoid falling out of the function with a wait cursor. Of course that's not really correct error handling ;-)
And voila, the app now works.
Of course there are some minor issues. Like, say, saving ;-)
Current time: 15:22
Saviour and Loader
The codeword here is serialization.
Simply put, I need to take the tree structure in the QListView and turn it into some sort of text file, and viceversa.
Well, I hear you all chanting XML! XML! back there. I am clueless about XML, so I will have to go do some research into the python library. Be right back.
Current time: 15:27
Ok, I am not going to learn XML in the next 15 minutes. Let's do it using something else.
Let's try... pickle.
What I need to do is implement the Window.fileSave slot.
Here's an idea. I will traverse the tree, and create simple python structure that represents it. Each node will have a title, a rest content, an html content, and an array of children.
Then I pickle the root node, and the rest is magic.
Here's the code. While this is probably not orthodox, it works, so don't complain too much ;-)
First two helper classes to traverse 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 actual load and save functions:
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)
Notice how you don't choose what file you load. There is only one, and it opens and saves automatically.
To make it open automatically, I added a self.fileLoad() call in the window constructor, and to make it save automatically, I added a self.fileSave 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 create an instance of it in the window constructor, and it's done (the delay is in minutes, BTW)
Also, to make it save on exit, implemented the Window.fileExit slot in a trivial manner.
def close(self,alsoDelete): self.fileSave() WindowBase.close(self,True) def fileExit(self): self.close()
And voilà, the app is now really an app, and this is a real article.
Current time: 16:18
Not bad for three and a half hours of work! In fact, Notty is good enough to contain this whole article as a note :-)
Of course, lots of work remain to be done, spit&polish. But that's for the next issue.