Python - wprowadzenie

11 - Programy z GUI - PyQt5 cz. II

Teraz utworzymy aplikację, która będzie otwierała plik z danymi w formacie csv a następnie umożliwi wygenerowanie prostego wykresu słupkowego, który będzie można zapisać w kilku formatach.

Działająca aplikacja będzie wyglądać mniej więcej tak:

Wizualizator

Utworz nowy formularz, tym razem wybierając Main Window:

Wizualizator

Zmień rozmiar formularza na 700 x 420, możesz to ustawić w panelu właściwości po prawej (właściwość geometry: Szerokość i Wysokość). Zapisz w osobnym katalogu jako wizualizator.ui. Formularz powinien wyglądać mniej więcej tak:

Wizualizator

Jak widać, na górze okna pojawił się pasek Menu na którym znajduje się napis Wpisz tutaj. Kliknij dwukrotnie w ten napis i zmień jego treść na Plik. Jeśli teraz znów klikniesz na napis, pojawi się rozwijane Menu a w nim kolejny napis Wpisz tutaj.

Wizualizator

Teraz możemy dodać kolejne pozycje Menu.

Dodaj kolejno pozycje i ustaw ich nazwy (Właściwość -> QObject -> objectName):

  • Otwórz dane - nazwa: actionOtworz
  • Zapisz wykres - nazwa: actionZapisz
  • Zamknij aplikację - nazwa: actionZamknij

Menu powinno wyglądać tak:

Wizualizator

Teraz zmień dla pozycji Zapisz wykres właściwość QAction -> enabled na Fałsz, czyli trzeba odznaczyć ,,ptaszka’':

Wizualizator

Zmieniając tę właściwość spowodowaliśmy, że ta pozycja w menu będzie domyślnie nieaktywna. Uaktywni się, dopiero wtedy, gdy wygenerujemy wykres, ale to zakodujemy później.

Na górze formularza umieść widżet Label, a następnie:

  • rozciągnij go na szerokość okna (zachowując pewien margines)
  • zmień jego nazwę na otwartyPlik
  • ustaw jego wysokość na 16 (geometry -> Wysokość)
  • ustaw właściwość sizePolicy-> Strategia Pionowa na Fixed

Wizualizator

Przy wciąż zaznaczonym widżecie otwartyPlik znajdź właściwość styleSheet, kliknij w prawej kolumnie i pojawi się okienko z przyciskiem z trzema kropkami:

Wizualizator

Kliknij w ,,trzy kropki’’, pojawi się okienko:

Wizualizator

Możemy w nim edytować wygląd widżetów, przy pomocy kaskadowych arkuszy stylów (CSS) o których nie będziemy szerzej tu mówić, ,,wyklikamy’’ odpowiednie opcje. Kliknij na strzałkę w dół znajdującą się na prawo od Dodaj kolor i wybierz background-color:

Wizualizator

Pojawi się okienko, w którym wybierz jakiś kolor:

Wizualizator

Po zatwierdzeniu w poprzednim okienku pojawi się wybrana wartość:

Wizualizator

Zatwierdź. Teraz zmienił się kolor widżetu:

Wizualizator

Teraz z palety po lewej znajdź sekcję Item Views (Model-Based) i tam wybierz widżet Table View:

Wizualizator

Przeciągnij na formularz i umieść po lewej stronie stronie, powiększ tak aby zajmował mniej więcej taki obszar:

Wizualizator

Zmień nazwę widżetu na tabela.

Po prawej stronie umieść kolejny widżet Label a następnie:

  • zmień jego tło na dowolny kolor,
  • zmień jego wielkość tak aby zajmował podobny obszar jak tabela po jej prawej stronie
  • zmień nazwę na wykres
  • usuń domyślny tekst Teraz nasz formularz powinien wyglądać mniej więcej tak:

Wizualizator

W lewym dolnym rogu dodaj Push Button, zmień tekst na Generuj wykres, nazwa widżetu powinna zmienić się na generujButton, jeśli nie to zmień na taką:

Wizualizator

Zaznacz teraz widżety tabela i wykres a następnie kliknij na ikonę Rozmieść poziomo w splitterze.

Wizualizator

Splitter organizuje układ obu widżetów w poziomie i pozwoli na regulację szerokości obszaru zajmowanego przez oba widżety.

Całość się zmniejszyła:

Wizualizator

Przywróć widżetom poprzednią wielkość, przeciągając niebieskie kwadraty zaznaczenia.

Kliknij w miejsce formularza, gdzie nie znajduje się żaden z dodanych widżetów. Następnie kliknij na ikonę Rozmieść w siatce.

Wizualizator

Teraz formularz będzie wyglądał mniej więcej tak:

Wizualizator

Ustawiliśmy właśnie Top Level Layout. Dzięki tej operacji, widżety będą automatycznie zmieniały wielkość wraz ze zmianami wielkości okna.

Zmieniła się wielkość przycisku, ustawimy jego wielkość na stałą. Znajdź właściwość sizePolicy -> Strategia pozioma na Fixed. Przy okazji odznacz właściwość QWidget - enabled. Teraz przycisk będzie nieaktywny. Nie ma sensu, żeby był aktywny, dopóki nie wczytamy danych.

W końcu projekt naszego interfejsu będzie wyglądał mniej więcej tak:

Wizualizator

W panelu po prawej, w Hierarchii obiektów wybierz centralwidget i we Właściwościach zmień wartości marginesów na 10.

Wizualizator

Możesz podejrzeć wygląd aplikacji wybierając w menu Formularz -> Podgląd... (klawisze: Ctrl+R).

Oczywiście to tylko podgląd interfejsu, więc aplikacja jeszcze nie działa, ale można sprawdzić np. zachowanie widżetów w odpowiedzi na zmianę wielkości okna, czy przesuwanie ,,splittera’’ między widżetami tabela i wykres.

Zawartość pliku wizualizator.ui powinna być mniej więcej (mogą być różnice np. w wybranych kolorach, czy rozmiarach widżetów) taka:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>700</width>
    <height>422</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout">
    <property name="leftMargin">
     <number>10</number>
    </property>
    <property name="topMargin">
     <number>10</number>
    </property>
    <property name="rightMargin">
     <number>10</number>
    </property>
    <property name="bottomMargin">
     <number>10</number>
    </property>
    <item row="0" column="0">
     <widget class="QLabel" name="otwartyPlik">
      <property name="sizePolicy">
       <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
        <horstretch>0</horstretch>
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
      <property name="autoFillBackground">
       <bool>false</bool>
      </property>
      <property name="styleSheet">
       <string notr="true">background-color: rgb(226, 226, 226);</string>
      </property>
      <property name="text">
       <string/>
      </property>
     </widget>
    </item>
    <item row="1" column="0">
     <widget class="QSplitter" name="splitter">
      <property name="orientation">
       <enum>Qt::Horizontal</enum>
      </property>
      <widget class="QTableView" name="tabela"/>
      <widget class="QLabel" name="wykres">
       <property name="styleSheet">
        <string notr="true">background-color: rgb(230, 230, 230);</string>
       </property>
       <property name="text">
        <string/>
       </property>
      </widget>
     </widget>
    </item>
    <item row="2" column="0">
     <widget class="QPushButton" name="generujButton">
      <property name="enabled">
       <bool>false</bool>
      </property>
      <property name="sizePolicy">
       <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
        <horstretch>0</horstretch>
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
      <property name="text">
       <string>Generuj Wykres</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>700</width>
     <height>20</height>
    </rect>
   </property>
   <widget class="QMenu" name="menuPlik">
    <property name="title">
     <string>Plik</string>
    </property>
    <addaction name="actionOtworz"/>
    <addaction name="actionZapisz"/>
    <addaction name="actionZamknij"/>
   </widget>
   <addaction name="menuPlik"/>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
  <action name="actionOtworz">
   <property name="text">
    <string>Otwórz dane</string>
   </property>
  </action>
  <action name="actionZapisz">
   <property name="text">
    <string>Zapisz wykres</string>
   </property>
  </action>
  <action name="actionZamknij">
   <property name="text">
    <string>Zamknij aplikację</string>
   </property>
  </action>
 </widget>
 <resources/>
 <connections/>
</ui>

Teraz przygotujmy dwa pliki: tabela_model.py oraz wizualizator.py, które będą zawierały kod Pythona, który sprawi, ze nasza prosta aplikacja będzie nie tylko wyglądać ale także działać. Pierwszy z nich zawiera klasę potomną abstrakcyjnej klasy QAbstractTableModel. Nie omawialiśmy na kursie klas abstrakcyjnych, w skrócie: musimy napisać określone metody, aby klasa działała. Drugi będzie głównym plikiem aplikacji, w którym znajdziemy główny kod aplikacji.

Zawartość pliku tabela_model.py:

from PyQt5.QtCore import Qt, QAbstractTableModel

class TabelaModel(QAbstractTableModel):
    """Klasa potomna abstrakcyjnej klasy QAbstractTableModel
    należy zaimplementować poniższe metody.
    """
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, parent=None):
        """Zwraca liczbę rzędów"""
        return self._data.shape[0]

    def columnCount(self, parent=None):
        """Zwraca liczbę kolumn"""
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        """Zwraca dane z tabeli określone przez index"""
        if index.isValid():
            if role == Qt.DisplayRole:
                return str(self._data.iloc[index.row(), index.column()])
        return None

    def headerData(self, col, orientation, role):
        """Ustawia nazwy kolumn w tabeli"""
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._data.columns[col]
        return None

Zawartość pliku wizualizator.py:

import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog 
from PyQt5.uic import loadUi
from PyQt5.QtGui import QPixmap
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import tabela_model as tab


class Wizualizator(QMainWindow):
    """Główna klasa aplkacji"""
    def __init__(self):
        super().__init__()
        # Import interfejsu użytkownika bezpośrednio
        # z pliku wizualizator.ui
        loadUi("wizualizator.ui", self)
        self.show()
        # Powiązanie sygnałów (akcji wywoływanych na widżetach) 
        # i slotów (funkcji)
        self.actionOtworz.triggered.connect(self.otworz_plik)
        self.actionZamknij.triggered.connect(self.zakoncz)
        self.actionZapisz.triggered.connect(self.zapisz_wykres)
        self.generujButton.clicked.connect(self.generuj_wykres)
        self.tabela.clicked.connect(self.pobierz_wspolrzedne)
        # Status załadowania danych do tabeli potrzebne 
        # do sprawdzenia czy można generować wykres
        self.dane_zaladowane = False

        
    def pobierz_wspolrzedne(self, item):
        """Pobiera ideks wybranej kolumny w tabeli.
        Wykres będzie generowany dla danych w tej kolumnie
        """
        if item.column() < 2:
            self.wybrana_kolumna = 1
        else:
            self.wybrana_kolumna = item.column()

    def otworz_plik(self):      
        """Otwiera plik z użyciem okienka dialogowego"""
        # Otwieranie okna dialogiwego
        plik, _ = QFileDialog.getOpenFileName(self,
                "Otwórz plik", 
                "",
                "Wszystkie pliki (*);;CSV files (*.csv)")       
        # Jeżeli wybrano plik do otwarcia
        if plik:
            # Odczyt danych z pliku csv
            self.dane = pd.read_csv(plik)
            self.dane_zaladowane = True
            # Tworzenie obiektu tabeli 
            dane_tab = tab.TabelaModel(self.dane)
            # Załadowanie tabeli do widżetu
            self.tabela.setModel(dane_tab)
            # Uaktywnienie przycisku generowania wykresu
            self.generujButton.setEnabled(True)
            # W widżecie otwartyPlik zostanie wyświetlona 
            # ścieżka i nazwa otwartego poliku z danymi
            self.otwartyPlik.setText(plik)
            # Zmiana koloru tła widżetu
            self.otwartyPlik.setStyleSheet("background-color: rgb(240, 240, 240)")
            # Domyślnie do wykresu używane sa dane z 2 kolumny
            # (index = 1) 
            self.wybrana_kolumna = 1

    def zapisz_wykres(self):
        """Zapisuje wykres w wybranym formacie"""
        # Okno dialogowe wyboru pliku do zapisania
        plik, _ = QFileDialog.getSaveFileName(
                                        self,
                                        "Zapisz wykres jako",
                                        ".png",
                                        "Wszystkie pliki (*);;"+
                                        "Pliki bitmapowe (*.png *.jpg);;"+
                                        "Pliki wektorowe (*.svg *.pdf)",
                                        )
        # Jeśli została wybrana nazwa pliku, to zapis wykresu
        if plik:
            self.wykres_sb.get_figure().savefig(plik)

    def generuj_wykres(self):
        """Generuje wykres"""
        if self.dane_zaladowane:
            # Dane dla osi x - pierwsza kolumna danych
            dane_x = self.dane.iloc[:,0]
            # Dane dla osi y - wybrana kolumna (przez kliknięcie komórki)
            # Domyślnie jest to 2 kolumna
            dane_y = self.dane.iloc[:,self.wybrana_kolumna]
            # Nazwa pliku używanego przez program do zapisu
            # wykresu, który będzie wyświetlany
            plik = 'wykres.png'
            # Potrzebne aby wykresy się "resetowały" przy zmianie danych
            plt.figure()
            # Tworzenie wykresu z użyciem seaborn
            self.wykres_sb = sns.barplot(x = dane_x,
                                    y = dane_y,
                                    palette = ['brown', 'red', 'darkorange'])
            # Zapis wykresu do pliku, który będzie wyświetlany
            self.wykres_sb.get_figure().savefig(plik)
            # Tworzenie obiektu z obrazem wykresu
            wykres_png = QPixmap(plik)
            # Umieszczenie obrazu wykresu w widżecie wykres
            self.wykres.setPixmap(wykres_png) 
            # Zmiana rozmiaru widżetu wykres do rozmiaru obrazka         
            self.wykres.resize(wykres_png.width(),wykres_png.height())
            # Zmiana rozmiarów okna aplikacji
            self.resize(self.tabela.width() +
                        wykres_png.width() +
                        25,wykres_png.height()+100)
            # Uaktywnienie pozycji w menu umozliwiającej zapis wykresu
            self.actionZapisz.setEnabled(True)
        else:
            print("Brak danych")

    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 = Wizualizator()  
    # Wyświetlenie okna
    okno.show()
    # Przy zamknięciu aplikacji, zwalnia się pamięć komputera
    sys.exit(aplikacja.exec_())

Uruchom program:

python wizualizator.py

Wczytaj plik z danymi. Powinien to być plik w formacie csv (dane poprzedzielane przecinkami), w pierwszej kolumnie powinny się znajdować nazwy słupków wykresu, w innych dane liczbowe. Możesz użyć np. pliku dane.csv lub dane2.csv.

Po wczytaniu danych, kliknij na przycisk Generuj wykres, powinien się wygenerować wykres słupkowy dla pierwszej kolumny danych (czyli drugiej kolumny tabeli). Kliknij w dowolną komórkę innej kolumny, ponownie wygeneruj wykres - teraz będzie to wykres dla danych we wskazanej kolumnie. Zapisz wykres używając odpowiedniej opcji w menu.

Wizualizator

Zadanie

Napisz program, który pokazuje wyniki symulacji wpływu mutacji na frekwencje alleli. Ogólne zasady symulacji opisałem w zadaniu do lekcji Biblioteki. Tym razem jednak, program powinien mieć GUI, w którym użytkownik wprowadza parametry, pojawiają się wyniki ,,znakowe’’ dla każdego pokolenia co sekundę i w którym wyświetla się końcowy wykres.

Last updated on 5 May 2021
Published on 5 May 2021