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.
Będziemy 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.
Widać okno główne programu i mniejsze okno, w którym możemy wybrać szablon nowej aplikacji.
Wybierzmy trzecią 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:
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:
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:
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
:
Podobnie zmień nazwę obiektu Label
na etykieta
:
Oraz usuń domyślny tekst dla tego widżetu:
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, przejdź do katalogu, w którym znajduje się zapisany plik, a następnie 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:
Kliknij w przycisk, pojawi się napis:
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.
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
.
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:
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:
Posłużymy się, rzecz jasna, także Biopython-em.
Utwórz nowy formularz w Qt Designerze, tak jak poprzednio (Dialog without Buttons
).
Zapisz jako sekwencje.ui
. Należy tu zauważyć, że ponieważ QT Designer nie zawsze jest stabilny, warto dość często zapisywać kolejne etapy pracy.
Następnie utwórz interfejs użytkownika tak jak opisałem 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.
Następnie zmieniamy windowTitle
na 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.
Wybierz Form Layout
, przeciągnij do okienka z naszą aplikacją i zmień rozmiar na mniej więcej taki:
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:
W obrysie umieść widget Line Edit
.
Teraz zmieniamy w Label
wyświetlany tekst na Sekwencja DNA
oraz nazwę widgetu na sekwencjaLabel
.
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
:
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.
Dodaj w taki sam sposób kolejne etykiety:
Następnie w czterech ostatnio dodanych widżetach Line Edit
(ale nie pierwszym!) - zaznacz właściwość readOnly
.
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.
Umieść w nim kolejno widżety i ustaw odpowiednie właściwości:
Label
-text
:Dł. DNA/RNA:
,objectName
: domyślnieLabel
-text
:0
,objectName
:dlNuklLabel
Vertical Line
Label
-text
:Dł. peptydu:
,objectName
: domyślnieLabel
-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:
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()))
self.dlPeptLabel.setText(str(len(self.sekw.translate(to_stop=True))))
# 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.
Uruchom program:
python sekwencje.py
Zadanie
Utwórz aplikację, do której w okienku użytkownik wpisuje serię liczb a program wylicza i wyświetla podstawowe wartości statystyczne dla nich (średnia arytmetyczna, odchylenie standardowe, wariancja, mediana). Można wykorzystać moduł statistics
opisany w lekcji Biblioteki.