參考:《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 自定義的組合邏輯。既然趟了路,就把經驗羅列出來,其他人也可以省下點時間。
另外,這個模式的應用場景和價值卻很大——
- 首先,查詢GUI的API是一件繁瑣的事情,而Qt Designer已經把這個工作做到了“盡可能完善”;
- 其次,當你設計一個組合控件,其中不再涉及View對象(這里只適用於靜態圖元對象)的創建、管理時,你的代碼將更具條理——都是在處理控制過程;
- 對於動態圖元的創建,也適用於上一條:它也屬於控制過程的一部分——你總得根據環境的特殊性或變化觸發動態對象的創建事件;
- 最后,你完全解耦了Data與View層——這二者的耦合是造成代碼混亂的直接原因。
本篇博文的內容還沒有經過足夠的驗證,歡迎大家指正。我也將持續更新這個流程,以待完善。
