Python - wprowadzenie

10 - Programy z GUI - PyQt5 cz. I

Dotychczas pisane przez nas programy uruchamialiśmy i komunikowaliśmy się z nimi w trybie tekstowym. W wielu przypadkach jest to wystarczające, a często nawet wygodniejsze niż używanie programów ‘‘okienkowych’’. Czasem jednak, zwłaszcza jeśli piszemy program dla użytkowników nieobeznanych z pracą w terminalu, lepiej jest napisać program z GUI, czyli z graficznym interfejsem użytkownika (ang. graphical user interface). Jak widać np. na stronie Wikipedii, dostępnych jest wiele platform programistycznych (frameworków) i narzędzi umożliwiających tworzenie ,,okienkowych'' aplikacji w języku Python. Należą do nich m.in. TkInter, PyGtk, wxPython, czy PyQt. Na naszych zajęciach zetkniemy się z tym ostatnim, dokładniej PyQt5. Warto wspomnieć, że jeśli planujemy rozpowszechnianie napisanych przez nas programów, warto się zapoznać z odpowiednimi licencjami dla frameworków/narzędzi/bibliotek, które wykorzystujemy.

Strona domowa projektu PyQt znajduje się pod adresem https://riverbankcomputing.com/software/pyqt. Bedziemy także wykorzystywać program Qt Designer, o którym można poczytać na stronie: https://doc.qt.io/qt-5/qtdesigner-manual.html.

Qt Designer i pierwszy program z GUI

PyQt a także Qt Designer powinny być zainstalowane wraz ze środowiskiem Anaconda.

Otwórz Anaconda Prompt i uruchom polecenie:

designer

Pojawi się okno programu Qt Designer.

Qt Designer

Widać okno główne programu i mniejsze okno, w którym możemy wybrać szablon nowej aplikacji. Wybierzmy pierwszą opcję: Dialog without Buttons i zatwierdźmy wybór klikając Utwórz. W głównym oknie aplikacji pojawi się okienko z projektem interfejsu naszej aplikacji: Qt Designer

Po lewej stronie okna znajduje się panel z pogrupowanymi tematycznie widżetami, które możemy przeciągać do okienka z projektem interfejsu aplikacji.

Przeciągnij z lewego panelu widżety Push Button (sekcja Buttons) i Label (sekcja Display Widgets), powiększ rozmiar, tak aby uzyskać mniej więcej taki wygląd interfejsu:

Qt Designer

Zaznacz teraz wstawiony przycisk i w prawym panelu znajdź w Edytorze właściwości QAbstractButton i w linii text zmień wartość z PushButton na Kliknij!. Zauważ, że zmienił się napis na przycisku: Qt Designer Zmianę tekstu na przycisku można też wykonać prościej - jeśli podwójnie klikniemy na przycisku, napis stanie się edytowalny.

Następnie zmień właściwość objectName z pushButton na przycisk:

Qt Designer

Podobnie zmień nazwę obiektu Label na etykieta: Qt Designer

Oraz usuń domyślny tekst dla tego widżetu:

Qt Designer

Zapisz plik, najlepiej w osobnym katalogu, jako pierwsza_aplikacja.ui. Otwórz plik w edytorze testu i przeanalizuj jego zawartość. Jest to plik XML-owy, który zawiera informacje dotyczące budowy projektowanego przez nas interfejsu.

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Dialog</class>
 <widget class="QDialog" name="Dialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <widget class="QPushButton" name="przycisk">
   <property name="geometry">
    <rect>
     <x>160</x>
     <y>240</y>
     <width>80</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>Kliknij!</string>
   </property>
  </widget>
  <widget class="QLabel" name="etykieta">
   <property name="geometry">
    <rect>
     <x>170</x>
     <y>110</y>
     <width>59</width>
     <height>15</height>
    </rect>
   </property>
   <property name="text">
    <string/>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

W okienku Anaconda Prompt (pod Windows) lub w terminalu, wykonaj polecenie:

pyuic5 pierwsza_aplikacja.ui -o pierwsza_aplikacja_ui.py

Zostanie wygenerowany plik pierwsza_aplikacja_ui.py, którego zawartość wyglądać będzie mniej więcej tak:

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

# Form implementation generated from reading ui file 'pierwsza_aplikacja.ui'
#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(400, 300)
        self.przycisk = QtWidgets.QPushButton(Dialog)
        self.przycisk.setGeometry(QtCore.QRect(160, 240, 80, 23))
        self.przycisk.setObjectName("przycisk")
        self.etykieta = QtWidgets.QLabel(Dialog)
        self.etykieta.setGeometry(QtCore.QRect(170, 110, 59, 15))
        self.etykieta.setText("")
        self.etykieta.setObjectName("etykieta")

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

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
        self.przycisk.setText(_translate("Dialog", "Kliknij!"))

