Come creare un'interfaccia GUI con PyQt



Introduzione

Sono molte le persone che vorrebbero imparare a costruire un'applicazione GUI, ma spesso non sanno nemmeno da dove iniziare. La maggior parte dei tutorial è basata su tecniche testuali, ed è veramente molto difficile imparare lo sviluppo di una GUI utilizzando solo testo, dal momento che le GUI sono principalmente uno strumento visuale.

Aggireremo l'ostacolo proponendo un esercizio per la costruzione di una semplice applicazione GUI, mostrando subito i vari passaggi delle fasi visuali, e completando quindi il tutto con l'aggiunta delle parti testuali. Una volta comprese le basi sarà facile aggiungere altro materiale.

Qt è un framework GUI (telaio di sviluppo), disponibile come software libero e open source, mentre PyQt è il wrapper Python per Qt, ossia l'insieme dei metodi Python per interfacciarsi a Qt. I due criteri guida per la realizzazione della nostra interfaccia GUI sono:

PyQt è lo strumento giusto perchè soddisfa abbastanza bene il primo criterio, e soddisfa sicuramente il secondo criterio. Nonostante la potenza dello strumento, con PyQt è abbastanza facile fare anche le cose semplici. In questo lavoro useremo PyQt per costruire l'interfaccia grafica per un semplice convertitore di temperature Celsius/Fahrenheit.

Tutto il lavoro di composizione sarà svolto graficamente, senza interventi manuali.


Prerequisiti

E' indispensabile che nel sistema usato per lo sviluppo ci sia oltre l'interprete Pyton anche il pacchetto QtDesigner e le librerie di supporto.
In ambito Linux Debian l'obiettivo si raggiunge immediatamente eseguendo da linea di comando:

apt-get install python-qt4 pyqt4-dev-tools qt4-designer

Questo lavoro didattico è stato prodotto tramite un computer con installato Debian e l'ambiente citato, in particolare con la versione 4.8.6 di Qt Designer e la versione 2.7.9 di Python.

Ma esiste anche un prodotto, Anaconda, (disponibile per Windows, Mac, Linux) che è in grado di creare automaticamente un completo ambiente di sviluppo per Python e Qt.

Lo strumento per lo sviluppo di GUI che abbiamo scelto è facile da usare, è basato sull metodologia grafica drag-and-drop e risulta l'ideale per questo tipo di lavoro. In estrema sintesi si prelevano i vari widget (pulsanti, caselle di testo, etichette, ecc) trascinandoli sull'area di lavoro, qui si ridimensionano e se ne modificano le proprietà, e poi la GUI builder genera automaticamente il codice necessario. Ecco come si presente Qt Designer:

pyqtba-01-640

Sulla sinistra notiamo tutti i vari tipi di widget, raccolti per categorie, che possono essere trascinati sulla finestra di lavoro centrale (MainWindow) e qui ridimensionati e posizionati. Sulla destra, tra i vari strumenti, c'è l'editor di proprietà che consente la modifica di nomi ed attributi vari. Quando si è completata la fase di disegno e di assegnazione delle proprietà si salva il lavoro come file .ui e si passa alla fase di creazione del codice. Avendo scelto Python come linguaggio di sviluppo si creerà un file Python contenente: il codice dell'applicazione, la logica del programma, i gestori di eventi per i vari widget usati nella GUI. Sia il codice di interfaccia grafica sia il codice dell'applicazione richiedono la libreria PyQt.


Preparazione dell'interfaccia

Dopo aver avviato QtCreator (abbiamo usato la versione 4), creiamo una finestra principale su cui posizionare i widget della nostra prima applicazione: 2 Push Button, 1 Line Edit, 1 Spin Box, 2 Label.

pyqtba-02

