Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

PyQt By Example (Session 4)

PyQt By Example: Session 4

Action!

Requirements

If you ha­ve not do­ne it ye­t, plea­se che­ck the pre­vious ses­sion­s:

All fi­les for this ses­sion are he­re: Ses­sion 4 at Gi­tHub. You can use the­m, or you can fo­llow the­se ins­truc­tions star­ting wi­th the fi­les from Ses­sion 3 and see how we­ll you wo­rke­d!

Action!

What's an Action?

When we fi­nis­hed ses­sion 3 we had a ba­sic to­do appli­ca­tio­n, wi­th ve­ry li­mited func­tio­na­li­ty: you can ma­rk ta­sks as do­ne, but you can't edit the­m, you can't crea­te new ones, and you can't re­mo­ve the­m, mu­ch le­ss do things like fil­te­ring the­m.

/static/tut3-window5.png

A ve­ry li­mited appli­ca­tion

To­day we wi­ll start wri­ting co­de and de­sig­ning UI to do tho­se things.

The key con­cept he­re is Ac­tion­s.

  • He­l­­p? Tha­­t's an ac­­tion

  • Open Fi­­le? Tha­­t's an ac­­tion

  • Cut / Co­­­py / Pa­s­­te? Tho­­­se are ac­­tions too.

Le­t's quo­te The Fi­ne Ma­nua­l:

The QAc­tion cla­ss pro­vi­des an abs­tract user in­ter­fa­ce ac­tion that can be in­serted in­to wi­dge­ts.

In appli­ca­tions many co­m­mon co­m­man­ds can be in­vo­ked via me­nus, tool bar bu­tton­s, and ke­y­board shor­tcu­ts. Sin­ce the user ex­pec­ts ea­ch co­m­mand to be per­for­med in the sa­me wa­y, re­gard­le­ss of the user in­ter­fa­ce us­e­d, it is use­ful to re­pre­sent ea­ch co­m­mand as an ac­tio­n.

Ac­tions can be added to me­nus and tool bar­s, and wi­ll au­to­ma­ti­ca­lly keep them in syn­c. For exam­ple, in a word pro­ce­s­so­r, if the user pres­ses a Bold tool bar bu­tto­n, the Bold me­nu item wi­ll au­to­ma­ti­ca­lly be che­cke­d.

A QAc­tion may con­tain an ico­n, me­nu tex­t, a shor­tcu­t, sta­tus tex­t, "Wha­t's This?" tex­t, and a tool­ti­p.

The beau­ty of ac­tions is that you do­n't ha­ve to co­de things twi­ce. Why add a "Co­p­y" bu­tton to a tool ba­r, then a "Co­p­y" me­nu en­tr­y, then wri­te two hand­ler­s?

Crea­te ac­tions for eve­r­y­thing the user can do then plug them in your UI in the ri­ght pla­ce­s. If you put them in a me­nu, it's a me­nu en­tr­y. If you put it in a tool ba­r, it's a bu­tto­n. Then wri­te a hand­ler for the ac­tion, con­nect it to the ri­ght sig­na­l, and you are do­ne.

Le­t's start wi­th a sim­ple ac­tio­n: Re­mo­ve Ta­sk. We wi­ll be doing the first half of the jo­b, crea­ting the ac­tion itself and the UI, using de­sig­ne­r.

Creating Actions in Designer

Firs­t, le­t's go to the Ac­tion Edi­tor and ob­vious­ly cli­ck on the "New Ac­tio­n" bu­tton and start crea­ting it:

/static/tut4-action1.png

Crea­ting a New Ac­tion

A few re­ma­rks:

  • If you do­­n't know whe­­re the "X" icon ca­­me fro­­­m, you ha­­ve not read ses­­sion 3 ;-)

  • The ac­tion­De­le­te_­Ta­sk ob­ject na­me is au­to­ma­ti­ca­lly ge­ne­ra­ted from the text fiel­d. In so­me ca­ses that can lead to aw­ful na­me­s. If tha­t's the ca­se, you can just chan­ge the ob­ject na­me.

  • The sa­­me text is us­ed for the ico­n­­Text and tool­­Tip pro­­­pe­r­­tie­s. If tha­­t's not co­­­rre­c­­t, you can chan­­ge it la­­te­­r.