Jest to zakodowany, tym razem w języku Python, wygląd interfejsu naszego programu. Oczywiście moglibyśmy go napisać od razu jako kod Pythona, ale używanie Qt Designera może nieco ułatwić to zadanie.

Plik pierwsza_aplikacja_ui.py, jak wspomniałem, zawiera projekt interfejsu i nie będzie zawierał kodu odpowiedzialnego za samo działanie aplikacji. Ten umieścimy w osobnym pliku, w którym zaimportujemy pierwsza_aplikacja_ui.py. Takie oddzielenie kodu określającego wygląd od kodu odpowiedzialnego za działanie aplikacji znakomicie ułatwia uporządkowanie i późniejsze modyfikacje programu.

Utwórz plik pierwsza_aplikacja.py i umieść w nim poniższy kod:

# Import potrzebnych modułów 
import sys
from PyQt5.QtWidgets import QDialog, QApplication
# Import pliku z interfejsem użytkownika
from pierwsza_aplikacja_ui import *
# Klasa aplikacji, dziedzicząca po klasie `QDialog`
class Pierwsza(QDialog):
    # Tworzenie obiektu 
    def __init__(self):
        super().__init__()
        # Tworzenie interfejsu uzytkownika
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        # Wyświetlanie interfejsu
        self.show()
        # Powiązanie kliknięcia przycisku z funkcją 'napisz'
        self.ui.przycisk.clicked.connect(self.napisz)
    def napisz(self):
        """Ustawia napis na etykiecie"""
        self.ui.etykieta.setText('Witaj Świecie!')

# Uruchomienie obiektu aplikacji
aplikacja = QApplication(sys.argv)
# Tworzenie okna
okno = Pierwsza()
# WYświetlenie okna
okno.show()
# Przy zamknięciu aplikacji, zwalnia się pamięć komputera
sys.exit(aplikacja.exec_())

Następnie uruchom program:

python pierwsza_aplikacja.py

Powinno się pojawić takie okienko:

Qt Designer

Kliknij w przycisk, pojawi się napis:

Qt Designer

Niestety, jak widać, widżet Label nie jest w stanie pomieścić całego napisu “Witaj Świecie”. Zatem wróćmy do QT Designera. Kliknij w widget Label, ponieważ może nie być widoczny, możesz go wybrać z panelu po prawej (Hierarchia obiektów), wtedy zaznaczy się na projekcie interfejsu.

Qt Designer

Teraz używając niebieskich ,,uchwytów'' rozciągnij widżet Label a w panelu po prawej, we Właściwościach ustaw aligment->poziomo na Wyrównaie w poziomie do środka.

Qt Designer

Zapisz ponownie plik pierwsza_aplikacja.ui. Wykonaj polecenie:

pyuic5 pierwsza_aplikacja.ui -o pierwsza_aplikacja_ui.py

Uruchom program:

python pierwsza_aplikacja.py

Kliknij na przycisk Kliknij!. Tym razem powinien się pokazać cały napis:

Qt Designer

Wróćmy teraz na chwilę do pliku pierwsza_aplikacja.py. Znajduje się tam taka linia: self.ui.przycisk.clicked.connect(self.napisz) Ustanowiliśmy tu połączenie między widżetem przycisk a funkcją napisz(). Mamy tu do czynienia z mechanizmem sygnałów i slotów (ang. signals and slots). Sygnały pojawiają się w związku z jakimiś wydarzeniami, takimi jak kliknięcie przycisku (czy innego widżetu), umieszczenie kursora nad widżetem, czy wpisanie tekstu do okienka z tekstem. Sygnał możemy powiązać ze slotem, którym jest funkcja/metoda, która będzie wywoływana w odpowiedzi na zaistnienie danego sygnału. W naszej pierwszej okienkowej aplikacji sygnałem jest kliknięcie przycisku przycisk a slotem jest funkcja napisz(). Jedno z drugim wiąże metoda connect(). Powiązania między sygnałami i slotami można także definiować w Qt Designerze, ale będziemy to raczej robić bezpośrednio modyfikując kod.

Nieco bardziej złożona aplikacja z GUI

Stwórzmy teraz drugą aplikację, która będzie tym razem robiła coś pożytecznego, może się przydać np. przy projektowaniu starterów do reakcji PCR. Będzie zawierała okienko, w którym będziemy mogli wpisać sekwencję nukleotydów. Po kliknięciu w przycisk, zostaną wypisane:

  • sekwencja komplementarna
  • sekwencja odwrócona komplementarna
  • nić RNA
  • sekwencja aminokwasów
  • liczba aminokwasów do znaku STOP Ponadto w trakcie wpisywania sekwencji na bieżąco będzie wyświetlana liczba wpisanych zasad.

