Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Publicaciones sobre qt (publicaciones antiguas, página 1)

PyQt by example (Session 2)

I am fi­nal­ly pub­lish­ing my Lati­noWare 2008 tu­to­ri­al, in re­vised and ex­pand­ed for­m. It will prob­a­bly be a 10-­part se­ries, and here is ses­sion 2.

See al­so ses­sion 1.

Using assistant from PyQt

The uRSSus doc is slow­ly grow­ing, so I hooked as­sis­tant to the UI. Not dif­fi­cult, but I have not seen it else­where.

Here's how:

As­sume the "Hand­book ac­tion" is called ac­tion_Hand­book. Set win­dow.as­sis­tant to None in __init__.

def on_action_Handbook_triggered(self, i=None):
    if i==None: return

    if not self.assistant or \
       not self.assistant.poll()==None:

        helpcoll=os.path.join(os.path.abspath(os.path.dirname(__file__)),\
                              'help',\
                              'out',\
                              'collection.qhc')
        cmd="assistant -enableRemoteControl -collectionFile %s"%helpcoll
        self.assistant=subprocess.Popen(cmd,
                                        shell=True,
                                        stdin=subprocess.PIPE)
    self.assistant.stdin.write("SetSource qthelp://urssus/doc/handbook.html\n")

And that's it. Now I ned to fig­ure out con­text help.

Now you, too can create Qt Help Files painlessly

I de­cid­ed to add a man­u­al for uRSSus. Since it's a Qt ap­p, I checked how to do one of those neat help files, like the ones that come with Qt app­s.

It turns out it's not so sim­ple to cre­ate one of those.

You need to cre­ate your help in one or more HTML files, then cre­ate a XML file that de­scribes what each file is, ref­er­ences for each sec­tion, and ref­er­ences for each key­word you want in the in­dex.

For any re­al-life-­size doc­u­men­t, that's go­ing to be in­cred­i­bly an­noy­ing.

So, I took my usu­al es­cape route when I don't want to do grunt work on doc­s: Do­cu­tils.

Specif­i­cal­ly, I wrote rst2qhc which takes one (or more) re­struc­tured text files, and cre­ates a nice and clean Qt Help Project file from them, in­clud­ing sec­tion ti­tles, ref­er­ences and key­word­s, which you mark on the text us­ing the 'key­word' role.

What does this mean? Let me be graph­i­cal:

rst2qhc2

The one on the right is nice to read. The one on the left is nice to write. The ar­row in the mid­dle is rst2qhc :-)

And here's how the gen­er­at­ed qhp file looks for a triv­ial out­line of a man­u­al (and the rea­son why I don't want to do this man­u­al­ly ;-):

<?xml version="1.0" encoding="UTF-8"?>
<QtHelpProject version="1.0">
    <namespace>urssus</namespace>
    <virtualFolder>doc</virtualFolder>
    <customFilter name="Unknown">
        <filterAttribute></filterAttribute>
    </customFilter>
    <filterSection>
        <filterAttribute></filterAttribute>
        <toc>
            <section title="The uRSSus Handbook" ref="manual.html">
                <section title="Introduction" ref="manual.html#introduction"/>
                <section title="Quick Start" ref="manual.html#quick-start"/>
                <section title="Configuration" ref="manual.html#configuration"/>
                <section title="Credits and License" ref="manual.html#credits-and-license"/>
            </section>
        </toc>
        <keywords>
                <keyword name="what can you" ref="manual.html#what-can-you-do-using-urssus"/>
                <keyword name="quick" ref="manual.html#quick-start"/>
                <keyword name="main window" ref="manual.html#the-main-window"/>
        </keywords>
        <files>
                <file>manual.html</file>
        </files>
    </filterSection>
</QtHelpProject>

Al­so, as a bonus, you can cre­ate PDF, La­TeX, HTML and ODT files from the same source (heck, you can cre­ate freak­ing man pages).

Indeed screw all gui builders... for java!

Read­ing DZone I ran in­to an in­ter­est­ing post ti­tled Screw all GUI builders which ad­vo­cates drop­ping all GUI builders and in­stead cod­ing your UI by hand.

I al­ways ad­vo­cate us­ing Qt De­sign­er in­stead of cod­ing by hand, so I want­ed to see, even it's talk­ing about Java, I thought, "why don't I feel that way"?

My con­clu­sion? I don't see it be­cause in PyQt we are just lucky be­cause our tools don't suck quite as much.

It gives sev­er­al ar­gu­ments:

  • You don't know how ex­ac­t­­ly the gen­er­at­ed code work­s. You don't need to. You start not to care and GUI ap­­pli­­ca­­tion de­vel­op­­ment be­­comes a process of draw­ing and adding sim­­ple event han­dlers here and there.