On­ce you crea­te the ac­tio­n, it wi­ll not be ma­rked as "U­s­e­d" in the ac­tion edi­to­r. Tha­t's be­cau­se it exis­ts, but has not been ma­de avai­la­ble to the user an­ywhe­re in the win­dow we are crea­tin­g.

The­re are two ob­vious pla­ces for this ac­tio­n: a tool ba­r, and a me­nu ba­r.

Adding Actions to a Tool Bar

To add an ac­tion to a tool ba­r, first make su­re the­re is one. If you do­n't ha­ve one in your "Ob­ject Ins­pec­to­r", then ri­ght cli­ck on Mai­nWin­dow (ei­ther the ac­tual win­do­w, or its en­try in the ins­pec­to­r), and choose "A­dd Tool Ba­r".

You can add as many tool bars as you wan­t, but try to want on­ly one, un­le­ss you ha­ve a ve­ry good rea­son (we wi­ll ha­ve one in ses­sion 5 ;-)

After you crea­te the tool ba­r, you wi­ll see emp­ty spa­ce be­tween the me­nu bar (that sa­ys "Ty­pe He­re") and the ta­sk list wi­dge­t. That spa­ce is the tool ba­r.

Drag your ac­tio­n's icon from the ac­tion edi­tor to the tool ba­r.

Tha­t's it!

/static/tut4-action2.png

The De­le­te Ta­sk ac­tion is now in the tool ba­r.

Adding Actions to a Menu Bar

Agai­n, our me­nu bar is emp­ty, it has on­ly a sign sa­ying "Ty­pe He­re". Whi­le we could just drag our ac­tion to the me­nu ba­r, that would pla­ce "De­le­te Ta­sk" as a top le­vel me­nu, whi­ch is rea­lly an unu­sual choi­ce.

So, le­t's crea­te a "Ta­sk" me­nu firs­t:

  • Cli­­ck on the "Ty­­pe He­­re".

  • Ty­­pe "Ta­sk" (wi­­thout the quo­­­tes)

/static/tut4-action3.png

Crea­ting a me­nu

If you see the last ima­ge, by doing that we created a QMe­nu ob­ject ca­lled me­nu­Ta­sk (a­gai­n, the ob­ject na­me is ba­sed on what we ty­pe­d).

We want to add the de­le­te ta­sk ac­tion to that me­nu. To do tha­t, drag the ac­tion to the "Ta­sk" in the me­nu ba­r, then to the me­nu that appears when you do tha­t.

/static/tut4-action4.png

The De­le­te Ta­sk ac­tion is now in the me­nu bar

Now we ha­ve our ac­tion in the tool bar and in the me­nu ba­r. But of cour­se, it does no­thin­g. So, le­t's wo­rk on that nex­t.

Save it, run build.sh, let's move forward.

Deleting a Task

From session 2 you should remember AutoConnect. If you don't, refresh that session, because that's what we will use now. We want to do something when the actionDelete_Task object emits its triggered signal.