Docelowo, aplikacja będzie wyglądała tak:

Sekwencje

Posłużymy się, rzecz jasna także Biopython-em.

Utwórz nowy formularz w Qt Designerze, taj jak poprzednio (Dialog without Buttons). Zapisz jako sekwencje.ui, nawiasem mówiąc ponieważ QT Designer nie zawsze jest stabilny, warto dość często zapisywać kolejne etapy pracy.

Następnie utwórz interfejs użytkownika tak jak poniżej.

Najpierw zmień rozmiar interfejsu na ok. 550 x 225. Rozmiar zmienia się przeciągając krawędzie okienka, wymiary widać w prawym panelu w sekcji geometry, można także tam dokonać ich modyfikacji.

Sekwencje

Następnie zmieniamy windowTitle na Sekwencje:

Sekwencje

W Panelu widżetów znajdź sekcję Layouts. Znajdują się tam widżety, które ułatwiają uporządkowanie umieszczonych w nich innych widżetów.

Sekwencje

Wybierz Form Layout, przeciągnij do okienka z naszą aplikacją i zmień rozmiar na mniej więcej taki:

Sekwencje

Ten rodzaj widżetu typu Layout jest przeznaczony do rozmieszczenia widżetów w taki sposób jaki często znajdujemy w formularzach, czyli sąsiadują ze sobą opisy pól i miejsca gdzie wprowadzamy odpowiednie informacje/odpowiedzi. My wykorzystamy go nieco inaczej. W pierwszym rzędzie będziemy wprowadzać dane, a w kolejnych będą one wyświetlane, ale w taki sposób, żeby łatwo można było je kopiować.

W widżecie Form Layout umieść widżet Label. Zwróć uwagę, żeby tak umieścić tę kontrolkę aby znalazła się po lewej stronie a po prawej pojawił się czerwony obrys przeznaczony dla następnego widżetu:

Sekwencje

W obrysie umieść widget Line Edit.

Sekwencje

Teraz zmieniamy w Label wyświetlany tekst na Sekwencja DNA oraz nazwę widgetu na sekwencjaLabel.

Sekwencje

Sekwencje

Zmień też nazwę widżetu Line Edit na sekwencjaLineEdit.

Następnie w Panelu widżetów znajdź sekcję Display Widget, wybierz Horizontal Line, przeciągnij na projekt aplikacji, poniżej widżetu sekwencjaLineEdit:

Sekwencje

Następnie dodamy cztery kolejne pary widzetów Label i Line Edit. Można to robić tak jak poprzednio, przeciągając widżety w odpowiednie miejsca, ale jest także inna możliwość. Kliknij dwukrotnie w pole widzetu Form Layout, pojawi się okienko, w którym wypełnij Tekst etykiety na Komplementarna. Zauważ, że automatycznie wypełniają się nazwy etykiety i nazwa pola.

Sekwencje

Dodaj w taki sam sposób kolejne etykiety:

Sekwencje

Następnie w czterech ostatnio dodanych widżetach Line Edit (ale nie pierwszym!) - zaznacz właściwość readOnly.

Sekwencje

Teraz wybierz z sekcji Layouts widget Horizontal Layout, który służy do rozmieszczenia widżetów w rzędzie. Umieść go na spodzie projektowanego interfejsu i dopasuj rozmiar.

Sekwencje

Umieść w nim kolejno widżety i ustaw odpowiednie właściwości:

  • Label - text: Dł. DNA/RNA:, objectName: domyślnie
  • Label - text: 0, objectName: dlNuklLabel
  • Vertical Line
  • Label - text: Dł. peptydu:, objectName: domyślnie
  • Label - text: -, objectName: dlPeptLabel
  • Vertical Line
  • Push Button - text: OK, objectName: okButton
  • Push Button - text: Zakończ, objectName: zakonczButton

Na końcu projekt powinien wyglądać tak:

Sekwencje

W katalogu, w którym został zapisany interfejs (plik sekwencje.ui) utwórz plik sekwencje.py. Umieść w nim kod:

# Import potrzebnych modułów 
import sys
from PyQt5.uic import loadUi
from PyQt5.QtWidgets import QDialog, QApplication
from Bio.Seq import Seq

