[PyQt] PySide2 教程 #10: 專案!學以致用!

Qt的物件相當的多,但實際上常用到的也就是那些,前面的章節我們已經將基礎又好用的物件給介紹完畢了,是時候用我們會的東西實作一個作品了! Python雖然是類Script的語言,不如JAVA般嚴謹,但開發上也是需要遵守一些規則並且結構化的設計。本篇主要會著重於PySide2 Project的開發,以及一些撰寫概念!

What I gonna do?

本篇會實做一個相對完整的PySide2 Project,主要還是以前端顯示為主,畢竟…我也沒有教後端的東西馬XDD。這個專案目的是提供第一次接觸、或是接觸一陣子但對於介面沒有什麼想法的開發者一個簡單又不失專業的Framework, 讓開發者可以自由新增、修改喜歡的樣式以及介面。

Project Tree

首先我們要簡單的規劃一下專案的分層結構,Qt專案通常會分成幾樣東西(當然這還是有個人喜好存在),包括顯示畫面的ui 檔、多媒體圖片、以及最重要的原始碼。除此之外,我也會把前後端的工作盡量區分開來,原始碼的部分也會做相應的分類及整理。

basic_ui
├── util
├── main_window.py
├── main.py
└── user_interface
├── form
│ └── mainwindow.ui
└── media

將 form 以及 media 資料夾歸在user interface的資料夾中,負責其端的UI顯示,form主要作為放置ui 檔的資料夾,而media則用來放影音、照片、icon等等多媒體元件。 源碼及其他module則直接放在根目錄之下。

Optional Panel

這是一個自定義的UI物件,目的在於建立一個類似ToolBox的功能,但比起ToolBox相對簡單且美觀。

option panel demo

主要利用StyleSheet來做出hover、highlight、color change等等效果,如果想要客製化,只需要修改顏色維碼就可以了。
這個範例使用了 Chapter #8 中所教的QListWidget來實作我們自己的UI物件並且套用到主畫面中,這邊可以練習全源碼撰寫的widget物件,如何加入已存在的UI檔案之中做顯示

from PySide2.QtCore import QObject, Qt, QSize, QFile
from PySide2.QtGui import QFont, QIcon
from PySide2.QtWidgets import QListView, QListWidget, QListWidgetItem

class OptionPanel(QListWidget):
    STYLE = 'QListWidget { border : none; ridge #404244;' \
            'font : 10pt bold \"Source Code Pro\";' \
            'background-color: #3c413f; color: #d3d7cf; outline: none;}' \
            'QListWidget::Item {border-bottom: 1px solid #999999;}' \
            'QListWidget::item:hover { background-color: ' \
            'qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1,' \
            ' stop: 1 #4d4d4d, stop: 0 transparent); color: #d3d7cf;}' \
            'QListView::item:selected { background-color:' \
            'qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1,' \
            ' stop: 1 transparent, stop: 0 #304030); color: #55dd66;}'

    def __init__(self, parent=None):
        super(OptionPanel, self).__init__(parent)
        self._parent = parent
        self._options_buttons = OrderedDict()

        self.setCurrentRow(0)
        self.setMaximumWidth(80)
        self.setMinimumSize(QSize(80, 400))
        self.setViewMode(QListView.IconMode)
        self.setMovement(QListView.Static)
        self.setIconSize(QSize(48, 48))
        self.setSpacing(3)
        self.setItemAlignment(Qt.AlignCenter)
        self.setEnabled(True)
        self.setStyleSheet(self.STYLE)

    def option_buttons(self):
        """List of option buttons"""
        return self._options_buttons

    def add_button(self, name, icon):
        self._options_buttons[name] = self.create_item(name, icon)

    def create_item(self, name, icon):
        """Create a standard list widget item, for option panel.
        Args:
          name: Name of option button
          icon: Icon name of option button
        Returns:
          item: created option button
        """
        item = QListWidgetItem(self)
        item.setText(name)
        item.setIcon(QIcon(icon))
        item.setStatusTip(name)
        item.setSizeHint(QSize(75, 70))
        item.setTextAlignment(Qt.AlignCenter)
        item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        return item

MainWindow

主畫面已經利用QtDesigner設計編排好了,直接使用就可以了非常簡單。
有一點值得提的是:StyleSheet 通常是static,如果後續不需要修改我會盡量在ui檔中定義好。 同理,許多align,、字型、enable等等的參數如果是固定的,在QtDesigner中設定會方便非常多,也省得打一堆程式碼。

ui_setup( )

對於介面,初始化是最重要的一環了。
這邊會對初始化做一些介紹,有一些之前沒教過的新東西唷!

  • StatusBar

Status Bar在PySide中相對麻煩,因為比起Qt中可以直接取用內建的Status Bar,python必須獨立宣告設定再將他載入回main window之中。

from PySide2.QtWidgets import QStatusBar
__copyright__ = 'Copyright © 2020 DekBan - All Right Reserved.'
status_bar = QStatusBar(self._window)
status_bar.showMessage(__copyright__)
self._window.setStatusBar(status_bar)
  • Window Setting

修改視窗的title還有Dock中顯示的Icon。 這個功能既簡單又無腦,很多教學卻獨立發一篇文章…

icon = QIcon('user_interface/media/bucketing_icon.jpeg')
self._window.setWindowIcon(icon)
self._window.setWindowTitle('PySide2 Project - Basic UI Framework')

如果是 MacOSX,則需要再main.py中對app 做設定 ( Reference )

app.setWindowIcon(QIcon('user_interface/media/bucketing_icon.jpeg'))
  • 修改已知 UI內容 、新增 OptionPanel 到 Main Window

這個作法是反向取Layout的位置並做設定,是個相對少用的作法,但有時候挺方便的。

另外我們將定義好的Option Panel物件加入main layout之中。

self._option_panel = OptionPanel(self._window)
self._option_panel.add_button('DekBan',
'./user_interface/media/dekban.png')
self._option_panel.add_button('Charlie',
'./user_interface/media/charlie.jpeg')
self._option_panel.add_button('Simon',
'./user_interface/media/Simon.jpeg')

# Add widget to main layout
main_layout = self._window.main_layout
main_layout.itemAtPosition(0, 0).setAlignment(QtCore.Qt.AlignCenter)
main_layout.itemAtPosition(0, 1).setAlignment(QtCore.Qt.AlignVCenter)
main_layout.addWidget(self._option_panel, 2, 0, 1, 1)

main_window.py source code

from collections import OrderedDict

from PySide2 import QtCore
from PySide2.QtCore import QObject, Qt, QSize, QFile
from PySide2.QtGui import QFont, QIcon
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QStatusBar

__copyright__ = 'Copyright © 2020 DekBan - All Right Reserved.'


class MainWindow(QObject):
    def __init__(self):
        super(MainWindow, self).__init__()
        self._window = None
        self._option_panel = None

        self.ui_setup()

    @property
    def window(self):
        """MainWindow widget."""
        return self._window

    def ui_setup(self):
        """Initialize user interface of main window."""
        loader = QUiLoader()
        file = QFile('./user_interface/form/mainwindow.ui')
        file.open(QFile.ReadOnly)
        self._window = loader.load(file)
        file.close()

        status_bar = QStatusBar(self._window)
        status_bar.showMessage(__copyright__)
        self._window.setStatusBar(status_bar)
        self._window.setWindowIcon(QIcon('./user_interface/media/bucketing_icon.jpeg'))
        self._window.setWindowTitle('PySide2 Project - Basic UI Framework')

        self._option_panel = OptionPanel(self._window)
        self._option_panel.add_button('DekBan', './user_interface/media/dekban.png')
        self._option_panel.add_button('Charlie', './user_interface/media/charlie.jpeg')
        self._option_panel.add_button('Simon', './user_interface/media/Simon.jpeg')

        # Add widget to main layout
        main_layout = self._window.main_layout
        main_layout.itemAtPosition(0, 0).setAlignment(QtCore.Qt.AlignCenter)
        main_layout.itemAtPosition(0, 1).setAlignment(QtCore.Qt.AlignVCenter)
        main_layout.addWidget(self._option_panel, 2, 0, 1, 1)
FrontEnd Demo

Create Ur Own Page!

基礎的框架已經完成了,接下來就是各位開發者們發揮想像力的時候了!

中央的空白部分實際上是Chapter #9中所學到的容器物件 — QStackWidget,而我們可以透過左方的OptionPanel來切換不同的頁面。 至於新增頁面的方式,我們只需要建立一個繼承於QWidget的自定義物件,並將其加入QStackWidget即可! (加入stack 詳細請參考 Chapter #9)

這邊要說的是,利用ui 檔來製作頁面,最後加入QStackWidget之中。

Create QWidget ui file

首先我們要建立新的ui檔,但不同的是要繼承QWidget而非QMainWindow
接下來建立相對應的python檔,並建立一個Class,同樣使用QUILoader的方法來連結這個UI檔。

這邊我將使用 Chapter #8 的 ItemWidget的UI檔,並修改為繼承QWidget的版本,其他頁面則是用 Chapter #9 的 TextPage的純源碼UI。

新增page到QStackWidget中

# Add page widget to stack
self._pages['item'] = ItemWidget()
self._pages['text1'] = TextPage(text=PAUSE_TEXT)
self._pages['text2'] = TextPage(text=STOP_TEXT)

for index, name in enumerate(self._pages):
print('pages {} : {} page'.format(index, name))
self._window.widget_stack.addWidget(self._pages[name].widget)

self._window.widget_stack.setCurrentIndex(0)

最後串接Signal / Slot就完成啦!

# main_window.pyself._option_panel.currentItemChanged.connect(self.set_page)

@QtCore.Slot(int, int)
def set_page(self, current, previous):
"""Slot, switch shown page."""
widget_num = self._option_panel.currentIndex().row()
self._window.widget_stack.setCurrentIndex(widget_num)

Result

PySide2 Project Demo

Source Code

完整代碼請看 :basic_ui

結論

本篇主要著重在專案的建置、版面設計、以及一些小技巧,主要是讓大家對之前學的東西更有概念,同時提供一個稍微完整的Framework作為Project的起始Template,以加速開發速度,希望會有幫助囉!

发表评论

您的电子邮箱地址不会被公开。