Therefore, we want to implement Main.on_actionDelete_Task_triggered (see why action's object names are important? I could have called it delete instead).

An Im­por­tant Is­sue

He­re we take a sma­ll de­tour be­cau­se the­re is a pro­blem wi­th Py­Qt whi­ch is mild­ly an­no­yin­g.

Con­si­der this tri­vial ver­sion of our me­tho­d:

def on_actionDelete_Task_triggered(self,checked=None):
    print "adtt",checked

Wha­t's printed if I cli­ck on the tool­bar bu­tto­n?

[ralsina@hp session4]$ python main.py
adtt False
adtt None

The sa­me thing ha­ppens if you se­lect "De­le­te Ta­sk" from the me­nu: our slot ge­ts ca­lled twi­ce. ­This pro­blem is the­re when you use Au­to­Con­nect for sig­nals wi­th ar­gu­men­ts that can al­so be emi­tted wi­thout ar­gu­men­ts.

How can you te­ll if tha­t's the ca­se? In the Qt do­cs they wi­ll be lis­ted wi­th de­fault ar­gu­men­ts.

For exam­ple, this sig­nal has the pro­ble­m:

void triggered ( bool checked = false )

This one does­n'­t:

void toggled ( bool checked )

The te­ch­ni­cal ex­pla­na­tion for this is ... in­vol­ved but the prac­ti­cal so­lu­tion is tri­via­l:

Make su­re che­cked is not No­ne in your slo­t:

def on_actionDelete_Task_triggered(self,checked=None):
    if checked is None: return

This wa­y, you wi­ll ig­no­re the slot ca­lled wi­th no ar­gu­men­ts, and run the real co­de on­ly on­ce.

And he­re is the real co­de, whi­ch is qui­te shor­t:

def on_actionDelete_Task_triggered(self,checked=None):
    if checked is None: return
    # First see what task is "current".
    item=self.ui.list.currentItem()

    if not item: # None selected, so we don't know what to delete!
        return
    # Actually delete the task
    item.task.delete()
    todo.saveData()

    # And remove the item. I think that's not pretty. Is it the only way?
    self.ui.list.takeTopLevelItem(self.ui.list.indexOfTopLevelItem(item))

Ex­cept for the last li­ne, that co­de should be ob­vious. The last li­ne? I am not rea­ll­y ­su­re it's even ri­ght but it wo­rks.

You can now test the feature. Remember that if you run out of tasks, you can execute python todo.py and get new ones.

Fine Tuning Your Actions

The­re are so­me in­ter­fa­ce pro­ble­ms wi­th our wo­rk so fa­r:

  1. The Ta­sk me­­nu and the De­­le­­te Ta­sk ac­­tion la­­ck ke­­y­­board sho­r­­tcu­­ts.

    This is ve­­ry im­­po­r­­tan­­t. It makes the app wo­­­rk be­­­tter for the re­­gu­­lar use­­r. Be­­­si­­des, often they wi­­ll ex­­pect the sho­r­­tcu­­ts to be the­­re and the­­re is no rea­­son to frus­­tra­­te your use­­r!

    Lu­­cki­­l­­y, this is tri­­vial to fix, just set the sho­r­­tcut pro­­­pe­r­­ty for ac­­tio­­n_­­De­­le­­te_­­Ta­sk, and chan­­ge me­­nu­­Ta­sk's text pro­­­pe­r­­ty to "&­­Ta­sk".

  2. The De­le­te Ta­sk ac­tion is ena­bled even when it does­n't appl­y. If you ha­ve no ta­sk se­lec­te­d, the user can tri­gger it, but it does no­thing. Tha­t's a bit sur­pri­sin­g, and sur­pri­sing your users is not ve­ry ni­ce, IMHO.

    The­re is so­me ar­gu­ment about this, no­ta­bly from Joel Spol­sky so ma­y­be I am just old fas­hio­ne­d!

    To selectively enable or disable your actions when there is/isn't a selected item in our task list, we need to react to our list's currentItemChanged signal. Here's the code:

    def on_list_currentItemChanged(self,current=None,previous=None):
        if current:
            self.ui.actionDelete_Task.setEnabled(True)
        else:
            self.ui.actionDelete_Task.setEnabled(False)

    Al­so, we need to make De­le­te Ta­sk start di­sa­bled be­cau­se we start the app wi­th no ta­sk se­lec­te­d. Tha­t's do­ne from de­sig­ner using its "e­na­ble­d" pro­per­ty.

    Sin­ce the­re is on­ly one De­le­te Ta­sk ac­tio­n, this co­de affec­ts the ta­sk bar and al­so the me­nu ba­r. That helps keep your UI con­sis­tent and we­ll-­be­ha­ve­d.

/static/tut4-window6.png

A ve­ry li­mited appli­ca­tion

Coming Soon

We­ll, that was a ra­ther long ex­pla­na­tion for a sma­ll fea­tu­re, was­n't it? Do­n't wo­rr­y, the next ac­tions wi­ll be mu­ch ea­sier to add, be­cau­se I ex­pect you to read "I added an ac­tion ca­lled New Ta­sk" and know what I am ta­lking abou­t.

And in the next ses­sion, we wi­ll do just tha­t. And we wi­ll crea­te our first dia­lo­g.

Further Reading


He­re you can see what chan­ged be­tween the old and new ver­sion­s:

Modified lines:  None
Added line:  44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62
Removed line:  None
Generated by diff2html
© Yves Bailly, MandrakeSoft S.A. 2001
diff2html is licensed under the GNU GPL.

  session3/main.py     session4/main.py
  60 lines
1557 bytes
Last modified : Thu Mar 5 02:03:34 2009

    79 lines
2300 bytes
Last modified : Sat Mar 7 02:06:29 2009

1 # -*- coding: utf-8 -*-   1 # -*- coding: utf-8 -*-
2   2
3 """The user interface for our app"""   3 """The user interface for our app"""
4   4
5 import os,sys   5 import os,sys
6   6
7 # Import Qt modules   7 # Import Qt modules
8 from PyQt4 import QtCore,QtGui   8 from PyQt4 import QtCore,QtGui
9   9
10 # Import the compiled UI module   10 # Import the compiled UI module
11 from windowUi import Ui_MainWindow   11 from windowUi import Ui_MainWindow
12   12
13 # Import our backend   13 # Import our backend
14 import todo   14 import todo
15   15
16 # Create a class for our main window   16 # Create a class for our main window
17 class Main(QtGui.QMainWindow):   17 class Main(QtGui.QMainWindow):
18     def __init__(self):   18     def __init__(self):
19         QtGui.QMainWindow.__init__(self)   19         QtGui.QMainWindow.__init__(self)
20   20
21         # This is always the same   21         # This is always the same
22         self.ui=Ui_MainWindow()   22         self.ui=Ui_MainWindow()
23         self.ui.setupUi(self)   23         self.ui.setupUi(self)
24   24
25         # Let's do something interesting: load the database contents   25         # Let's do something interesting: load the database contents
26         # into our task list widget   26         # into our task list widget
27         for task in todo.Task.query().all():   27         for task in todo.Task.query().all():
28             tags=','.join([t.name for t in task.tags])   28             tags=','.join([t.name for t in task.tags])
29             item=QtGui.QTreeWidgetItem([task.text,str(task.date),tags])   29             item=QtGui.QTreeWidgetItem([task.text,str(task.date),tags])
30             item.task=task   30             item.task=task
31             if task.done:   31             if task.done:
32                 item.setCheckState(0,QtCore.Qt.Checked)   32                 item.setCheckState(0,QtCore.Qt.Checked)
33             else:   33             else:
34                 item.setCheckState(0,QtCore.Qt.Unchecked)   34                 item.setCheckState(0,QtCore.Qt.Unchecked)
35             self.ui.list.addTopLevelItem(item)   35             self.ui.list.addTopLevelItem(item)
36   36
37     def on_list_itemChanged(self,item,column):   37     def on_list_itemChanged(self,item,column):
38         if item.checkState(0):   38         if item.checkState(0):
39             item.task.done=True   39             item.task.done=True
40         else:   40         else:
41             item.task.done=False   41             item.task.done=False
42         todo.saveData()   42         todo.saveData()
43   43
      44     def on_actionDelete_Task_triggered(self,checked=None):
      45         if checked is None: return
      46         # First see what task is "current".
      47         item=self.ui.list.currentItem()
      48
      49         if not item: # None selected, so we don't know what to delete!
      50             return
      51         # Actually delete the task
      52         item.task.delete()
      53         todo.saveData()
      54
      55         # And remove the item. I think that's not pretty. Is it the only way?
      56         self.ui.list.takeTopLevelItem(self.ui.list.indexOfTopLevelItem(item))
      57
      58     def on_list_currentItemChanged(self,current=None,previous=None):
      59         if current:
      60             self.ui.actionDelete_Task.setEnabled(True)
      61         else:
      62             self.ui.actionDelete_Task.setEnabled(False)
44   63
45 def main():   64 def main():
46     # Init the database before doing anything else   65     # Init the database before doing anything else
47     todo.initDB()   66     todo.initDB()
48   67
49     # Again, this is boilerplate, it's going to be the same on   68     # Again, this is boilerplate, it's going to be the same on
50     # almost every app you write   69     # almost every app you write
51     app = QtGui.QApplication(sys.argv)   70     app = QtGui.QApplication(sys.argv)
52     window=Main()   71     window=Main()
53     window.show()   72     window.show()
54     # It's exec_ because exec is a reserved word in Python   73     # It's exec_ because exec is a reserved word in Python
55     sys.exit(app.exec_())   74     sys.exit(app.exec_())
56   75
57   76
58 if __name__ == "__main__":   77 if __name__ == "__main__":
59     main()   78     main()
60   79

Generated by diff2html on Sat Mar 7 02:08:22 2009
Command-line:
/home/ralsina/bin/diff2html session3/main.py session4/main.py