Una volta che i widget si trovano nella GUI è possibile riposizionarli e ridimensionarli tramite il mouse. Per posizionamenti e ridimensionamenti esatti si possono, anche, utilizzare le proprietà: X, Y, Width, Height.
Quando ne viene selezionato uno, le sue proprietà sono indicate nel Property Editor sulla destra. La prima di queste proprietà in elenco è l'objectName, che viene utilizzato per riferirsi all'oggetto nel codice Python. Abbiamo cambiato i nomi di 4 di questi per renderli più significativi: btn_CtoF, btn_FtoC, editCel, spinFahr. Non ci siamo preoccupati di rinominare, invece, i widget delle etichette dal momento che non è necessario farvi riferimento nel codice.
Un'ultima modifica la dobbiamo effettuare sulla proprietà maximum di spinFahr cambiandone il valore predefinito da 99 a 999, altrimenti non si potranno visualizzare valori elevati della temperatura Fahrenheit.
Appena ultimato il lavoro dell'interfaccia grafica possiamo salvarlo come tempconv.ui. Possiamo esaminarne il codice .ui con qualsiasi editor di testo per vedere cosa contiene. Si tratta soltanto di un file XML che descrive le posizioni e le proprietà dei vari widget GUI.


Preparazione del codice

Ora dobbiamo scrivere il codice per azionare i widget della GUI. In questo codice ci saranno dei gestori di eventi per i pulsanti, in modo che quando si fa clic su di essi, avverrà la conversione richiesta della temperatura e la visualizzazione del risultato.
Allora tramite un editor scriviamo un nuovo file, tempconv.py, come questo che segue:

# Temperature-conversion program using PyQt

import sys # blocco1
from PyQt4 import QtCore, QtGui, uic form_class = uic.loadUiType("tempconv.ui")[0] # Carica il file .ui
class MyWindowClass(QtGui.QMainWindow, form_class): # blocco2 def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, parent) self.setupUi(self) self.btn_CtoF.clicked.connect(self.btn_CtoF_clicked) # Associa i gestori di eventi ... self.btn_FtoC.clicked.connect(self.btn_FtoC_clicked) # ... ai pulsanti def btn_CtoF_clicked(self): # Gestore del pulsante CtoF cel = float(self.editCel.text()) fahr = cel * 9 / 5.0 + 32 self.spinFahr.setValue(int(fahr + 0.5)) def btn_FtoC_clicked(self): # Gestore del pulsante FtoC fahr = self.spinFahr.value() cel = (fahr - 32) * 5.0 / 9 self.editCel.setText(str(cel)) app = QtGui.QApplication(sys.argv) myWindow = MyWindowClass(None) myWindow.show() app.exec_()

Nota: nel listato ci sono 2 blocchi di codice evidenziati con uno sfondo diverso che chiameremo blocco1 e blocco2; il motivo risulterà chiaro alla fine dell'esercitazione.

Nella fase iniziale vengono importate le librerie PyQt e viene caricato il file tempconv.ui generato in precedenza con QtDesigner.

Viene, quindi, definita una classe che è la finestra principale dell'interfaccia. In questa classe vengono definite 3 funzioni:

Questo è tutto quello che c'è da fare per preparare una semplice GUI con l'utilizzo di PyQt. Adesso si può provare ad eseguirlo per assicurarsi che tutto funzioni correttamente. Come esercizio si può provare ad aggiungere altri elementi all'interfaccia ed a variare il codice secondo le proprie necessità specifiche.


Osservazioni

Il lavoro svolto comporta la presenza di 2 file: tempconv.ui e tempconv.py. Questo fatto, però, talvolta può risultare scomodo. Il file .ui è un file xml che può essere convertito in un file .py ed integrato con il file del codice Python. Vediamo come procedere nel nostro caso.
Da linea di comando eseguiamo:

pyuic4 tempconv.ui -o codice.py

Si otterrà il file codice.py che sarà molto simile al seguente listato:

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

# Form implementation generated from reading ui file 'tempconv.ui'
#
# Created: Thu Mar 31 05:47:46 2016
#      by: PyQt4 UI code generator 4.11.2
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    def _fromUtf8(s):
        return s