To that, my re­ac­tion was yes, in­deed I don't care, as long as it work­s, which it has 99.99% of the time. When it did­n't, I did un­der­stand the gen­er­at­ed code, though.

The rea­son for this will be more ob­vi­ous once you see the code in both cas­es, I think.

  • Most GUI builders force you to use sin­­gle class for sin­­gle win­­dow, so gen­er­at­ed class­es tend to have thou­sands of lines of code.

Hm­m­m... I re­al­ly don't know what this means in con­tex­t, but that's just my ja­va ig­no­rance. OTO­H, yes, one class per win­dow, but al­most nev­er thou­sands of lines of code.

For ex­am­ple, the UI for uRSSus main win­dow is quite com­plex, and it is on­ly 530 lines of code. Here's how it look­s, so you see it's not a triv­ial win­dow:

urssus23 * Most GUI builders don't want you to modify the generated code. And if you do, they either break or rewrite your code.

In­deed De­sign­er's code is not meant to be mod­i­fied. That's what in­her­i­tance is there for. Over­load what­ev­er you want changed. Maybe this is eas­i­er in PyQt be­cause Python is more dy­nam­ic than Java? Not sure.

  • GUI builders force you to use an IDE, most­­ly one you start­ed cod­ing with. So if you start with Net­Bean­s, you most like­­ly be forced to stay with it for the whole pro­jec­t.

Just not true for De­sign­er. I can see how that would suck, though.

  • The gen­er­at­ed code is far from be­ing op­ti­­mal. It's not re­­size-friend­­ly, not dy­­nam­ic enough, it has many hard-­­cod­ed val­ues, refac­­tor­ing is most like­­ly im­­pos­si­ble, be­­cause builder would not al­low that.

De­sign­er does gen­er­ate re­size-friend­ly di­alogs if you use it cor­rect­ly. The hard-­cod­ed val­ues are run­time-ed­itable, refac­tor­ing is pret­ty sim­ple (take what­ev­er you wan­t, cre­ate a wid­get with it?)

And since I want­ed to com­pare ap­ples to ap­ples...

Here is his di­alog, done via PyQt and De­sign­er:

hawkscope1

No, I did­n't both­er in­sert­ing text in the di­a­log so it loks the same ;-)

The "close" but­ton clos­es the win­dow, the URL opens in your sys­tem brows­er.

The text wid­get ac­tu­al­ly sup­ports a sub­set of HTM­L, so there is a valid HTML doc­u­ment there, in­stead of just plain tex­t.

Al­so, an­oth­er thing is ... De­sign­er's (or rather pyuic's) gen­er­at­ed code is straightor­ward stuff.

Here's the code, which I think is rough­ly equiv­a­lent to his, on­ly it's just 123 LOC, in­stead of 286 (his gen­er­at­ed ver­sion) or 279 (his hand-­made ver­sion). And I did­n't delete the com­ments.

If I were do­ing this for re­al, all wid­gets would have de­scrip­tive names in­stead of Push­But­ton1 and what­ev­er.

Al­so, it's i18n-ready, un­like the Ja­va ver­sion­s, un­less I missed some­thing.