# Import pliku z interfejsem użytkownika
# Klasa aplikacji, dziedzicząca po klasie `QDialog`
class Sekwencje(QDialog):
    # Tworzenie obiektu 
    def __init__(self):
        super().__init__()
        # Import interfejsu użytkownika bezpośrednio
        # z pliku sekwencje.ui
        loadUi("sekwencje.ui", self)
        self.show()
        # Powiązanie kliknięcia przycisków z funkcjami     
        self.okButton.clicked.connect(self.analizuj)
        self.zakonczButton.clicked.connect(self.zakoncz)
        # Powiązanie wydarzenia edycji tekstu z funkcją oblicz_dlugosc
        self.sekwencjaLineEdit.textEdited.connect(self.oblicz_dlugosc)
    def analizuj(self):
        # Pobranie sekwencji z widżetu, w którym ją wpisujemy 
        # i tworzenie obiektu typu Seq
        self.sekw = Seq(self.sekwencjaLineEdit.text())
        # Umieszczanie odpowiednich sekwencji w widżetach 
        # po przetworzeniu przez metody obiektu Seq
        # Sekwencja komplementarna
        self.komplementarnaLineEdit.setText(str(self.sekw.complement()))
        # Sekwencja odwrócona komplementarna
        self.odwrKomplementLineEdit.setText(str(self.sekw.reverse_complement()))
        # Sekwencja RNA
        self.sekwRNALineEdit.setText(str(self.sekw.transcribe()))
        # Sprawdzamy czy istnieją zasady nie tworzące kompletnego kodonu
        # w takim wypadku bez_kodonu != 0
        self.bez_kodonu = len(self.sekw) % 3
        # Jeśli kompletne kodony to...
        if  self.bez_kodonu == 0:
            # po prostu wpisujemy wynik translacji
            self.sekwAminokwLineEdit.setText(str(self.sekw.translate()))
        # Jeśli nie kompletne kodony 
        else:
            # wpisujemy wynik translacji odcinka bez zasad nie tworzących
            # kompletnego kodonu (dł. sekwencji - reszta z dzielenia %)
            self.sekwAminokwLineEdit.setText(
                str(self.sekw[:-self.bez_kodonu].translate())+
            '.'*self.bez_kodonu)
        # Długość nici aminokwasów do sygnału STOP
        self.dlPeptLabel.setText(
            str(len(self.sekw[:-self.bez_kodonu].translate(to_stop=True))))

    def oblicz_dlugosc(self):
        """Oblicza długość sekwencji zasad i umieszcza w widżecie"""
        self.dlNuklLabel.setText(str(len(self.sekwencjaLineEdit.text())))
        
    def zakoncz(self):
        """Wyłącza aplikację"""
        self.close()

if __name__ == "__main__":
    """Główna funkcja aplikacji"""
    # Uruchomienie obiektu aplikacji
    aplikacja = QApplication(sys.argv)
    # Tworzenie okna
    okno = Sekwencje()
    # WYświetlenie okna
    okno.show()
    # Przy zamknięciu aplikacji, zwalnia się pamięć komputera
    sys.exit(aplikacja.exec_())

Większość kodu wyjaśniłem w komentarzach, chciałbym jeszcze zwrócić uwagę na dwa fragmenty:

import sys
from PyQt5.uic import loadUi
...
loadUi("sekwencje.ui", self)

Tym razem nie użyliśmy narzędzia pyuic5 ale zastosowaliśmy loadUi() z modułu uic (User Interface Compiler) aby utworzyć interfejs użytkownika z pliku sekwencje.ui. Poznaliśmy zatem dwa sposoby korzystania z plików opisującyh GUI generowanych przez Qt Designer-a.

Zauważ także, że umieściliśmy część kodu, wywoływaną przy uruchamianiu programu, pod wyrażeniem:

if __name__ == "__main__":

Zapewne, jeśli korzystasz z książek, czy stron internetowych dotyczących programowania w języku Python, nie pierwszy raz się spotykasz z takim wyrażeniem. Choć jego użycie w tym programie nie jest konieczne, potraktuję go jako okazję, żeby wyjaśnić w skrócie jego znaczenie.

Przy uruchamianiu głównego pliku programu a także przy importowaniu modułów, jest wykonywany zawarty w nim kod. Zostaje przy tym ustawiona wartość pewnych specjalnych zmiennych jak. np. zmiennej __name__. Jeśli uruchamiany jest plik jako główny plik programu, to przyjmuje ona wartość __main__. Przy imporcie modułu, zmienna przyjmie nazwę modułu. Zatem, powyższe wyrażenie, zabezpiecza fragment kodu przed wykonaniem go, jeśli dany plik z kodem nie zostanie uruchomiony jako główny plik programu, ale jako zaimportowany moduł.

Więcej na ten temat można przeczytać np. tu.

Last updated on 27 Apr 2021
Published on 27 Apr 2021