try:
    _encoding = QtGui.QApplication.UnicodeUTF8
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig)

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName(_fromUtf8("MainWindow"))
        MainWindow.resize(481, 274)
        self.centralwidget = QtGui.QWidget(MainWindow)
        self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
        self.btn_CtoF = QtGui.QPushButton(self.centralwidget)
        self.btn_CtoF.setGeometry(QtCore.QRect(100, 30, 280, 40))
        self.btn_CtoF.setObjectName(_fromUtf8("btn_CtoF"))
        self.btn_FtoC = QtGui.QPushButton(self.centralwidget)
        self.btn_FtoC.setGeometry(QtCore.QRect(100, 80, 280, 40))
        self.btn_FtoC.setObjectName(_fromUtf8("btn_FtoC"))
        self.editCel = QtGui.QLineEdit(self.centralwidget)
        self.editCel.setGeometry(QtCore.QRect(100, 150, 120, 22))
        self.editCel.setObjectName(_fromUtf8("editCel"))
        self.spinFahr = QtGui.QSpinBox(self.centralwidget)
        self.spinFahr.setGeometry(QtCore.QRect(270, 150, 110, 22))
        self.spinFahr.setMaximum(999)
        self.spinFahr.setObjectName(_fromUtf8("spinFahr"))
        self.label = QtGui.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(130, 180, 57, 14))
        self.label.setObjectName(_fromUtf8("label"))
        self.label_2 = QtGui.QLabel(self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(290, 180, 71, 16))
        self.label_2.setObjectName(_fromUtf8("label_2"))
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtGui.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 481, 19))
        self.menubar.setObjectName(_fromUtf8("menubar"))
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtGui.QStatusBar(MainWindow)
        self.statusbar.setObjectName(_fromUtf8("statusbar"))
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None))
        self.btn_CtoF.setText(_translate("MainWindow", "da Celsius a Fahrenheit  =-->", None))
        self.btn_FtoC.setText(_translate("MainWindow", "<--=  da Fahrenheit a Celsius", None))
        self.label.setText(_translate("MainWindow", "Celsius", None))
        self.label_2.setText(_translate("MainWindow", "Fahrenheit", None))

Per completare il lavoro dobbiamo fondere assieme i due listati python: quello che abbiamo appena ottenuto dalla conversione del file .ui e quello prodotto nel paragrafo Preparazione del codice. Per raggiungere il risultato definitivo occorrono 3 operazioni:

Infine per poter avviare automaticamente il programma suggeriamo di inserire, come prima riga, la direttiva seguente:

#!/usr/bin/env python

Si otterrà il listato definitivo comprendente tutto il codice Python necessario per l'interfaccia grafica e per l'algoritmo dell'applicazione. Per comodità riportiamo di seguito il codice completo:

#!/usr/bin/env python
# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'tempconv.ui' # # Created: Thu Mar 31 05:47:46 2016 # by: PyQt4 UI code generator 4.11.2 # # WARNING! All changes made in this file will be lost!
import sys
from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: def _fromUtf8(s): return s try: _encoding = QtGui.QApplication.UnicodeUTF8 def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig, _encoding) except AttributeError: def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig) class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(_fromUtf8("MainWindow")) MainWindow.resize(481, 274) self.centralwidget = QtGui.QWidget(MainWindow) self.centralwidget.setObjectName(_fromUtf8("centralwidget")) self.btn_CtoF = QtGui.QPushButton(self.centralwidget) self.btn_CtoF.setGeometry(QtCore.QRect(100, 30, 280, 40)) self.btn_CtoF.setObjectName(_fromUtf8("btn_CtoF")) self.btn_FtoC = QtGui.QPushButton(self.centralwidget) self.btn_FtoC.setGeometry(QtCore.QRect(100, 80, 280, 40)) self.btn_FtoC.setObjectName(_fromUtf8("btn_FtoC")) self.editCel = QtGui.QLineEdit(self.centralwidget) self.editCel.setGeometry(QtCore.QRect(100, 150, 120, 22)) self.editCel.setObjectName(_fromUtf8("editCel")) self.spinFahr = QtGui.QSpinBox(self.centralwidget) self.spinFahr.setGeometry(QtCore.QRect(270, 150, 110, 22)) self.spinFahr.setMaximum(999) self.spinFahr.setObjectName(_fromUtf8("spinFahr")) self.label = QtGui.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(130, 180, 57, 14)) self.label.setObjectName(_fromUtf8("label")) self.label_2 = QtGui.QLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(290, 180, 71, 16)) self.label_2.setObjectName(_fromUtf8("label_2")) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtGui.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 481, 19)) self.menubar.setObjectName(_fromUtf8("menubar")) MainWindow.setMenuBar(self.menubar) self.statusbar = QtGui.QStatusBar(MainWindow) self.statusbar.setObjectName(_fromUtf8("statusbar")) MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None)) self.btn_CtoF.setText(_translate("MainWindow", "da Celsius a Fahrenheit =-->", None)) self.btn_FtoC.setText(_translate("MainWindow", "<--= da Fahrenheit a Celsius", None)) self.label.setText(_translate("MainWindow", "Celsius", None)) self.label_2.setText(_translate("MainWindow", "Fahrenheit", None))
class MyWindowClass(QtGui.QMainWindow, Ui_MainWindow): # blocco2 def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, parent) self.setupUi(self) self.btn_CtoF.clicked.connect(self.btn_CtoF_clicked) # Associa i gestori di eventi ... self.btn_FtoC.clicked.connect(self.btn_FtoC_clicked) # ... ai pulsanti def btn_CtoF_clicked(self): # Gestore del pulsante CtoF cel = float(self.editCel.text()) fahr = cel * 9 / 5.0 + 32 self.spinFahr.setValue(int(fahr + 0.5)) def btn_FtoC_clicked(self): # Gestore del pulsante FtoC fahr = self.spinFahr.value() cel = (fahr - 32) * 5.0 / 9 self.editCel.setText(str(cel)) app = QtGui.QApplication(sys.argv) myWindow = MyWindowClass(None) myWindow.show() app.exec_()

Grazie alla direttiva inserita, come prima riga, si può rendere eseguibile il programma, senza invocare ogni volta l'interprete python; basterà eseguire, una tantum, il seguente comando:

chmod +x codice.py


Sviluppi ulteriori

Proviamo ad aggiungere qualche piccola variante alla nostra interfaccia grafica, 2 pulsanti: Help e Fine. Il primo per visualizzare una finestra di informazioni, il secondo per chiudere l'applicazione. In base alle convenzioni adottate nell'esercizio chiameremo i 2 nuovi pulsanti: btn_Help e btn_Quit. Il nuovo aspetto dell'interfaccia è riportato nella figura seguente:

pyqtba-03

Per quanto riguarda il codice, invece, dobbiamo aggiungere 2 righe al metodo __init__, che diverrà:

    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)
        self.setupUi(self)
        self.btn_CtoF.clicked.connect(self.btn_CtoF_clicked)  # Associa i gestori di eventi ...
        self.btn_FtoC.clicked.connect(self.btn_FtoC_clicked)  # ... ai pulsanti
self.btn_Help.clicked.connect(self.btn_Help_clicked) # nuovo pulsante Help self.btn_Quit.clicked.connect(self.btn_Quit_clicked) # nuovo pulsante Fine

E dobbiamo, infine, inserire i 2 metodi corrispondenti nella classe MyWindowClass:

def btn_Help_clicked(self): # Gestore del pulsante Help msgBox = QtGui.QMessageBox() msgBox.setText('Informazioni d\'aiuto.\nLeggere con attenzione.') ret = msgBox.exec_(); def btn_Quit_clicked(self): # Gestore del pulsante Quit app.quit()

La finestra di Help si presenterà come in figura:

pyqtba-04