You can al­so get hawkscope.ui from this site to play with, it's done with De­sign­er from Qt 4.4.

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'hawkscope.ui'
#
# Created: Fri Feb 13 22:39:47 2009
#      by: PyQt4 UI code generator 4.4.4
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(444, 357)
        self.verticalLayout_2 = QtGui.QVBoxLayout(Dialog)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.horizontalLayout_2 = QtGui.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.label_2 = QtGui.QLabel(Dialog)
        self.label_2.setPixmap(QtGui.QPixmap("hawkscope.png"))
        self.label_2.setAlignment(QtCore.Qt.AlignCenter)
        self.label_2.setObjectName("label_2")
        self.horizontalLayout_2.addWidget(self.label_2)
        self.verticalLayout = QtGui.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.label_3 = QtGui.QLabel(Dialog)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label_3.setFont(font)
        self.label_3.setObjectName("label_3")
        self.verticalLayout.addWidget(self.label_3)
        self.label_4 = QtGui.QLabel(Dialog)
        self.label_4.setObjectName("label_4")
        self.verticalLayout.addWidget(self.label_4)
        self.gridLayout = QtGui.QGridLayout()
        self.gridLayout.setObjectName("gridLayout")
        self.label_5 = QtGui.QLabel(Dialog)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label_5.setFont(font)
        self.label_5.setObjectName("label_5")
        self.gridLayout.addWidget(self.label_5, 0, 0, 1, 1)
        self.label_10 = QtGui.QLabel(Dialog)
        self.label_10.setObjectName("label_10")
        self.gridLayout.addWidget(self.label_10, 0, 1, 1, 1)
        self.label_9 = QtGui.QLabel(Dialog)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label_9.setFont(font)
        self.label_9.setObjectName("label_9")
        self.gridLayout.addWidget(self.label_9, 1, 0, 1, 1)
        self.label_8 = QtGui.QLabel(Dialog)
        self.label_8.setObjectName("label_8")
        self.gridLayout.addWidget(self.label_8, 1, 1, 1, 1)
        self.label_7 = QtGui.QLabel(Dialog)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label_7.setFont(font)
        self.label_7.setObjectName("label_7")
        self.gridLayout.addWidget(self.label_7, 2, 0, 1, 1)
        self.label_6 = QtGui.QLabel(Dialog)
        self.label_6.setOpenExternalLinks(True)
        self.label_6.setObjectName("label_6")
        self.gridLayout.addWidget(self.label_6, 2, 1, 1, 1)
        self.verticalLayout.addLayout(self.gridLayout)
        self.horizontalLayout_2.addLayout(self.verticalLayout)
        self.verticalLayout_2.addLayout(self.horizontalLayout_2)
        self.label = QtGui.QLabel(Dialog)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label.setFont(font)
        self.label.setObjectName("label")
        self.verticalLayout_2.addWidget(self.label)
        self.textBrowser = QtGui.QTextBrowser(Dialog)
        self.textBrowser.setObjectName("textBrowser")
        self.verticalLayout_2.addWidget(self.textBrowser)
        self.horizontalLayout = QtGui.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.pushButton = QtGui.QPushButton(Dialog)
        self.pushButton.setObjectName("pushButton")
        self.horizontalLayout.addWidget(self.pushButton)
        self.pushButton_2 = QtGui.QPushButton(Dialog)
        self.pushButton_2.setObjectName("pushButton_2")
        self.horizontalLayout.addWidget(self.pushButton_2)
        self.verticalLayout_2.addLayout(self.horizontalLayout)

        self.retranslateUi(Dialog)
        QtCore.QObject.connect(self.pushButton_2, QtCore.SIGNAL("clicked()"), Dialog.accept)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "Dialog", None, QtGui.QApplication.UnicodeUTF8))
        self.label_3.setText(QtGui.QApplication.translate("Dialog", "Hawkscope", None, QtGui.QApplication.UnicodeUTF8))
        self.label_4.setText(QtGui.QApplication.translate("Dialog", "Access anything with single click!", None, QtGui.QApplication.UnicodeUTF8))
        self.label_5.setText(QtGui.QApplication.translate("Dialog", "Version:", None, QtGui.QApplication.UnicodeUTF8))
        self.label_10.setText(QtGui.QApplication.translate("Dialog", "0.4.1", None, QtGui.QApplication.UnicodeUTF8))
        self.label_9.setText(QtGui.QApplication.translate("Dialog", "Released:", None, QtGui.QApplication.UnicodeUTF8))
        self.label_8.setText(QtGui.QApplication.translate("Dialog", "2009-02-06", None, QtGui.QApplication.UnicodeUTF8))
        self.label_7.setText(QtGui.QApplication.translate("Dialog", "Homepage:", None, QtGui.QApplication.UnicodeUTF8))
        self.label_6.setText(QtGui.QApplication.translate("Dialog", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Droid Sans\'; font-size:8pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><a href=\"http://hawkscope.googlecode.com\"><span style=\" text-decoration: underline; color:#3c7dbe;\">http://hawkscope.googlecode.com</span></a></p></body></html>", None, QtGui.QApplication.UnicodeUTF8))
        self.label.setText(QtGui.QApplication.translate("Dialog", "Environment", None, QtGui.QApplication.UnicodeUTF8))
        self.textBrowser.setHtml(QtGui.QApplication.translate("Dialog", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Droid Sans\'; font-size:8pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Text goes here</p>\n"
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"></p></body></html>", None, QtGui.QApplication.UnicodeUTF8))
        self.pushButton.setText(QtGui.QApplication.translate("Dialog", "C&opy To Clipboard", None, QtGui.QApplication.UnicodeUTF8))
        self.pushButton.setShortcut(QtGui.QApplication.translate("Dialog", "Alt+O", None, QtGui.QApplication.UnicodeUTF8))
        self.pushButton_2.setText(QtGui.QApplication.translate("Dialog", "&Close", None, QtGui.QApplication.UnicodeUTF8))

Contents © 2000-2023 Roberto Alsina