Як створити таблицю в PyQt5

Python – то вам не Delphi. Тут новачки починають свій шлях з таємничого порожнього файлу, де пишуть код, і лише через деякий час приходять до візуалізації. Сьогодні в цій статті ми якраз і зробимо деякі кроки в даному напрямку, а саме познайомимося з тим, як створити/ намалювати/ візуалізувати (тут обирайте самі) таблицю в PyQt5.

Нагадаю, PyQt5 – це набір бібліотек та модулів Python для одного з найпопулярніших фреймворків Qt5. У свою чергу Qt5 було розроблено на мові C++ для відображення: саме завдяки ньому ви можете створити віконце з кнопками, полями для вводу та виводу інформації, графіками, діаграмами, вкладками тощо. До речі, погратися з цією штукою можна в додатку Qt Designer, який показує вам код кожного віджету, що ви намалювали на екрані.

Почнемо ми базових кроків і продемонструємо, як саме має виглядати «скелет» нашого додатку (раптом хто вирішив  знайомитися з PyQt5 вперше одразу з побудови таблиці). На початку ці кроки можуть здаватися складними, але з часом поступово все стане зрозумілим. Так виглядатиме базовий код (нижче описала деталі):

from PyQt5.QtWidgets import QMainWindow, QApplication

class TableApp(QMainWindow):                #створимо клас, який успадковує QMainWindow
    def __init__(self):
        super().__init__()                  #отримуємо доступ до батьківського класу 
        self.setWindowTitle("Table App")    #прописуємо назву нашого додатку
        self.setFixedSize(1000, 1000)       #вказуємо розміри нашого вікна


def run_app():
    form = QApplication([])                 #ініціалізація додатку загалом 
    WidMain = TableApp()                    #створюємо екземпляр вже вашого класу
    WidMain.show()                          #відобразимо наші віджети
    form.exec_()                            #запуск додатку

if __name__== '__main__':                   #це пишеться, щоб функція run_app() виконувалася лише під час 
                                            #прямого запуску цього модуля (не під час його імпортування)
    run_app()

QMainWindow – клас, який представляє головне вікно програми. Його ми наслідуємо і використовуємо у нашій програмі. Саме на це вікно зазвичай накладаються різноманітні віджети (простою мовою це і є ті самі кнопки, поля, діаграми, таблиці тощо). 

QApplication  відповідає за головний цикл подій та ініціалізацію додатку загалом. Екземпляр даного класу потрібно створити лише один раз у вашому модулі. Коли користувач щось натискає, переключає або вводить у вікні додатку, ця подія поміщається в чергу і потім оброблюється  – саме для такого і потрібен головний цикл подій. Можна передати параметр sys.argv (список аргументів командного вікна)  у QApplication, але він нам зараз не знадобиться, тому ми поставили [].

Запустимо і отримаємо наступну картину:

Звичайно, поки що наше вікно порожнє, бо ми туди нічого не додали, хоч і написали чимало коду.

Тепер будемо малювати таблицю. Спочатку я показуватиму шматки, а в кінці продемонструю весь код. Для створення нашої таблиці я додам екземпляр класу QTreeWidget:

Table = QTreeWidget()

Так, це не зовсім таблиця, проте мені подобається клас QTreeWidget тим, що за допомогою нього можна візуалізувати так звані «дерева». Наведу приклад з власного проекту:

Як бачите, ця таблиця містить не лише рядки, а й підрядки, що відкриваються при натисканні на стрілочку зліва від так званого «батьківського» рядка. Але, якщо вам це не потрібно, можна скористатися класом QTableWidget.

Також вкажу розмір таблиці:

Table.setFixedSize(1000, 1000) 

Щиро раджу вказувати розміри для всіх віджетів у вашому вікні. На початковому етапі це може бути не завжди важливо: іноді віджет і так має потрібний вам розмір. Однак під час створення більших додатків може виникати ситуація, коли віджет не відображається взагалі якраз через те, що немає вказаного розміру.

Що потрібно нашій таблиці? Правильно, заголовки. Для цього створимо віджет QHeaderView, пропишемо його колонки і додамо в нашу таблицю:

Headers = QHeaderView(Qt.Orientation.Horizontal, Table)
Table.setHeader(Headers)
Table.setColumnCount(2) 
Table.setHeaderLabels(['Name', 'Price, $'])

Тобто, ми зараз з вами створимо таблицю, яка матиме 2 колонки з назвами “Name” та “Price, $”. Звичайно, для них теж треба вказати розмір:

Table.setColumnWidth(0, 500)
Table.setColumnWidth(1, 500)

Але ми їх не побачимо, якщо далі не пропишемо рядок:

self.setCentralWidget(Table)

оскільки саме він розміщує нашу таблицю у головному вікні. Тепер можна запустити написаний код:

from PyQt5.QtWidgets import QMainWindow, QApplication, QTreeWidget, QHeaderView
from PyQt5.QtCore import Qt

class TableApp(QMainWindow):            
    def __init__(self):
        super().__init__()                  
        self.setWindowTitle("Table App")    
        self.setFixedSize(1000, 1000)       

        Table = QTreeWidget()
        Table.setFixedSize(1000, 1000) 
        Headers = QHeaderView(Qt.Orientation.Horizontal, Table)
        Table.setHeader(Headers)
        Table.setColumnCount(2) 
        Table.setHeaderLabels(['Name', 'Price, $'])
        self.setCentralWidget(Table)


def run_app():
    form = QApplication([])                 
    WidMain = TableApp()                    
    WidMain.show()                          
    form.exec_()                            

if __name__== '__main__':                   
    run_app()

Зверніть увагу на перші 2 рядки: там відбулося поповнення в плані імпорту класів. Якщо їх стане занадто багато, краще просто написати from PyQt5.QtWidgets import *

Отже побачимо таку картинку:

Поки що тут немає даних, однак вже вимальовуються 2 колонки. Для того, щоб почати створювати лінії, пропишемо масив products, з якого будемо черпати інформацію, а також створимо екземпляр класу QTreeWidgetItem (це, по суті, і є рядок нашої таблиці). Потім скористаємося методом  setText для того, щоб додати текст у відповідну комірку таблиці. Наш код виглядатиме так:

products = [('electronics', 1000), ('clothing', 50), 
                    ('books', 20), ('kitchenware', 30), ('groceries', 70)]
        
for p in products:
  TableItem = QTreeWidgetItem(Table)
  TableItem.setText(0, p[0])
  TableItem.setText(1, str(p[1]))

Після запуску оновленого коду ми побачимо наступне:

По суті, наша табличка готова. Однак напевно ж вам захочеться надати їй якогось більш привабливого вигляду. Зокрема, ми можемо вирівняти розміщення заголовків таблиці, наприклад, по лівій стороні:

Headers.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft)

Крім того, можна змінити колір заголовків і навіть їхній шрифт. Для цього я люблю використовувати CSS:

Headers.setStyleSheet('''QHeaderView {border: none} 
                         QHeaderView::section {background-color: #78C1C0; 
                                               color: white; 
                                               padding-left: 13px; 
                                               padding-top: 5px; 
                                               font: bold; 
                                               font-size: 9pt}''')

Таким чином отримую наступну картинку:

Я думаю, що властивості з CSS (color, padding, font) ви можете загуглити, проте важливими є селектори, які ви вказуєте в даному коді. Що я маю на увазі?

Якщо ви не вкажете QHeaderView::section, а просто QHeaderView, то в даному випадку може виникнути наступна історія:

Тобто колір знаходиться не на заголовках, а за їхніми межами. Тому важливо чітко вказувати, до чого саме ви хочете його застосувати. Здавалося б це очевидно, проте часом PyQt5 може адекватно відпрацьовувати і без точно вказаних селекторів, тому новачки на початку їх не вказують і лише потім розуміють, наскільки це важливо.

Все! Наша таблиця готова. Наостанок продемонструю вам повний код:

from PyQt5.QtWidgets import QMainWindow, QApplication, QTreeWidget, QHeaderView, QTreeWidgetItem
from PyQt5.QtCore import Qt

class TableApp(QMainWindow):            
    def __init__(self):
        super().__init__()                  
        self.setWindowTitle("Table App")    
        self.setFixedSize(1000, 1000)       

        Table = QTreeWidget()
        Table.setFixedSize(1000, 1000) 
        Headers = QHeaderView(Qt.Orientation.Horizontal, Table)
        Headers.setStyleSheet('''QHeaderView {border: none} 
                   QHeaderView::section {background-color: #78C1C0; color: white; padding-left: 13px; padding-top: 5px; font: bold; font-size: 9pt}''')
        Headers.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft)
        Table.setHeader(Headers)
        Table.setColumnCount(2) 
        Table.setColumnWidth(0, 500)
        Table.setColumnWidth(1, 500)
        Table.setHeaderLabels(['Name', 'Price, $'])
        self.setCentralWidget(Table)

        products = [('electronics', 1000), ('clothing', 50), 
                    ('books', 20), ('kitchenware', 30), ('groceries', 70)]
        
        for p in products:
            TableItem = QTreeWidgetItem(Table)
            TableItem.setText(0, p[0])
            TableItem.setText(1, str(p[1]))


def run_app():
    form = QApplication([])                 
    WidMain = TableApp()                    
    WidMain.show()                          
    form.exec_()                            

if __name__== '__main__':                   
    run_app()

Але, якщо ми вже заговорили про «дерева», то давайте продемонструю приклад, як же зробити те саме «дерево», щоб біля кожної позиції була стрілочка, яка відкриває деталі. Для цього дещо змінемо наш масив products, перетворивши його у словник і додавши деталі:

products = {
                    ('electronics', 1300): (('laptop', 800), ('smartphone', 500)),
                    ('clothing', 60): (('t-shirt', 20), ('jeans', 40)),
                    ('books', 40): (('fiction', 15), ('non-fiction', 25)),
                    ('kitchenware', 40): (('cookware', 25), ('cutlery', 15)),
                    ('groceries', 30): (('fruits', 10), ('vegetables', 15), ('bread', 5))
                    }

Та частина коду, де ми створювали TableItem, також зміниться, проте не надто катастрофічно:

for p in products:
  TableItem = QTreeWidgetItem(Table)
  TableItem.setText(0, p[0])
  TableItem.setText(1, str(p[1]))
  for d in products[p]:
    ChildItem = QTreeWidgetItem(TableItem)
    ChildItem.setText(0, d[0])
    ChildItem.setText(1, str(d[1]))

Тобто тепер ми на кожен TableItem створюємо ChildItem і прописуємо в ньому наші деталі аналогічно тому, як ми це робили для батьківського айтему. Як це виглядає в результаті:

Ось і все! Тепер ви можете створювати таблиці у вигляді «дерев» у ваших проектах.