Qt5 動態載入 *.ui 文件,及使用其圖元對象(基於pyqt5描述)


參考:《PyQt5:uic 官方教程

工具 pyuic5 的使用

如果沒有安裝,則可以通過以下指令安裝 pyuic5:

sudo apt-get install pyqt5-dev-tools

Usage: pyuic5 [options] <ui-file>

Options:
  -p, --preview  show a preview of the UI instead of generating code
  -o FILE      write generated code to FILE instead of stdout
  -x, --execute   generate extra code to test and display the class
  -d, --debug    show debug output
  --from-imports  generate imports relative to '.'
  --resource-suffix=SUFFIX  append SUFFIX to the basename of resource files  [default: _rc]

 

動態載入UI文件及圖元對象

import 模塊

import PyQt5.uic

其內容如下:

  • PACKAGE CONTENTS
    • Compiler (package)
    • Loader (package)
    • driver
    • exceptions
    • icon_cache
    • objcreator
    • port_v3 (package)
    • properties
    • pyuic
    • uiparser
  • SUBMODULES
    • compiler
    • indenter

how to use it

常用方法包括:

compileUi(uifile, pyfile, execute=False, indent=4, from_imports=False, resource_suffix='_rc')

compileUiDir(dir, recurse=False, map=None, **compileUi_args)

loadUi(uifile, baseinstance=None, package='') -> widget

loadUiType(uifile, from_imports=False) -> (form class, base class)

 注意后兩個函數,功能強大——它們首先動態編譯了ui文件並存儲在內存;然后

  • 對於loadUiType(),它導出一個tuple,裝載着ui的圖元類及其基類;
  • 對於loadUi(),它導出一個ui圖元類的實例對象。

(請忽略崩潰的bug,它要求QApplication已經運行……)

在Qt5中應用MVC模式並調取 *.ui 文件(作為View)

使用uic動態載入 *.ui 的窗口對象

拓展:創建自定義Widget,並在UI Designer中載入新控件

模塊化:頁面嵌套的積木設計

復用性:使用容器窗口類封裝自定義Widget

Qt5的容器窗口(Containers Widgets)

以上控件從上到下依次是:

  • 組合框
  • 滾動區
  • 工具箱
  • 切換卡
  • 控件棧
  • 框架
  • 組件
  • MDI窗口顯示區
  • 停靠窗口
  • ActiveX...(呃,這個怎么表達)

這里僅對 QStackedWidget 加以說明:

The QStackedWidget class provides a stack of widgets where only one widget is visible at a time. QStackedWidget can be used to create a user interface similar to the one provided by QTabWidget. It is a convenience layout widget built on top of the QStackedLayout class.

Like QStackedLayout, QStackedWidget can be constructed and populated with a number of child widgets ("pages"):

QWidget *firstPageWidget = new QWidget;
QWidget *secondPageWidget = new QWidget;

QStackedWidget *stackedWidget = new QStackedWidget;
stackedWidget->addWidget(firstPageWidget);
stackedWidget->addWidget(secondPageWidget);

QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(stackedWidget);
setLayout(layout);

// 連接槽函數
connect(pageComboBox, SIGNAL(activated(int)),
        stackedWidget, SLOT(setCurrentIndex(int)));

When populating a stacked widget, the widgets are added to an internal list. The indexOf() function returns the index of a widget in that list. The widgets can either be added to the end of the list using the addWidget() function, or inserted at a given index using the insertWidget() function. The removeWidget() function removes a widget from the stacked widget. The number of widgets contained in the stacked widget can be obtained using the count() function.

The widget() function returns the widget at a given index position. The index of the widget that is shown on screen is given by currentIndex() and can be changed using setCurrentIndex(). In a similar manner, the currently shown widget can be retrieved using the currentWidget() function, and altered using the setCurrentWidget() function.

Whenever the current widget in the stacked widget changes or a widget is removed from the stacked widget, the currentChanged() and widgetRemoved() signals are emitted respectively.

示例

效果預覽

(讀者們,忽略這個丑陋的界面吧,關注技術實現即可……)

Qt Designer 設計頁面

  

這是主窗口,就一個自上而下的三層結構……在這里這三個層次的比例如何失調都沒關系——它會根據實際的填充而自動調整的。

需要注意的是:

  • 主顯示區(中間部分)是通過QWidget代表的,它將在運行時被一個子頁面的 “自定義組合控件” (管理對象)所替代。
  • 下側的frame是一個QFrame容器,我們將通過代碼在運行時動態向這個容器里填充Button圖元。

  

這就是Page頁面,在View設計中,只管利用Designer工具把圖形繪制的盡可能詳盡(越接近需求越好,這樣View的內容盡量多的通過Designer而不是代碼實現。要知道,我們的目標是MVC,代碼盡量少的參與View的設計與顯示控制,除非顯示樣式與交互相關)。這里用到最多的操作是:

  • 拖控件
  • 添加布局
  • 編輯樣式表

  

測試頁面布局(顯示效果)

我們可以使用 pyuic5 預覽設計效果,並調整頁面尺寸來觀察Layout的實際效果。

$ pyuic5.exe -p ui/editorpage.ui

組合控件(View + Controller)的接口設計

我們希望設計一個全新的組合控件,它包含了從ui文件繼承來的頁面圖元,並增加了該控件的自定義動作(通過信號槽實現)。對於ui文件,我們需要將其載入接口類;對於信號槽,我們需要實現槽函數,並connnect到響應的signal上面:

from PyQt5.QtWidgets import QWidget, QFrame
from PyQt5.uic import loadUi

UI_Mapping = {}

