大家好,我是痞子衡,是正經(jīng)搞技術(shù)的痞子。今天痞子衡給大家介紹的是 wxPython GUI 構(gòu)建工具 wxFormBuilder。
一、手工代碼布局 GUI 界面的煩惱
如果你曾經(jīng)設(shè)計過上位機軟件 GUI 界面,初始階段一定是純手工代碼布局 GUI 界面上的各個控件,相信你肯定遇到過如下煩惱:
控件類型較難找:UI 界面里有很多控件類型,純手工寫代碼需要翻看文檔一個個去查找這些控件的名字與用法。
尺寸位置難調(diào)整:如果界面上已經(jīng)布了多個控件,想要整體去調(diào)整這些控件的尺寸與位置是一件頭疼的事。
效果查看不實時:每新添加一個控件都需要運行才能查看整體效果,如果代碼添加不完善,可能無法運行看不到效果。
二、wxFormBuilder 工具背景
在講本文主角 wxFormBuilder 之前有必要提一下這個軟件的來歷,首先要追述到大名鼎鼎的跨平臺 GUI 庫 wxWidgets,這個庫主要是用 C++語言實現(xiàn)的;鑒于 wxWidgets 的流行,Robin Dunn 用 Python 語言對 wxWidgets 做了一層封裝,封裝后便成了 Python 版 GUI 庫 wxPython;下面是這兩個 GUI 庫的官方主頁:
wxWidgets 項目官方主頁: https://www.wxwidgets.org/
wxPython 項目官方主頁: https://www.wxpython.org/
wxWidgets 的各種 UI 控件功能均是通過 class 來實現(xiàn)的,這個鏈接 http://docs.wxwidgets.org/3.0/page_class_cat.html 列出了 wxWidgets 里的所有 class,wxPython 并沒有實現(xiàn) wxWidgets 里全部 class,但基本實現(xiàn)了大部分常用 class,這個鏈接 https://docs.wxpython.org/wx.1moduleindex.html 列出了 wxPython 里所有的 class。
知道了 wxPython 的 class 便可以開始設(shè)計 GUI 界面,但手工寫代碼設(shè)計界面太繁瑣,因此 wxFormBuilder 應(yīng)運而生,這是一款能夠可視化設(shè)計界面的工具(并不是唯一工具,還有 wxGlade、Boa Constructor 等),通過該工具設(shè)計 GUI 界面后可自動生成 wxPython 代碼,下面是 wxFormBuilder 的官方主頁:
wxFormBuilder 項目 Github: https://github.com/wxFormBuilder/wxFormBuilder
三、wxFormBuilder 快速上手
使用 wxFormBuilder 去設(shè)計 GUI 界面可以不用掌握 wxPython 里的各個控件 class 的具體用法,你只需要在 wxFormBuilder 軟件里添加這些控件即可,下面痞子衡將簡介 wxFormBuilder 的用法:
3.1 軟件界面
安裝好 wxFormBuilder 軟件之后打開這個軟件,可見到如下界面,界面主要分為四大區(qū):項目區(qū)、控件區(qū)、編輯區(qū)、屬性區(qū)。軟件使用起來非常簡單,就是在【控件區(qū)】里點擊添加需要的控件,這些控件的效果會在【編輯區(qū)】里實時顯示,并在【屬性區(qū)】這些控件的屬性,【項目區(qū)】用于顯示控件間的層級關(guān)系。
3.2 基礎(chǔ)布局
讓我們開始創(chuàng)建一個 GUI 的基礎(chǔ)框架,基礎(chǔ)框架包括:Frame(外圍輪廓)、Sizer(內(nèi)部控件區(qū))、menubar(頂部菜單欄)、statusBar(底部狀態(tài)欄)。
第一步是添加一個 Frame,這是 GUI 的輪廓基礎(chǔ),其 size(default 為 500;300)決定了 GUI 整體界面的大小。
第二步是在 Frame 下添加一個 Sizer,后續(xù)所有控件均是放在 Sizer 里的。關(guān)于 Sizer 部分需要特別說明一下,wxPython 提供的 Sizer 類型有如下七種:wxBoxSizer、wxWrapSizer、wxStaticBoxSizer、wxGridSizer、wxFlexGridSizer、wxGridBagSizer、wxStdDialogButtonSizer,Sizer 的樣式?jīng)Q定了后續(xù)控件的整體相對位置關(guān)系,選定了 Sizer 即選定了 GUI 界面樣式。關(guān)于這七種 Sizer 的具體樣式請見 https://docs.wxpython.org/sizers_overview.html#sizers-overview。如果你覺得單個 Sizer 里的控件布局太單調(diào),你可以嵌套使用 Sizer,這是實現(xiàn) GUI 界面控件布局多樣化的關(guān)鍵。
第三步是在 Frame 頂部添加一個 menubar:
第四步是在 Frame 底部添加一個 statusBar:
3.3 多種控件
基礎(chǔ)布局搞定之后,接下來便是在 Sizer 里添加控件,wxPython 支持的控件非常豐富,其中比較常用的是如下幾個:button(按鈕)、staticText(靜態(tài)顯示文本框)、textCtrl(輸入輸出文本框)、Choice(復(fù)選框)、checkBox(選中框)、slider(滑動條)。前面痞子衡選擇的 Sizer 是 wxBoxSizer,即自上而下布局,因此這些控件在 Sizer 是自上而下排列的,各個控件的位置后續(xù)在屬性里還可以微調(diào),但改變不了自上而下的格局。
3.4 控件屬性
添加了所有控件之后,下一步便是分別設(shè)置控件的屬性,進一步調(diào)整控件。痞子衡以 Button 屬性為例,痞子衡勾選了如下 4 項比較重要的屬性設(shè)置,分別是 name(button 在后續(xù) python 代碼的對象名,一般需要按其功能修改,修改后使得代碼閱讀 / 修改起來更直觀)、label(button 在 GUI 里顯示的標簽名,此處是 MyButton,也需要按其功能修改,方便用戶使用軟件)、size(設(shè)置 button 的尺寸,這個尺寸最大不應(yīng)超過 Sizer 尺寸)、flag(調(diào)整對齊方式從而調(diào)整 Button 在 Sizer 里的位置)。另外有一個屬性不得不說,即控件位置 pos,在 wxFormBuilder 里設(shè)置這個屬性并不生效,痞子衡猜想可能跟 Sizer 樣式有關(guān),因為 Sizer 決定了控件間相對位置關(guān)系,因此控件的 pos 不能隨意設(shè)置。
3.5 觸發(fā)事件
有些控件是需要有響應(yīng)的,比如 Button,在 GUI 軟件實際使用中,用戶如果按下了 Button,應(yīng)該需要觸發(fā)某個任務(wù),任務(wù)需要有響應(yīng)函數(shù),這個響應(yīng)函數(shù)需要在【Events】里設(shè)置,Button 的響應(yīng)函數(shù)在 OnButtonClick 里設(shè)置,痞子衡在這里指定了響應(yīng)函數(shù)名為 showMessage。在 wxFormBuilder 里我們只需要指定控件響應(yīng)函數(shù)名即可,響應(yīng)函數(shù)的具體功能實現(xiàn),不屬于 wxFormBuilder 設(shè)計范疇。
3.6 生成代碼
當 GUI 界面布局全部完成之后,需選擇 File->Generate Code 或 F8 生成 python 代碼,需要復(fù)制所有的 python 代碼并保存在單獨的文件里,痞子衡保存在了 my_win.py 文件里。
可以簡單看一下這個 my_win.py 里的內(nèi)容,代碼里首先 import 了 wx 庫(即 wxPython 庫),并定義了名為 MyFrame1 的 class,這個 class 主要包含兩個函數(shù) __init__()和 showMessage():__init__()里初始化了各個控件成員 self.m_xx,這與我們在 wxFormBuilder 里添加控件是對應(yīng)的;showMessage()是 Button 控件的響應(yīng)函數(shù),但這個響應(yīng)函數(shù)并沒有任何實質(zhì)代碼,當然我們可以在這個函數(shù)里面實現(xiàn) Button 響應(yīng)功能,但一般不建議直接在 wxFormBuilder 生成的代碼里添加代碼,因為你可能隨時調(diào)整 GUI 頁面布局,那么 main_win.py 里的代碼會重新生成,這樣會覆蓋我們自己添加的代碼,導(dǎo)致維護起來比較麻煩。
# -*- coding: utf-8 -*-
###########################################################################
## Python code generated with wxFormBuilder (version Jul 11 2018)
## http://www.wxformbuilder.org/
##
## PLEASE DO *NOT* EDIT THIS FILE!
###########################################################################
import wx
import wx.xrc
###########################################################################
## Class MyFrame1
###########################################################################
class MyFrame1 ( wx.Frame ):
? ??
? ? def __init__( self, parent ):
? ? ? ? wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )
? ? ? ??
? ? ? ? self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
? ? ? ??
? ? ? ? bSizer1 = wx.BoxSizer( wx.VERTICAL )
? ? ? ??
? ? ? ? self.m_button1 = wx.Button( self, wx.ID_ANY, u"MyButton", wx.Point( -1,-1 ), wx.DefaultSize, 0 )
? ? ? ? bSizer1.Add( self.m_button1, 0, wx.ALL, 5 )
? ? ? ??
? ? ? ? self.m_staticText1 = wx.StaticText( self, wx.ID_ANY, u"MyLabel", wx.DefaultPosition, wx.DefaultSize, 0 )
? ? ? ? self.m_staticText1.Wrap( -1 )
? ? ? ??
? ? ? ? bSizer1.Add( self.m_staticText1, 0, wx.ALL, 5 )
? ? ? ??
? ? ? ? self.m_textCtrl1 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.Point( -1,-1 ), wx.DefaultSize, 0 )
? ? ? ? bSizer1.Add( self.m_textCtrl1, 0, wx.ALL, 5 )
? ? ? ??
? ? ? ? m_choice1Choices = []
? ? ? ? self.m_choice1 = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_choice1Choices, 0 )
? ? ? ? self.m_choice1.SetSelection( 0 )
? ? ? ? bSizer1.Add( self.m_choice1, 0, wx.ALL, 5 )
? ? ? ??
? ? ? ? self.m_checkBox1 = wx.CheckBox( self, wx.ID_ANY, u"Check Me!", wx.DefaultPosition, wx.DefaultSize, 0 )
? ? ? ? bSizer1.Add( self.m_checkBox1, 0, wx.ALL, 5 )
? ? ? ??
? ? ? ? self.m_slider1 = wx.Slider( self, wx.ID_ANY, 50, 0, 100, wx.DefaultPosition, wx.DefaultSize, wx.SL_HORIZONTAL )
? ? ? ? bSizer1.Add( self.m_slider1, 0, wx.ALL, 5 )
? ? ? ??
? ? ? ??
? ? ? ? self.SetSizer( bSizer1 )
? ? ? ? self.Layout()
? ? ? ? self.m_menubar1 = wx.MenuBar( 0 )
? ? ? ? self.m_menu1 = wx.Menu()
? ? ? ? self.m_menuItem1 = wx.MenuItem( self.m_menu1, wx.ID_ANY, u"MyMenuItem", wx.EmptyString, wx.ITEM_NORMAL )
? ? ? ? self.m_menu1.Append( self.m_menuItem1 )
? ? ? ??
? ? ? ? self.m_menubar1.Append( self.m_menu1, u"MyMenu" )
? ? ? ??
? ? ? ? self.SetMenuBar( self.m_menubar1 )
? ? ? ??
? ? ? ? self.m_statusBar1 = self.CreateStatusBar( 1, wx.STB_SIZEGRIP, wx.ID_ANY )
? ? ? ??
? ? ? ? self.Centre( wx.BOTH )
? ? ? ??
? ? ? ? # Connect Events
? ? ? ? self.m_button1.Bind( wx.EVT_BUTTON, self.showMessage )
? ??
? ? def __del__( self ):
? ? ? ? pass
? ??
? ??
? ? # Virtual event handlers, overide them in your derived class
? ? def showMessage( self, event ):
? ? ? ? event.Skip()
? ??
四、使用 wxFormBuilder 生成的代碼
前面已經(jīng)使用 wxFormBuilder 生成 GUI 界面類 MyFrame1 并保存在 my_win.py 文件中,此時需要創(chuàng)建一個主函數(shù)文件去調(diào)用 MyFrame1,下面是痞子衡創(chuàng)建的 main_win.py 中的代碼:
import wx
# 導(dǎo)入 my_win.py 中內(nèi)容
import my_win
# 創(chuàng)建 mainWin 類并傳入 my_win.MyFrame1
class mainWin(my_win.MyFrame1):
? ?# 實現(xiàn) Button 控件的響應(yīng)函數(shù) showMessage
? ?def showMessage(self, event):
? ? ? ?self.m_textCtrl1.Clear()
? ? ? ?self.m_textCtrl1.SetValue('hello world')
if __name__ == '__main__':
? ? # 下面是使用 wxPython 的固定用法
? ? app = wx.App()
? ? main_win = mainWin(None)
? ? main_win.Show()
? ? app.MainLoop()
最后讓我們測試一下這個 GUI 軟件,在命令行下運行 main_win.py
PS D:my_git_repo> python .main_win.py
至此,wxPython GUI 構(gòu)建工具 wxFormBuilder 痞子衡便介紹完畢了,掌聲在哪里~~~