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.
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:
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, 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, 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.
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.