class IVacWidget(QFrame):  # component of view and controller
    def __init__(self, parent=None):
        super().__init__(parent)
        self._load_ui_file(self)  # to use cls.__name__

    @classmethod
    def _load_ui_file(cls, parent):
        # print("-->>> {} ".format(cls.__name__))  # 驗證:cls.__name__呈現多態(子類類名)
        try:
            loadUi(UI_Mapping[cls.__name__], parent)
        except KeyError:
            print(UI_Mapping)
            logging.error("Unable to find the Page[{}]".format(cls.__name__))


    def _active(self):
        """ 連接信號槽,激活widget模塊的功能 """
        pass

    def _import(self):
        """ 載入數據 """
        pass

這里將 ui 文件的路徑通過UI_Mapping映射確定,而該映射則在運行時讀取配置文件載入數據。它的內容可以是這樣的(json格式):

{
    "MainWndVac"        : "ui/mainwnd.ui",
    "BasePageVac"       : "ui/basepage.ui",
    "EditorPageVac"     : "ui/editorpage.ui"
}

那么解析它也很容易了:

# 以下內容用於動態改寫 src.vacwx.UI_Mapping 值
import json
import src.vacwx

with open("uimap.json") as fp:
    src.vacwx.UI_Mapping = json.load(fp)

組合控件的實現(編寫UI控制器)

如果我們僅僅需要把當前的頁面載入我們的程序中, 那很簡單:

from PyQt5.QtWidgets import QPushButton
from src.vacwx import IVacWidget

class BasePageVac(IVacWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

    def _active(self):
        self.test_btn.pressed.connect(self.test_slot)

    def test_slot(self):
        # anything you want to do...
        pass


class EditorPageVac(IVacWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

如上,Editor僅僅是顯示了頁面,於是就有了我們開頭看到的那個丑陋的小頁面。

當然,你也可以通過實現 _active() 和自定義槽函數,綁定標准控件的各種行為……

由於MainWnd需要依賴子頁面的實現,我們將它作為獨立文件設計,並添加頁面下方的動態按鈕,以及實現頁面切換的效果:

import logging
from PyQt5.QtWidgets import QPushButton
from src.vacwx import IVacWidget, UI_Mapping
from wx.pagevac import *

logging.basicConfig(level=logging.INFO,  # filename="test.log",
                    format="[%(asctime)s] %(levelname)s --> %(message)s")
LOG = logging.getLogger(__file__)


class MainWndVac(IVacWidget):
    page_mapping = [
        {"首頁": BasePageVac},
        {"PLC": BasePageVac},
        {"編輯模式": EditorPageVac},
    ]

    def __init__(self):
        super().__init__()

        self.create_pages()
        self._active()
        self.show()

    def create_pages(self):
        self.page_list = []
        self.page_btn_list = []
        for dict_page in self.page_mapping:
            page_name = list(dict_page)[0]
            page_class = dict_page[page_name]

            page = page_class()
            # LOG.info("(1) page_id->{}".format(id(page)))  # check Page ID first.

            page_btn = QPushButton(page_name)
            # page_btn.clicked.connect(lambda: self.switch_page(page))  # ?? while the index++, the connect-map is changed.
            # self.page_btn_list[index].clicked.connect(lambda: self.switch_page(self.page_list[index]))  # failed again...
            self.page_btn_layout.addWidget(page_btn)

            self.page_list.append(page)
            self.page_btn_list.append(page_btn)

        # 初始化首頁
        self.switch_page(self.page_list[0])
        # for page in self.page_list:  # check Page ID second.
        #     LOG.info("(2) page_id->{}".format(id(page)))

    def switch_page(self, switch_to: IVacWidget):
        """ switch_to is an IVacWidget """
        # LOG.info("VacMainCtrller::switch_page({}) is called...Page[{}] is activated.".format(switch_to, id(switch_to)))
        for page in self.page_list:
            if not page.isHidden():
                if page != switch_to:
                    self.page_layout.removeWidget(page)
                    page.hide()
                else:  return
        self.page_layout.addWidget(switch_to)
        switch_to.show()

    def _active(self):
        self.page_btn_list[0].clicked.connect(lambda: self.switch_page(self.page_list[0]))
        self.page_btn_list[1].clicked.connect(lambda: self.switch_page(self.page_list[1]))
        self.page_btn_list[2].clicked.connect(lambda: self.switch_page(self.page_list[2]))

最后是整個程序的入口:

if __name__ == '__main__':
    try:
        app = QApplication(sys.argv)
        mainwin = MainWndVac()
        sys.exit(app.exec_())
    except Exception as e:
        LOG.error(e)
        traceback.print_exc()
        sys.exit(-1)

OK,至此功能完成。

總結

這個過程並不復雜,封裝也很簡單,只是如果實現容器對 ui 的載入等操作有着 pyuic 自定義的組合邏輯。既然趟了路,就把經驗羅列出來,其他人也可以省下點時間。

另外,這個模式的應用場景和價值卻很大——

  1. 首先,查詢GUI的API是一件繁瑣的事情,而Qt Designer已經把這個工作做到了“盡可能完善”;
  2. 其次,當你設計一個組合控件,其中不再涉及View對象(這里只適用於靜態圖元對象)的創建、管理時,你的代碼將更具條理——都是在處理控制過程;
  3. 對於動態圖元的創建,也適用於上一條:它也屬於控制過程的一部分——你總得根據環境的特殊性或變化觸發動態對象的創建事件;
  4. 最后,你完全解耦了Data與View層——這二者的耦合是造成代碼混亂的直接原因。

本篇博文的內容還沒有經過足夠的驗證,歡迎大家指正。我也將持續更新這個流程,以待完善。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM