概述
MFC是我接觸到的第一個(gè)界面庫,當(dāng)時(shí)的操作系統(tǒng)還是Windows95。在那個(gè)IT技術(shù)日新月異的年代,就像一個(gè)從荒蠻部落闖進(jìn)文明社會(huì)的野人第一眼看見汽車那樣,我對MFC充滿了好奇和迷戀。盡管后來斷斷續(xù)續(xù)接觸了WPF、Qt等GUI庫,卻始終對MFC情有獨(dú)鐘,以至于愛屋及烏,喜歡上了wxWidgets。
wxWidgets和MFC的確太相似了,連命名習(xí)慣和架構(gòu)都高度相似。事實(shí)上,wxWidgets就是跨平臺(tái)的MFC,對各個(gè)平臺(tái)的差異做了抽象,后端還是用各平臺(tái)原生的API實(shí)現(xiàn)。這正是wxWidgets的優(yōu)點(diǎn):編譯出來的程序發(fā)行包比較小,性能也相當(dāng)優(yōu)異。
隨著MFC的日漸式微,Qt異軍突起,目前已成為最強(qiáng)大,最受歡迎的跨平臺(tái)GUI庫之一。在Python生態(tài)圈里,PyQt的用戶群也遠(yuǎn)超wxPython。喜歡Qt的人認(rèn)為這是技術(shù)競爭的結(jié)果,但我覺得這更像是開源理念和商業(yè)化思想的差異造成的。
wxWidgets像是一個(gè)孤獨(dú)的勇士,高舉開源的大旗,試圖以一己之力構(gòu)建一個(gè)相互承認(rèn)、相互尊重的理想社會(huì);而Qt則更像是一個(gè)在商業(yè)資本驅(qū)使下不斷擴(kuò)張的帝國,它不滿足于封裝不同平臺(tái)的API,而是要?jiǎng)?chuàng)造出自己的API和框架,它不僅僅是UI,而是囊括了APP開發(fā)用到的所有東西,包括網(wǎng)絡(luò)、數(shù)據(jù)庫、多媒體、藍(lán)牙、NFC、腳本引擎等。
缺少或拒絕商業(yè)化運(yùn)作的支持,wxWidgets的悲情結(jié)局早已是命中注定。如果不是因?yàn)镻ython的興盛和wxPython的復(fù)興,wxWidgets也許早已經(jīng)和MFC一樣被遺忘在了角落里。不無夸張地說,wxPython是以MFC為代表的一個(gè)時(shí)代的挽歌,更是一曲理想主義的絕唱。
1.1 組織架構(gòu)
其實(shí),wxPython談不上什么組織架構(gòu),因?yàn)樽烂娉绦蜷_發(fā)所用的類、控件、組件和常量幾乎都被放到了頂級(jí)命名空間wx下面了。這樣做看似雜亂無章,但用起來卻是非常便捷。比如,導(dǎo)入必要的模塊,PyQt通常要這樣寫:
from PyQt6.QtWidgets import QApplication, QWidget, QComboBox, QPushButton, QHBoxLayout, QVBoxLayout, QColorDialog
from PyQt6.QtGui import QIcon, QPainter, QPen, QColor, QPolygon
from PyQt6.QtCore import Qt, QPoint
PyQt巨人般的體量限制了使用星號(hào)導(dǎo)入所有的模塊,只能用什么導(dǎo)入什么。而wxPython只需要簡短的一句話:
import wx
再比如一些常量的寫法,wxPython同樣簡潔,PyQt已經(jīng)長到匪夷所思的程度了。比如左對齊和確定取消鍵,wxPython這樣寫:
wx.ALIGN_LEFT
wx.OK | wx.CANCEL
PyQt寫出來幾乎要占一整行:
Qt.AlignmentFlag.AlignLeft
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel
盡管wxPython也與時(shí)俱進(jìn)地增加了一些諸如wx.xml、wx.svg之類地外圍模塊,但除了wx這個(gè)核心模塊之外,我個(gè)人覺得只有wx.aui和wx.grid模塊算是必要的擴(kuò)展。如果想讓界面更花哨點(diǎn),那就要了解以下wx.adv、wx.ribbon這兩個(gè)模塊,純python構(gòu)建的控件庫wx.lib也絕對值得一試??傊驹谖业膽?yīng)用領(lǐng)域看,wxPython的組織架構(gòu)如下圖所示。根據(jù)使用頻率的高低,我給各個(gè)模塊標(biāo)注了紅黃綠藍(lán)四種顏色。
1.2 安裝
截至本文寫作時(shí),wxPython的最新版本是4.1.1。Windows用戶和macOS用戶可以直接使用下面的命令安裝。
pip install -U wxPython
由于Linux平臺(tái)存在發(fā)行版之間的差異,必須使用相應(yīng)的包管理器進(jìn)行下載和安裝。例如,在Ubuntu系統(tǒng)上可以嘗試下面的安裝命令。
sudo apt-get install python3-wxgtk4.0 python3-wxgtk-webview4.0 python3-wxgtk-media4.0
快速體驗(yàn)
2.1 桌面應(yīng)用程序開發(fā)的一般流程
用wxPython寫一個(gè)桌面應(yīng)用程序,通常分為6個(gè)步驟:
第1步:導(dǎo)入模塊
第2步:創(chuàng)建一個(gè)應(yīng)用程序
第3步:創(chuàng)建主窗口
第4步:在主窗口上實(shí)現(xiàn)業(yè)務(wù)邏輯
第5步:顯示窗主口
第6步:應(yīng)用程序進(jìn)入事件處理主循環(huán)
除第4步之外的其它步驟,基本都是一行代碼就可以完成,第4步的復(fù)雜程度取決于功能需求的多寡和業(yè)務(wù)邏輯的復(fù)雜度。下面這段代碼就是這個(gè)一般流程的體現(xiàn)。
# 第1步:導(dǎo)入模塊
import wx
# 第2步:創(chuàng)建一個(gè)應(yīng)用程序
app = wx.App()
# 第3步:創(chuàng)建主窗口
frame = wx.Frame(None)
# 第4步:在主窗口上實(shí)現(xiàn)業(yè)務(wù)邏輯
st = wx.StaticText(frame, -1, 'Hello World')
# 第5步:顯示窗主口
frame.Show()
# 第6步:應(yīng)用程序進(jìn)入事件處理主循環(huán)
app.MainLoop()
2.2 Hello World
實(shí)際應(yīng)用wxPython開發(fā)桌面應(yīng)用程序的的時(shí)候,上面這樣的寫法難以實(shí)現(xiàn)和管控復(fù)雜的業(yè)務(wù)邏輯,因而都是采用面向?qū)ο蟮膽?yīng)用方式。下面的代碼演示了以O(shè)OP的方式使用wxPython,并且為窗口增加了標(biāo)題和圖標(biāo),設(shè)置了窗口尺寸和背景色,同時(shí)也給靜態(tài)文本控件StaticText設(shè)置了字體字號(hào)。
import wx
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, -1,style=wx.DEFAULT_FRAME_STYLE)
self.SetTitle('最簡的的應(yīng)用程序')
self.SetIcon(wx.Icon('res/wx.ico')) # 設(shè)置圖標(biāo)
self.SetBackgroundColour((217, 228, 0)) # 設(shè)置窗口背景色
self.SetSize((300, 80)) # 設(shè)置窗口大小
self.Center() # 窗口在屏幕上居中
st = wx.StaticText(self, -1, 'Hello World', style=wx.ALIGN_CENTER) # 生成靜態(tài)文本控件,水平居中
st.SetFont(wx.Font(20, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False, 'Arial')) # 設(shè)置字體字號(hào)
if __name__ == '__main__':
app = wx.App() # 創(chuàng)建一個(gè)應(yīng)用程序
frame = MainFrame(None) # 創(chuàng)建主窗口
frame.Show() # 顯示窗主口
app.MainLoop() # 應(yīng)用程序進(jìn)入事件處理主循環(huán)
代碼中用到了一個(gè).png格式的圖像文件文件,想要運(yùn)行這段代碼的話,請先替換成本地文件。至于文件格式,SetIcon方法沒有限定,常見的包括.ico和.jpg在內(nèi)的圖像格式都支持。代碼運(yùn)行界面如下圖所示。
2.3 常用控件介紹
盡管wxPython的核心模塊和擴(kuò)展模塊提供了數(shù)以百計(jì)的各式控件和組件,但真正常用且必不可少的控件只有為數(shù)不多的幾個(gè):
wx.Frame - 窗口
wx.Panel - 面板
wx.StaticText - 靜態(tài)文本
StaticBitmap - 靜態(tài)圖片
wx.TextCtrl - 單行或多行文本輸入框
wx.Button - 按鈕
wx.RadioButton - 單選按鈕
wx.CheckBox - 復(fù)選按鈕
wx.Choice - 下拉選擇框
所有的wxPython控件都有一個(gè)不可或缺的parent參數(shù)和若干關(guān)鍵字參數(shù),通常,關(guān)鍵字參數(shù)都有缺省默認(rèn)值。
parent - 父級(jí)對象
id - 控件的唯一標(biāo)識(shí)符,缺省或-1表示自動(dòng)生成
pos - 控件左上角在其父級(jí)對象上的絕對位置
size - 控件的寬和高
name - 用戶定義的控件名
style - 控件風(fēng)格
wxPython的控件在使用風(fēng)格上保持著高度的一致性,一方面因?yàn)樗鼈儚囊粋€(gè)共同的基類派生而來,更重要的一點(diǎn),wxPython不像PyQt那樣充斥著隨處可見的重載函數(shù)。比如,PyQt的菜單欄QMenuBar增加菜單,就有addMenu(QMenu)、addMenu(str)和addMenu(QIcon, str)等三種不同的重載形式。方法重載固然帶來了很多便利,但也會(huì)增加使用難度,讓用戶無所適從。
下面的代碼演示了上述常用控件的使用方法。
import wx
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
# 調(diào)用父類的構(gòu)造函數(shù),從默認(rèn)風(fēng)格中去除改變窗口大小
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)
self.SetTitle('wxPython控件演示')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetSize((860, 450))
self.Center()
# 創(chuàng)建一個(gè)面板,用于放置控件
panel = wx.Panel(self, -1)
# 在x=20,y=20的位置,創(chuàng)建靜態(tài)文本控件
st = wx.StaticText(panel, -1, '我是靜態(tài)文本控件', pos=(20, 20))
# 在x=300,y=20的位置,創(chuàng)建靜態(tài)圖片
bmp = wx.Bitmap('res/forever.png')
sb = wx.StaticBitmap(panel, -1, bmp, pos=(280, 10))
# 在x=20, y=50的位置,創(chuàng)建文本輸入框,指定輸入框的寬度為260像素,高度默認(rèn)
tc1 = wx.TextCtrl(panel, -1, value='我是文本輸入框', pos=(20, 50), size=(260, -1))
# 在x=20, y=90的位置,創(chuàng)建文本輸入框,指定樣式為密碼
tc2 = wx.TextCtrl(panel, -1, value='我是密碼', pos=(20, 90), style=wx.TE_PASSWORD)
# 在x=20, y=130的位置,創(chuàng)建單選按鈕,成組的單選按鈕,第一個(gè)需要指定樣式wx.RB_GROUP
rb1 = wx.RadioButton(panel, -1, '單選按鈕1', pos=(20, 130), style=wx.RB_GROUP, name='rb1')
# 在x=100, y=130的位置,創(chuàng)建單選按鈕,不再需要指定樣式wx.RB_GROUP
rb2 = wx.RadioButton(panel, -1, '單選按鈕2', pos=(100, 130), name='rb2')
# 在x=180, y=130的位置,創(chuàng)建單選按鈕,不再需要指定樣式wx.RB_GROUP
rb3 = wx.RadioButton(panel, -1, '單選按鈕3', pos=(180, 130), name='rb3')
# 在x=20, y=160的位置,創(chuàng)建復(fù)選按鈕
cb1 = wx.CheckBox(panel, -1, '復(fù)選按鈕', pos=(20, 160))
# 在x=100, y=160的位置,創(chuàng)建復(fù)選按鈕,指定其樣式為wx.ALIGN_RIGHT
cb2 = wx.CheckBox(panel, -1, '文字在左側(cè)的復(fù)選按鈕', pos=(100, 160), style=wx.ALIGN_RIGHT)
# 在x=20,y=190的位置,創(chuàng)建按鈕
ch = wx.Choice(panel, -1, choices=['wxPython', 'PyQt', 'Tkinter'], pos=(20, 190), size=(100, -1))
ch.SetSelection(0)
# 在x=120,y=190的位置,創(chuàng)建按鈕
btn = wx.Button(panel, -1, '按鈕', pos=(150, 190))
# 在x=20,y=230的位置,創(chuàng)建文本框,指定大小為260*150,并指定其樣式為多行和只讀
tc3 = wx.TextCtrl(panel, -1, value='我是多行文本輸入框', pos=(20, 230), size=(260, 150), style=wx.TE_MULTILINE | wx.CB_READONLY)
if __name__ == '__main__':
app = wx.App() # 創(chuàng)建一個(gè)應(yīng)用程序
frame = MainFrame(None) # 創(chuàng)建主窗口
frame.Show() # 顯示窗主口
app.MainLoop() # 應(yīng)用程序進(jìn)入事件處理主循環(huán)
代碼運(yùn)行界面如下圖所示。
控件布局
3.1. 分區(qū)布局
上面的例子里,輸入框、按鈕等控件的位置由其pos參數(shù)確定,即絕對定位。絕對定位這種布局方式非常直觀,但不能自動(dòng)適應(yīng)窗口的大小變化。更普遍的方式是使用被稱為布局管理器的wx.Sizer來實(shí)現(xiàn)分區(qū)布局。所謂分區(qū)布局,就是將一個(gè)矩形區(qū)域沿水平或垂直方向分割成多個(gè)矩形區(qū)域,并可嵌套分區(qū)布局管理器wx.Sizer的派生類有很多種,最常用到是wx.BoxSizer和wx.StaticBoxSizer。
和一般的控件不同,布局管理器就像是一個(gè)魔法口袋:它是無形的,但可以裝進(jìn)不限數(shù)量的任意種類的控件——包括其他的布局管理器。當(dāng)然,魔法口袋也不是萬能的,它有一個(gè)限制條件:裝到里面的東西,要么是水平排列的,要么是垂直排列的,不能排成方陣。好在程序員可以不受限制地使用魔法口袋,當(dāng)我們需要排成方陣時(shí),可以先每一行使用一個(gè)魔法口袋,然后再把所有的行裝到一個(gè)魔法口袋中。
創(chuàng)建一個(gè)魔法口袋,裝進(jìn)幾樣?xùn)|西,然后在窗口中顯示的偽代碼是這樣的:
魔法口袋 = wx.BoxSizer() # 默認(rèn)是水平的,想要垂直放東西,需要加上 wx.VERTICAL 這個(gè)參數(shù)
魔法口袋.add(確認(rèn)按鈕, 0, wx.ALL, 0) # 裝入確認(rèn)按鈕
魔法口袋.add(取消按鈕, 0, wx.ALL, 0) # 裝入取消按鈕
窗口.SetSizer(魔法口袋) # 把魔法口袋放到窗口上
窗口.Layout() # 窗口重新布局
魔法口袋的 add() 方法總共有4個(gè)參數(shù):第1個(gè)參數(shù)很容易理解,就是要裝進(jìn)口袋的物品;第2個(gè)參數(shù)和所有 add() 方法的第2個(gè)參數(shù)之和的比,表示裝進(jìn)口袋的物品占用空間的比例,0表示物品多大就占多大地兒,不額外占用空間;第3個(gè)參數(shù)相對復(fù)雜些,除了約定裝進(jìn)口袋的物品在其占用的空間里面水平垂直方向的對齊方式外,還可以指定上下左右四個(gè)方向中的一個(gè)或多個(gè)方向的留白(padding);第4個(gè)參數(shù)就是留白像素?cái)?shù)。
下面是一個(gè)完整的例子。
import wx
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
self.SetTitle('分區(qū)布局')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetSize((640, 320)) # 設(shè)置窗口大小
self._init_ui() # 初始化界面
self.Center() # 窗口在屏幕上居中
def _init_ui(self):
'''初始化界面'''
# 創(chuàng)建容器面板
panel = wx.Panel(self, -1)
# 生成黑色背景的預(yù)覽面板
view = wx.Panel(panel, -1, style=wx.SUNKEN_BORDER)
view.SetBackgroundColour(wx.Colour(0, 0, 0))
# 生成按鈕和多行文本控件
btn_capture = wx.Button(panel, -1, '拍照', size=(100, -1))
btn_up = wx.Button(panel, -1, '↑', size=(30, 30))
btn_down = wx.Button(panel, -1, '↓', size=(30, 30))
btn_left = wx.Button(panel, -1, '←', size=(30, 30))
btn_right = wx.Button(panel, -1, '→', size=(30, 30))
tc = wx.TextCtrl(panel, -1, '', style=wx.TE_MULTILINE)
# 左右按鈕裝入一個(gè)水平布局管理器
sizer_arrow_mid = wx.BoxSizer()
sizer_arrow_mid.Add(btn_left, 0, wx.RIGHT, 16)
sizer_arrow_mid.Add(btn_right, 0, wx.LEFT, 16)
# 生成帶標(biāo)簽的垂直布局管理器
sizer_arrow = wx.StaticBoxSizer(wx.StaticBox(panel, -1, '方向鍵'), wx.VERTICAL)
sizer_arrow.Add(btn_up, 0, wx.ALIGN_CENTER|wx.ALL, 0) # 裝入上按鈕
sizer_arrow.Add(sizer_arrow_mid, 0, wx.TOP|wx.BOTTOM, 1) # 裝入左右按鈕
sizer_arrow.Add(btn_down, 0, wx.ALIGN_CENTER|wx.ALL, 0) # 裝入下按鈕
# 生成垂直布局管理器
sizer_right = wx.BoxSizer(wx.VERTICAL)
sizer_right.Add(btn_capture, 0, wx.ALL, 20) # 裝入拍照按鈕
sizer_right.Add(sizer_arrow, 0, wx.ALIGN_CENTER|wx.ALL, 0) # 裝入方向鍵
sizer_right.Add(tc, 1, wx.ALL, 10) # 裝入多行文本控件
# 生成水平布局管理器
sizer_max = wx.BoxSizer()
sizer_max.Add(view, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.BOTTOM, 5) # 裝入左側(cè)的預(yù)覽面板
sizer_max.Add(sizer_right, 0, wx.EXPAND|wx.ALL, 0) # 裝入右側(cè)的操作區(qū)
# 為容器面板指定布局管理器,并調(diào)用布局方法完成界面布局
panel.SetSizer(sizer_max)
panel.Layout()
if __name__ == '__main__':
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
3.2. 柵格布局
顧名思義,柵格布局就是將布局空間劃分成網(wǎng)格,將控件放置到不同的網(wǎng)格內(nèi)。柵格布局比較簡單,用起來非常方便。柵格布局布局管理器也有很多種,GridBagSizer是最常用的一種。下面是一個(gè)使用GridBagSizer實(shí)現(xiàn)柵格布局的例子。
import wx
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
self.SetTitle('柵格布局')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetSize((800, 440)) # 設(shè)置窗口大小
self._init_ui() # 初始化界面
self.Center() # 窗口在屏幕上居中
def _init_ui(self):
'''初始化界面'''
panel = wx.Panel(self, -1) # 創(chuàng)建容器面板
sizer = wx.GridBagSizer(10, 10)# 每個(gè)控件之間橫縱間隔10像素
st = wx.StaticText(panel, -1, '用戶名')
sizer.Add(st, (0, 0), flag=wx.TOP | wx.ALIGN_RIGHT, border=20) # 在第0行0列,距離上邊緣20像素,右對齊
userName = wx.TextCtrl(panel, -1)
sizer.Add(userName, (0, 1), (1, 3), flag=wx.EXPAND | wx.TOP, border=20) # 在第0行1列,跨3列,距離上邊緣20像素
sb = wx.StaticBitmap(panel, -1, wx.Bitmap('res/python.jpg'))
sizer.Add(sb, (0, 5), (7, 1), flag=wx.TOP | wx.RIGHT, border=20) # 在第0行4列,跨7行,距離上右邊緣20像素
st = wx.StaticText(panel, -1, '密碼')
sizer.Add(st, (1, 0), flag=wx.ALIGN_RIGHT) # 在第1行0列,右對齊
password = wx.TextCtrl(panel, -1, style=wx.TE_PASSWORD)
sizer.Add(password, (1, 1), (1, 3), flag=wx.EXPAND) # 在第1行1列,跨3列
st = wx.StaticText(panel, -1, '學(xué)歷')
sizer.Add(st, (2, 0), flag=wx.ALIGN_RIGHT) # 在第2行0列,右對齊
level1 = wx.RadioButton(panel, -1, '???)
sizer.Add(level1, (2, 1)) # 在第2行1列
level2 = wx.RadioButton(panel, -1, '本科')
sizer.Add(level2, (2, 2)) # 在第2行1列
level3 = wx.RadioButton(panel, -1, '研究生及以上')
sizer.Add(level3, (2, 3)) # 在第2行1列
st = wx.StaticText(panel, -1, '職業(yè)')
sizer.Add(st, (3, 0), flag=wx.ALIGN_RIGHT) # 在第3行0列,右對齊
professional = wx.Choice(panel, -1, choices=['學(xué)生', '程序員', '軟件工程師', '系統(tǒng)架構(gòu)師'])
professional.SetSelection(0)
sizer.Add(professional, (3, 1), (1, 3), flag=wx.EXPAND) # 在第3行1列,跨3列
# 語言技能
st = wx.StaticText(panel, -1, '語言技能')
sizer.Add(st, (4, 0), flag=wx.ALIGN_RIGHT | wx.LEFT, border=20) # 在第4行0列,距離左邊緣20像素,右對齊
choices = ['C', 'C++', 'Java', 'Python', 'Lua', 'JavaScript', 'TypeScript', 'Go', 'Rust']
language = wx.ListBox(panel, -1, choices=choices, style=wx.LB_EXTENDED)
sizer.Add(language, (4, 1), (1, 3), flag=wx.EXPAND) # 在第4行1列,跨3列
isJoin = wx.CheckBox(panel, -1, '已加入QQ群', style=wx.ALIGN_RIGHT)
sizer.Add(isJoin, (5, 0), (1, 4), flag=wx.ALIGN_CENTER) # 在第5行0列,跨4列, 居中
btn = wx.Button(panel, -1, '提交')
sizer.Add(btn, (6, 0), (1, 4), flag=wx.ALIGN_CENTER | wx.BOTTOM, border=20) # 在第6行0列,跨4列, 居中
sizer.AddGrowableRow(4) # 設(shè)置第4行可增長
sizer.AddGrowableCol(3) # 設(shè)置第3列可增長
panel.SetSizer(sizer)
panel.Layout()
if __name__ == '__main__':
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
事件驅(qū)動(dòng)
一個(gè)桌面程序不單是控件的羅列,更重要的是對外部的刺激——包括用戶的操作做出反應(yīng)。如果把窗體和控件比作是桌面程序的軀體,那么響應(yīng)外部刺激就是它的靈魂。wxPython的靈魂是事件驅(qū)動(dòng)機(jī)制:當(dāng)某事件發(fā)生時(shí),程序就會(huì)自動(dòng)執(zhí)行預(yù)先設(shè)定的動(dòng)作。
4.1 事件
所謂事件,就是我們的程序在運(yùn)行中發(fā)生的事兒。事件可以是低級(jí)的用戶動(dòng)作,如鼠標(biāo)移動(dòng)或按鍵按下,也可以是高級(jí)的用戶動(dòng)作(定義在wxPython的窗口部件中的),如單擊按鈕或菜單選擇。事件可以產(chǎn)生自系統(tǒng),如關(guān)機(jī),,也可以由用戶自定義事件。
除了用戶自定義事件,在wxPython中我習(xí)慣把事件分為4類:
鼠標(biāo)事件:鼠標(biāo)左右中鍵和滾輪動(dòng)作,以及鼠標(biāo)移動(dòng)等事件
鍵盤事件:用戶敲擊鍵盤產(chǎn)生的事件
控件事件:發(fā)生在控件上的事件,比如按鈕被按下、輸入框內(nèi)容改變等
系統(tǒng)事件:關(guān)閉窗口、改變窗口大小、重繪、定時(shí)器等事件
事實(shí)上,這個(gè)分類方法不夠嚴(yán)謹(jǐn)。比如,wx.Frame作為一個(gè)控件,關(guān)閉和改變大小也是控件事件,不過這一類事件通常都由系統(tǒng)綁定了行為?;诖耍梢灾匦露x所謂的控件事件,是指發(fā)生在控件上的、系統(tǒng)并未預(yù)定義行為的事件。
常用的鼠標(biāo)事件包括:
wx.EVT_LEFT_DOWN - 左鍵按下
wx.EVT_LEFT_UP - 左鍵彈起
wx.EVT_LEFT_DCLICK - 左鍵雙擊
wx.EVT_RIGHT_DOWN - 右鍵按下
wx.EVT_RIGHT_UP - 右鍵彈起
wx.EVT_RIGHT_DCLICK - 右鍵雙擊
wx.EVT_MOTION - 鼠標(biāo)移動(dòng)
wx.EVT_MOUSEWHEEL - 滾輪滾動(dòng)
wx.EVT_MOUSE_EVENTS - 所有的鼠標(biāo)事件
常用的鍵盤事件有:
wx.EVT_KEY_DOWN - 按鍵按下
wx.EVT_KEY_UP - 按鍵彈起
常用的系統(tǒng)事件包括:
wx.EVT_CLOSE - 關(guān)閉
wx.EVT_SIZE - 改變大小
wx.EVT_TIMER - 定時(shí)器事件
wx.EVT_PAINT - 重繪
wx.EVT_ERASE_BACKGROUND -背景擦除
常用的控件事件包括:
wx.EVT_BUTTON - 點(diǎn)擊按鈕
wx.EVT_CHOICE - 下拉框改變選擇
wx.EVT_TEXT - 輸入框內(nèi)容改變
wx.EVT_TEXT_ENTER - 輸入框回車
wx.EVT_RADIOBOX - 單選框改變選擇
wx.EVT_CHECKBOX - 點(diǎn)擊復(fù)選框
4.2 事件綁定
事件驅(qū)動(dòng)機(jī)制有三個(gè)要素:事件、事件函數(shù)和事件綁定。比如,當(dāng)一個(gè)按鈕被點(diǎn)擊時(shí),就會(huì)觸發(fā)按鈕點(diǎn)擊事件,該事件如果綁定了事件函數(shù),事件函數(shù)就會(huì)被調(diào)用。所有的事件函數(shù)都以事件對象為參數(shù),事件對象提供了事件的詳細(xì)信息,比如鍵盤按下事件的事件對象就包含了被按下的鍵的信息。
下面這個(gè)例子演示了如何定義事件函數(shù),以及綁定事件和事件函數(shù)之間的關(guān)聯(lián)關(guān)系。
import wx
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
self.SetTitle('事件和事件函數(shù)的綁定')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetBackgroundColour((224, 224, 224)) # 設(shè)置窗口背景色
self.SetSize((520, 220))
self._init_ui()
self.Center()
def _init_ui(self):
'''初始化界面'''
wx.StaticText(self, -1, '第一行輸入框:', pos=(40, 50), size=(100, -1), style=wx.ALIGN_RIGHT)
wx.StaticText(self, -1, '第二行輸入框:', pos=(40, 80), size=(100, -1), style=wx.ALIGN_RIGHT)
self.tip = wx.StaticText(self, -1, u'', pos=(145, 110), size=(150, -1), style=wx.ST_NO_AUTORESIZE)
self.tc1 = wx.TextCtrl(self, -1, '', pos=(145, 50), size=(150, -1), name='TC01', style=wx.TE_CENTER)
self.tc2 = wx.TextCtrl(self, -1, '', pos=(145, 80), size=(150, -1), name='TC02', style=wx.TE_PASSWORD|wx.ALIGN_RIGHT)
btn_mea = wx.Button(self, -1, '鼠標(biāo)左鍵事件', pos=(350, 50), size=(100, 25))
btn_meb = wx.Button(self, -1, '鼠標(biāo)所有事件', pos=(350, 80), size=(100, 25))
btn_close = wx.Button(self, -1, '關(guān)閉窗口', pos=(350, 110), size=(100, 25))
self.tc1.Bind(wx.EVT_TEXT, self.on_text) # 綁定文本內(nèi)容改變事件
self.tc2.Bind(wx.EVT_TEXT, self.on_text) # 綁定文本內(nèi)容改變事件
btn_close.Bind(wx.EVT_BUTTON, self.on_close, btn_close) # 綁定按鍵事件
btn_close.Bind(wx.EVT_MOUSEWHEEL, self.on_wheel) # 綁定鼠標(biāo)滾輪事件
btn_mea.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) # 綁定鼠標(biāo)左鍵按下
btn_mea.Bind(wx.EVT_LEFT_UP, self.on_left_up) # 綁定鼠標(biāo)左鍵彈起
btn_meb.Bind(wx.EVT_MOUSE_EVENTS, self.on_mouse) # 綁定所有鼠標(biāo)事件
self.Bind(wx.EVT_CLOSE, self.on_close) # 綁定窗口關(guān)閉事件
self.Bind(wx.EVT_SIZE, self.on_size) # 綁定改變窗口大小事件
self.Bind(wx.EVT_KEY_DOWN, self.on_key_down) # 綁定鍵盤事件
def on_text(self, evt):
'''輸入框事件函數(shù)'''
obj = evt.GetEventObject()
objName = obj.GetName()
text = evt.GetString()
if objName == 'TC01':
self.tc2.SetValue(text)
elif objName == 'TC02':
self.tc1.SetValue(text)
def on_size(self, evt):
'''改變窗口大小事件函數(shù)'''
print('你想改變窗口,但是事件被Skip了,所以沒有任何改變')
evt.Skip() # 注釋掉此行(事件繼續(xù)傳遞),窗口大小才會(huì)被改變
def on_close(self, evt):
'''關(guān)閉窗口事件函數(shù)'''
dlg = wx.MessageDialog(None, '確定要關(guān)閉本窗口?', '操作提示', wx.YES_NO | wx.ICON_QUESTION)
if(dlg.ShowModal() == wx.ID_YES):
self.Destroy()
def on_left_down(self, evt):
'''左鍵按下事件函數(shù)'''
self.tip.SetLabel('左鍵按下')
def on_left_up(self, evt):
'''左鍵彈起事件函數(shù)'''
self.tip.SetLabel('左鍵彈起')
def on_wheel(self, evt):
'''鼠標(biāo)滾輪事件函數(shù)'''
vector = evt.GetWheelRotation()
self.tip.SetLabel(str(vector))
def on_mouse(self, evt):
'''鼠標(biāo)事件函數(shù)'''
self.tip.SetLabel(str(evt.EventType))
def on_key_down(self, evt):
'''鍵盤事件函數(shù)'''
key = evt.GetKeyCode()
self.tip.SetLabel(str(key))
if __name__ == '__main__':
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
兩個(gè)輸入框,一個(gè)明文居中,一個(gè)密寫右齊,但內(nèi)容始終保持同步。輸入焦點(diǎn)不在輸入框的時(shí)候,敲擊鍵盤,界面顯示對應(yīng)的鍵值。最上面的按鈕響應(yīng)鼠標(biāo)左鍵的按下和彈起事件,中間的按鈕響應(yīng)所有的鼠標(biāo)事件,下面的按鈕響應(yīng)滾輪事件和按鈕按下的事件。另外,程序還綁定了窗口關(guān)閉事件,重新定義了關(guān)閉函數(shù),增加了確認(rèn)選擇。
程序框架
5.1 菜單欄、工具欄和狀態(tài)欄
通常,一個(gè)完整的窗口程序一般都有菜單欄、工具欄和狀態(tài)欄。下面的代碼演示了如何創(chuàng)建菜單欄、工具欄和狀態(tài)欄,順便演示了類的靜態(tài)屬性的定義和用法。不過,說實(shí)話,wx的工具欄有點(diǎn)丑,幸好,wx還有一個(gè) AUI 的工具欄比較漂亮,我會(huì)在后面的例子里演示它的用法。
import wx
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
id_open = wx.NewIdRef()
id_save = wx.NewIdRef()
id_quit = wx.NewIdRef()
id_help = wx.NewIdRef()
id_about = wx.NewIdRef()
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
self.SetTitle('菜單、工具欄、狀態(tài)欄')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetBackgroundColour((224, 224, 224)) # 設(shè)置窗口背景色
self.SetSize((360, 180))
self._create_menubar() # 菜單欄
self._create_toolbar() # 工具欄
self._create_statusbar() # 狀態(tài)欄
self.Center()
def _create_menubar(self):
'''創(chuàng)建菜單欄'''
self.mb = wx.MenuBar()
# 文件菜單
m = wx.Menu()
m.Append(self.id_open, '打開文件')
m.Append(self.id_save, '保存文件')
m.AppendSeparator()
m.Append(self.id_quit, '退出系統(tǒng)')
self.mb.Append(m, '文件')
self.Bind(wx.EVT_MENU, self.on_open, id=self.id_open)
self.Bind(wx.EVT_MENU, self.on_save, id=self.id_save)
self.Bind(wx.EVT_MENU, self.on_quit, id=self.id_quit)
# 幫助菜單
m = wx.Menu()
m.Append(self.id_help, '幫助主題')
m.Append(self.id_about, '關(guān)于...')
self.mb.Append(m, '幫助')
self.Bind(wx.EVT_MENU, self.on_help,id=self.id_help)
self.Bind(wx.EVT_MENU, self.on_about,id=self.id_about)
self.SetMenuBar(self.mb)
def _create_toolbar(self):
'''創(chuàng)建工具欄'''
bmp_open = wx.Bitmap('res/open_mso.png', wx.BITMAP_TYPE_ANY) # 請自備按鈕圖片
bmp_save = wx.Bitmap('res/save_mso.png', wx.BITMAP_TYPE_ANY) # 請自備按鈕圖片
bmp_help = wx.Bitmap('res/help_mso.png', wx.BITMAP_TYPE_ANY) # 請自備按鈕圖片
bmp_about = wx.Bitmap('res/info_mso.png', wx.BITMAP_TYPE_ANY) # 請自備按鈕圖片
self.tb = wx.ToolBar(self)
self.tb.SetToolBitmapSize((16,16))
self.tb.AddTool(self.id_open, '打開文件', bmp_open, shortHelp='打開', kind=wx.ITEM_NORMAL)
self.tb.AddTool(self.id_save, '保存文件', bmp_save, shortHelp='保存', kind=wx.ITEM_NORMAL)
self.tb.AddSeparator()
self.tb.AddTool(self.id_help, '幫助', bmp_help, shortHelp='幫助', kind=wx.ITEM_NORMAL)
self.tb.AddTool(self.id_about, '關(guān)于', bmp_about, shortHelp='關(guān)于', kind=wx.ITEM_NORMAL)
self.tb.Realize()
def _create_statusbar(self):
'''創(chuàng)建狀態(tài)欄'''
self.sb = self.CreateStatusBar()
self.sb.SetFieldsCount(3)
self.sb.SetStatusWidths([-2, -1, -1])
self.sb.SetStatusStyles([wx.SB_RAISED, wx.SB_RAISED, wx.SB_RAISED])
self.sb.SetStatusText('狀態(tài)信息0', 0)
self.sb.SetStatusText('', 1)
self.sb.SetStatusText('狀態(tài)信息2', 2)
def on_open(self, evt):
'''打開文件'''
self.sb.SetStatusText(u'打開文件', 1)
def on_save(self, evt):
'''保存文件'''
self.sb.SetStatusText(u'保存文件', 1)
def on_quit(self, evt):
'''退出系統(tǒng)'''
self.sb.SetStatusText(u'退出系統(tǒng)', 1)
self.Destroy()
def on_help(self, evt):
'''幫助'''
self.sb.SetStatusText(u'幫助', 1)
def on_about(self, evt):
'''關(guān)于'''
self.sb.SetStatusText(u'關(guān)于', 1)
if __name__ == '__main__':
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
代碼里面用到了4個(gè)16x16的工具按鈕,請自備4個(gè)圖片文件,保存路徑請查看代碼中的注釋。代碼運(yùn)行界面如下圖所示。
5.2 Aui框架
Advanced User Interface,簡稱AUI,是wxPython的子模塊,使用AUI可以方便地開發(fā)出美觀、易用的用戶界面。從2.8.9.2版本之后,wxPython增加了一個(gè)高級(jí)通用部件庫Advanced Generic Widgets,簡稱AGW庫, AGW庫也提供了AUI模塊 wx.lib.agw.aui,而 wx.aui也依然保留著。相比較而言,我更喜歡使用wx.lib.agw的AUI框架。
使用AUI框架可以概括為以下四步:
創(chuàng)建一個(gè)布局管理器:mgr = aui.AuiManager()
告訴主窗口由mgr來管理界面:mgr.SetManagedWindow()
添加界面上的各個(gè)區(qū)域:mgr.AddPane()
更新界面顯示:mgr.Update()
下面的代碼演示了如何使用AUI布局管理器創(chuàng)建和管理窗口界面。
import wx
import wx.lib.agw.aui as aui
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
id_open = wx.NewIdRef()
id_save = wx.NewIdRef()
id_quit = wx.NewIdRef()
id_help = wx.NewIdRef()
id_about = wx.NewIdRef()
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
self.SetTitle('菜單、工具欄、狀態(tài)欄')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetBackgroundColour((224, 224, 224)) # 設(shè)置窗口背景色
self.SetSize((640, 480))
self._init_ui()
self.Center()
def _init_ui(self):
'''初始化界面'''
self.tb1 = self._create_toolbar()
self.tb2 = self._create_toolbar()
self.tbv = self._create_toolbar('V')
p_left = wx.Panel(self, -1)
p_center0 = wx.Panel(self, -1)
p_center1 = wx.Panel(self, -1)
p_bottom = wx.Panel(self, -1)
btn = wx.Button(p_left, -1, '切換', pos=(30,200), size=(100, -1))
btn.Bind(wx.EVT_BUTTON, self.on_switch)
text0 = wx.StaticText(p_center0, -1, '我是第1頁', pos=(40, 100), size=(200, -1), style=wx.ALIGN_LEFT)
text1 = wx.StaticText(p_center1, -1, '我是第2頁', pos=(40, 100), size=(200, -1), style=wx.ALIGN_LEFT)
self._mgr = aui.AuiManager()
self._mgr.SetManagedWindow(self)
self._mgr.AddPane(self.tb1,
aui.AuiPaneInfo().Name('ToolBar1').Caption('工具條').ToolbarPane().Top().Row(0).Position(0).Floatable(False)
)
self._mgr.AddPane(self.tb2,
aui.AuiPaneInfo().Name('ToolBar2').Caption('工具條').ToolbarPane().Top().Row(0).Position(1).Floatable(True)
)
self._mgr.AddPane(self.tbv,
aui.AuiPaneInfo().Name('ToolBarV').Caption('工具條').ToolbarPane().Right().Floatable(True)
)
self._mgr.AddPane(p_left,
aui.AuiPaneInfo().Name('LeftPanel').Left().Layer(1).MinSize((200,-1)).Caption('操作區(qū)').MinimizeButton(True).MaximizeButton(True).CloseButton(True)
)
self._mgr.AddPane(p_center0,
aui.AuiPaneInfo().Name('CenterPanel0').CenterPane().Show()
)
self._mgr.AddPane(p_center1,
aui.AuiPaneInfo().Name('CenterPanel1').CenterPane().Hide()
)
self._mgr.AddPane(p_bottom,
aui.AuiPaneInfo().Name('BottomPanel').Bottom().MinSize((-1,100)).Caption('消息區(qū)').CaptionVisible(False).Resizable(True)
)
self._mgr.Update()
def _create_toolbar(self, d='H'):
'''創(chuàng)建工具欄'''
bmp_open = wx.Bitmap('res/open_mso.png', wx.BITMAP_TYPE_ANY)
bmp_save = wx.Bitmap('res/save_mso.png', wx.BITMAP_TYPE_ANY)
bmp_help = wx.Bitmap('res/help_mso.png', wx.BITMAP_TYPE_ANY)
bmp_about = wx.Bitmap('res/info_mso.png', wx.BITMAP_TYPE_ANY)
if d.upper() in ['V', 'VERTICAL']:
tb = aui.AuiToolBar(self, -1, wx.DefaultPosition, wx.DefaultSize, agwStyle=aui.AUI_TB_TEXT|aui.AUI_TB_VERTICAL)
else:
tb = aui.AuiToolBar(self, -1, wx.DefaultPosition, wx.DefaultSize, agwStyle=aui.AUI_TB_TEXT)
tb.SetToolBitmapSize(wx.Size(16, 16))
tb.AddSimpleTool(self.id_open, '打開', bmp_open, '打開文件')
tb.AddSimpleTool(self.id_save, '保存', bmp_save, '保存文件')
tb.AddSeparator()
tb.AddSimpleTool(self.id_help, '幫助', bmp_help, '幫助')
tb.AddSimpleTool(self.id_about, '關(guān)于', bmp_about, '關(guān)于')
tb.Realize()
return tb
def on_switch(self, evt):
'''切換信息顯示窗口'''
p0 = self._mgr.GetPane('CenterPanel0')
p1 = self._mgr.GetPane('CenterPanel1')
p0.Show(not p0.IsShown())
p1.Show(not p1.IsShown())
self._mgr.Update()
if __name__ == '__main__':
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
示例和技巧
6.1. 相冊
前文的例子中已經(jīng)展示了wx.StaticBitmap控件作為圖像容器的例子,下面的例子用它制作了一個(gè)相冊,點(diǎn)擊前翻后翻按鈕可在多張照片之間循環(huán)切換。
import wx
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
self.SetTitle('相冊')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetBackgroundColour((224, 224, 224)) # 設(shè)置窗口背景色
self.SetSize((980, 680))
self._init_ui()
self.Center()
def _init_ui(self):
'''初始化界面'''
self.curr = 0
self.photos = ('res/DSC03363.jpg', 'res/DSC03394.jpg', 'res/DSC03402.jpg')
bmp = wx.Bitmap(self.photos[self.curr])
self.album = wx.StaticBitmap(self, -1, bmp, pos=(280, 10))
btn_1 = wx.Button(self, -1, '<', size=(80, 30), name='prev')
btn_2 = wx.Button(self, -1, '>', size=(80, 30), name='next')
btn_1.Bind(wx.EVT_BUTTON, self.on_btn)
btn_2.Bind(wx.EVT_BUTTON, self.on_btn)
sizer_btn = wx.BoxSizer()
sizer_btn.Add(btn_1, 0, wx.RIGHT, 20)
sizer_btn.Add(btn_2, 0, wx.LEFT, 20)
sizer_max = wx.BoxSizer(wx.VERTICAL)
sizer_max.Add(self.album, 1, wx.EXPAND | wx.ALL, 10)
sizer_max.Add(sizer_btn, 0, wx. ALIGN_CENTER | wx.BOTTOM, 20)
self.SetSizer(sizer_max)
self.Layout()
def on_btn(self, evt):
'''響應(yīng)按鍵'''
name = evt.GetEventObject().GetName()
if name == '<':
self.curr = (self.curr-1)%len(self.photos)
else:
self.curr = (self.curr+1)%len(self.photos)
self.album.SetBitmap(wx.Bitmap(self.photos[self.curr]))
if __name__ == '__main__':
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
6.2. 會(huì)彈琴的計(jì)算器
幾乎所有的GUI課程都會(huì)用計(jì)算器作為例子,wxPython怎能缺席呢?下面這個(gè)計(jì)算器除了常規(guī)的計(jì)算外,按下每個(gè)鍵都會(huì)發(fā)出不同的音調(diào),粗通樂理就可以彈奏出樂曲。此外,代碼中使用了wx.lib控件庫的按鍵,略帶3D風(fēng)格。
import wx
import wx.lib.buttons as wxbtn
import winsound
class MainFrame(wx.Frame):
'''桌面程序主窗口類'''
def __init__(self):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent=None, style=wx.CAPTION|wx.SYSTEM_MENU|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SIMPLE_BORDER)
self.SetTitle('會(huì)彈琴的計(jì)算器')
self.SetIcon(wx.Icon('res/wx.ico', wx.BITMAP_TYPE_ICO))
self.SetBackgroundColour((217, 228, 241))
self.SetSize((287, 283))
self._init_ui()
self.Center()
def _init_ui(self):
'''初始化界面'''
# 定義按鍵排列順序和名稱
keys = [
['(', ')', 'Back', 'Clear'],
['7', '8', '9', '/'],
['4', '5', '6', '*'],
['1', '2', '3', '-'],
['0', '.', '=', '+']
]
# 指定每個(gè)按鍵聲音的頻率,523赫茲就是C調(diào)中音
self.keySound = {
'(':392, ')': 440, '0':494, '1':523, '2':587, '3':659, '4':698, '5':784, '6':880, '7':988, '8':1047,
'9':1175, '.':1318, '+':523, '-':587, '*':659, '/':698, 'Clear':784, 'Back':880, '=':2000
}
# 用輸入框控件作為計(jì)算器屏幕,設(shè)置為只讀(wx.TE_READONLY)和右齊(wx.ALIGN_RIGHT)
self.screen = wx.TextCtrl(self, -1, '', pos=(10,10), size=(252,45), style=wx.TE_READONLY|wx.ALIGN_RIGHT)
self.screen.SetFont(wx.Font(20, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False, '微軟雅黑')) # 設(shè)置字體字號(hào)
self.screen.SetBackgroundColour((0, 0, 0)) # 設(shè)置屏幕背景色
self.screen.SetForegroundColour((0, 255, 0)) # 設(shè)置屏幕前景色
# 按鍵布局參數(shù)
btn_size = (60, 30) # 定義按鍵的尺寸,便于統(tǒng)一修改
x0, y0 = (10, 65) # 定義按鍵區(qū)域的相對位置
dx, dy = (64, 34) # 定義水平步長和垂直步長
# 生成所有按鍵
for i in range(len(keys)):
for j in range(len(keys[i])):
key = keys[i][j]
btn = wxbtn.GenButton(self, -1, key, pos=(x0+j*dx, y0+i*dy), size=btn_size, name=key)
if key in ['0','1','2','3','4','5','6','7','8','9','.']:
btn.SetBezelWidth(1) # 設(shè)置3D效果
btn.SetBackgroundColour(wx.Colour(217, 228, 241)) # 定義按鍵的背景色
elif key in ['(',')','Back','Clear']:
btn.SetBezelWidth(2)
btn.SetBackgroundColour(wx.Colour(217, 220, 235))
btn.SetForegroundColour(wx.Colour(224, 60, 60))
elif key in ['+','-','*','/']:
btn.SetBezelWidth(2)
btn.SetBackgroundColour(wx.Colour(246, 225, 208))
btn.SetForegroundColour(wx.Colour(60, 60, 224))
else:
btn.SetBezelWidth(2)
btn.SetBackgroundColour(wx.Colour(245, 227, 129))
btn.SetForegroundColour(wx.Colour(60, 60, 224))
btn.SetToolTip(u'顯示計(jì)算結(jié)果')
self.Bind(wx.EVT_BUTTON, self.on_button) # 將按鈕事件綁定在所有按鈕上
def on_button(self, evt):
'''響應(yīng)鼠標(biāo)左鍵按下'''
obj = evt.GetEventObject() # 獲取事件對象(哪個(gè)按鈕被按)
key = obj.GetName() # 獲取事件對象的名字
self.PlayKeySound(key) # 播放按鍵對應(yīng)頻率的聲音
if self.screen.GetValue == 'Error':
self.screen.SetValue('')
if key == 'Clear': # 按下了清除鍵,清空屏幕
self.screen.SetValue('')
elif key == 'Back': # 按下了回退鍵,去掉最后一個(gè)輸入字符
content = self.screen.GetValue()
if content:
self.screen.SetValue(content[:-1])
elif key == '=': # 按下了等號(hào)鍵,則計(jì)算
try:
result = str(eval(self.screen.GetValue()))
except:
result = 'Error'
self.screen.SetValue(result)
else: # 按下了其他鍵,追加到顯示屏上
self.screen.AppendText(key)
def PlayKeySound(self, key, Dur=100):
'''播放按鍵聲音'''
winsound.Beep(self.keySound[key], Dur)
if __name__ == '__main__':
app = wx.App()
frame = MainFrame()
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
6.3. 定時(shí)器和線程
在一個(gè)桌面程序中,GUI線程是主線程,其他線程若要更新顯示內(nèi)容,Tkinter使用的是類型對象,PyQt使用的信號(hào)和槽機(jī)制,wxPython則相對原始:它允許子線程更新GUI,但需要借助于wx.CallAfter()函數(shù)。
這個(gè)例子里面設(shè)計(jì)了一個(gè)數(shù)字式鐘表,一個(gè)秒表,秒表顯示精度十分之一毫秒。從代碼設(shè)計(jì)上來說沒有任何難度,實(shí)現(xiàn)的方法有很多種,可想要達(dá)到一個(gè)較好的顯示效果,卻不是一件容易的事情。請注意體會(huì) wx.CallAfter() 的使用條件。
import wx
import time
import threading
class MainFrame(wx.Frame):
'''桌面程序主窗口類'''
def __init__(self):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent=None, style=wx.CAPTION|wx.SYSTEM_MENU|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SIMPLE_BORDER)
self.SetTitle('定時(shí)器和線程')
self.SetIcon(wx.Icon('res/wx.ico', wx.BITMAP_TYPE_ICO))
self.SetBackgroundColour((224, 224, 224))
self.SetSize((320, 300))
self._init_ui()
self.Center()
def _init_ui(self):
'''初始化界面'''
font = wx.Font(30, wx.DECORATIVE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, 'Monaco')
self.clock = wx.StaticText(self, -1, '08:00:00', pos=(50,50), size=(200,50), style=wx.TE_CENTER|wx.SUNKEN_BORDER)
self.clock.SetForegroundColour(wx.Colour(0, 224, 32))
self.clock.SetBackgroundColour(wx.Colour(0, 0, 0))
self.clock.SetFont(font)
self.stopwatch = wx.StaticText(self, -1, '0:00:00.00', pos=(50,150), size=(200,50), style=wx.TE_CENTER|wx.SUNKEN_BORDER)
self.stopwatch.SetForegroundColour(wx.Colour(0, 224, 32))
self.stopwatch.SetBackgroundColour(wx.Colour(0, 0, 0))
self.stopwatch.SetFont(font)
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
self.timer.Start(50)
self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
self.sec_last = None
self.is_start = False
self.t_start = None
thread_sw = threading.Thread(target=self.StopWatchThread)
thread_sw.setDaemon(True)
thread_sw.start()
def on_timer(self, evt):
'''定時(shí)器函數(shù)'''
t = time.localtime()
if t.tm_sec != self.sec_last:
self.clock.SetLabel('%02d:%02d:%02d'%(t.tm_hour, t.tm_min, t.tm_sec))
self.sec_last = t.tm_sec
def on_key_down(self, evt):
'''鍵盤事件函數(shù)'''
if evt.GetKeyCode() == wx.WXK_SPACE:
self.is_start = not self.is_start
self.t_start= time.time()
elif evt.GetKeyCode() == wx.WXK_ESCAPE:
self.is_start = False
self.stopwatch.SetLabel('0:00:00.00')
def StopWatchThread(self):
'''線程函數(shù)'''
while True:
if self.is_start:
t = time.time() - self.t_start
ti = int(t)
wx.CallAfter(self.stopwatch.SetLabel, '%d:%02d:%02d.%.02d'%(ti//3600, ti//60, ti%60, int((t-ti)*100)))
time.sleep(0.02)
if __name__ == '__main__':
app = wx.App()
frame = MainFrame()
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。界面上方的時(shí)鐘一直再跑,下方的秒表則是按鍵啟動(dòng)或停止。
6.4. DC繪圖
DC 是 Device Context 的縮寫,字面意思是設(shè)備上下文——我一直不能正確理解DC這個(gè)中文名字,也找不到更合適的說法,所以,我堅(jiān)持使用DC而不是設(shè)備上下文。DC可以在屏幕上繪制點(diǎn)線面,當(dāng)然也可以繪制文本和圖像。事實(shí)上,在底層所有控件都是以位圖形式繪制在屏幕上的,這意味著,我們一旦掌握了DC這個(gè)工具,就可以自己創(chuàng)造我們想要的控件了
DC有很多種,PaintDC,ClientDC,MemoryDC等。通常,我們可以使用 ClientDC 和 MemoryDC,PaintDC 是發(fā)生重繪事件(wx.EVT_PAINT)時(shí)系統(tǒng)使用的。使用 ClientDC 繪圖時(shí),需要記錄繪制的每一步工作,不然,系統(tǒng)重繪時(shí)會(huì)令我們前功盡棄——這是使用DC最容易犯的錯(cuò)誤。
import wx
class MainFrame(wx.Frame):
'''桌面程序主窗口類'''
def __init__(self):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent=None, style=wx.CAPTION|wx.SYSTEM_MENU|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SIMPLE_BORDER)
self.SetTitle('使用DC繪圖')
self.SetIcon(wx.Icon('res/wx.ico', wx.BITMAP_TYPE_ICO))
self.SetBackgroundColour((224, 224, 224))
self.SetSize((800, 480))
self._init_ui()
self.Center()
def _init_ui(self):
'''初始化界面'''
self.palette = wx.Panel(self, -1, style=wx.SUNKEN_BORDER)
self.palette.SetBackgroundColour(wx.Colour(0, 0, 0))
btn_base = wx.Button(self, -1, '文字和圖片', size=(100, -1))
sizer_max = wx.BoxSizer()
sizer_max.Add(self.palette, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.BOTTOM, 5)
sizer_max.Add(btn_base, 0, wx.ALL, 20)
self.SetAutoLayout(True)
self.SetSizer(sizer_max)
self.Layout()
btn_base.Bind(wx.EVT_BUTTON, self.on_base)
self.palette.Bind(wx.EVT_MOUSE_EVENTS, self.on_mouse)
self.palette.Bind(wx.EVT_PAINT, self.on_paint)
self.xy = None
self.lines = list()
self.img = wx.Bitmap('res/forever.png', wx.BITMAP_TYPE_ANY)
self.update_palette()
def on_mouse(self, evt):
'''移動(dòng)鼠標(biāo)畫線'''
if evt.EventType == 10030: #左鍵按下
self.xy = (evt.x, evt.y)
elif evt.EventType == 10031: #左鍵彈起
self.xy = None
elif evt.EventType == 10036: #鼠標(biāo)移動(dòng)
if self.xy:
dc = wx.ClientDC(self.palette)
dc.SetPen(wx.Pen(wx.Colour(0,224,0), 2))
dc.DrawLine(self.xy[0], self.xy[1], evt.x, evt.y)
self.lines.append((self.xy[0], self.xy[1], evt.x, evt.y))
self.xy = (evt.x, evt.y)
def on_base(self, evt):
'''DC基本方法演示'''
img = wx.Bitmap('res/forever.png', wx.BITMAP_TYPE_ANY)
w, h = self.palette.GetSize()
dc = wx.ClientDC(self.palette)
dc.SetPen(wx.Pen(wx.Colour(224,0,0), 1))
dc.SetBrush(wx.Brush(wx.Colour(0,80,80) ))
dc.DrawRectangle(10,10,w-22,h-22)
dc.DrawLine(10,h/2,w-12,h/2)
dc.DrawBitmap(img, 10, 10)
dc.SetTextForeground(wx.Colour(224,224,224))
dc.SetFont(wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, 'Comic Sans MS'))
dc.DrawText('霜重閑愁起', 100, 360)
dc.DrawRotatedText('春深風(fēng)也疾', 400, 360, 30)
def on_paint(self, evt):
'''響應(yīng)重繪事件'''
dc = wx.PaintDC(self.palette)
self.paint(dc)
def update_palette(self):
'''刷新畫板'''
dc = wx.ClientDC(self.palette)
self.paint(dc)
def paint(self, dc):
'''繪圖'''
dc.Clear()
dc.SetPen(wx.Pen(wx.Colour(0,224,0), 2))
for line in self.lines:
dc.DrawLine(line[0],line[1],line[2],line[3])
if __name__ == '__main__':
app = wx.App()
frame = MainFrame()
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
6.5. 內(nèi)嵌瀏覽器
wx.html2是wxPython擴(kuò)展模塊中封裝得最干凈漂亮的模塊之一,它被設(shè)計(jì)為允許為每個(gè)端口創(chuàng)建多個(gè)后端,盡管目前只有一個(gè)可用。它與wx.html.HtmlWindow的不同之處在于,每個(gè)后端實(shí)際上都是一個(gè)完整的渲染引擎,MSW上是Trident, macOS和GTK上是Webkit。wx.html2渲染web文檔,對于HTML、CSS和javascript都可以有很好的支持。
import wx
import wx.html2 as webview
class MainFrame(wx.Frame):
'''桌面程序主窗口類'''
def __init__(self):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent=None)
self.SetTitle('內(nèi)嵌瀏覽器')
self.SetIcon(wx.Icon('res/wx.ico', wx.BITMAP_TYPE_ICO))
self.SetBackgroundColour((224, 224, 224))
self.SetSize((800, 480))
self.Center()
wv = webview.WebView.New(self)
wv.LoadURL('https://cn.bing.com')
if __name__ == '__main__':
app = wx.App()
frame = MainFrame()
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
集成應(yīng)用
7.1. 集成Matplotlib
Matplotlib的后端子模塊backends幾乎支持所有的GUI庫,wxPyton當(dāng)然也不例外,backend_wxagg是專門為wxPyton生成canvas的類,只要傳一個(gè)matplotlib.Figure實(shí)例即可。剩下的就是水到渠成了。
import numpy as np
import matplotlib
from matplotlib.backends import backend_wxagg
from matplotlib.figure import Figure
import wx
matplotlib.use('TkAgg')
matplotlib.rcParams['font.sans-serif'] = ['FangSong']
matplotlib.rcParams['axes.unicode_minus'] = False
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
self.SetTitle('集成Matplotlib')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetBackgroundColour((224, 224, 224)) # 設(shè)置窗口背景色
self.SetSize((800, 600))
self._init_ui()
self.Center()
def _init_ui(self):
'''初始化界面'''
self.fig = Figure()
self.canvas = backend_wxagg.FigureCanvasWxAgg(self, -1, self.fig)
btn_1 = wx.Button(self, -1, '散點(diǎn)圖', size=(80, 30))
btn_2 = wx.Button(self, -1, '等值線圖', size=(80, 30))
btn_1.Bind(wx.EVT_BUTTON, self.on_scatter)
btn_2.Bind(wx.EVT_BUTTON, self.on_contour)
sizer_btn = wx.BoxSizer()
sizer_btn.Add(btn_1, 0, wx.RIGHT, 20)
sizer_btn.Add(btn_2, 0, wx.LEFT, 20)
sizer_max = wx.BoxSizer(wx.VERTICAL)
sizer_max.Add(self.canvas, 1, wx.EXPAND | wx.ALL, 10)
sizer_max.Add(sizer_btn, 0, wx. ALIGN_CENTER | wx.BOTTOM, 20)
self.SetSizer(sizer_max)
self.Layout()
def on_scatter(self, evt):
'''散點(diǎn)圖'''
x = np.random.randn(50) # 隨機(jī)生成50個(gè)符合標(biāo)準(zhǔn)正態(tài)分布的點(diǎn)(x坐標(biāo))
y = np.random.randn(50) # 隨機(jī)生成50個(gè)符合標(biāo)準(zhǔn)正態(tài)分布的點(diǎn)(y坐標(biāo))
color = 10 * np.random.rand(50) # 隨即數(shù),用于映射顏色
area = np.square(30*np.random.rand(50)) # 隨機(jī)數(shù)表示點(diǎn)的面積
self.fig.clear()
ax = self.fig.add_subplot(111)
ax.scatter(x, y, c=color, s=area, cmap='hsv', marker='o', edgecolor='r', alpha=0.5)
self.canvas.draw()
def on_contour(self, evt):
'''等值線圖'''
y, x = np.mgrid[-3:3:60j, -4:4:80j]
z = (1-y**5+x**5)*np.exp(-x**2-y**2)
self.fig.clear()
ax = self.fig.add_subplot(111)
ax.set_title('有填充的等值線圖')
c = ax.contourf(x, y, z, levels=8, cmap='jet')
self.fig.colorbar(c, ax=ax)
self.canvas.draw()
if __name__ == '__main__':
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
7.2. 集成OpenGL
wx.glcanvas.GLCanvas是wxPython為顯示OpenGL提供的類,顧名思義,可以將其理解為OpenGL的畫板。有了這個(gè)畫板,我們就可以使用OpenGL提供的各種工具在上面繪制各種三維模型了。下面的代碼僅是一個(gè)demo,并未構(gòu)建投影系統(tǒng)和視點(diǎn)系統(tǒng)。
import numpy as np
from OpenGL.GL import *
import wx
from wx import glcanvas
class MainFrame(wx.Frame):
'''從wx.Frame派生主窗口類'''
def __init__(self, parent):
'''構(gòu)造函數(shù)'''
wx.Frame.__init__(self, parent, style=wx.DEFAULT_FRAME_STYLE)
self.canvas = glcanvas.GLCanvas(self, style=glcanvas.WX_GL_RGBA|glcanvas.WX_GL_DOUBLEBUFFER|glcanvas.WX_GL_DEPTH_SIZE)
self.context = glcanvas.GLContext(self.canvas)
self.csize = self.canvas.GetClientSize()
self.SetTitle('集成OpenGL')
self.SetIcon(wx.Icon('res/wx.ico'))
self.SetBackgroundColour((224, 224, 224)) # 設(shè)置窗口背景色
self.SetSize((800, 600))
self.Center()
sizer_max = wx.BoxSizer()
sizer_max.Add(self.canvas, 1, wx.EXPAND|wx.ALL, 5)
self.SetSizer(sizer_max)
self.Layout()
self.Bind(wx.EVT_SIZE, self.on_resize)
self.canvas.SetCurrent(self.context)
glClearColor(0,0,0,1) # 設(shè)置畫布背景色
self.draw()
def on_resize(self, evt):
'''窗口改變事件函數(shù)'''
self.canvas.SetCurrent(self.context)
self.csize = self.GetClientSize()
self.draw()
evt.Skip()
def draw(self):
'''繪制'''
# 清除屏幕及深度緩存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# ---------------------------------------------------------------
glBegin(GL_LINES) # 開始繪制線段(坐標(biāo)軸)
# 以紅色繪制x軸
glColor4f(1.0, 0.0, 0.0, 1.0) # 設(shè)置當(dāng)前顏色為紅色不透明
glVertex3f(-0.8, 0.0, 0.0) # 設(shè)置x軸頂點(diǎn)(x軸負(fù)方向)
glVertex3f(0.8, 0.0, 0.0) # 設(shè)置x軸頂點(diǎn)(x軸正方向)
# 以綠色繪制y軸
glColor4f(0.0, 1.0, 0.0, 1.0) # 設(shè)置當(dāng)前顏色為綠色不透明
glVertex3f(0.0, -0.8, 0.0) # 設(shè)置y軸頂點(diǎn)(y軸負(fù)方向)
glVertex3f(0.0, 0.8, 0.0) # 設(shè)置y軸頂點(diǎn)(y軸正方向)
# 以藍(lán)色繪制z軸
glColor4f(0.0, 0.0, 1.0, 1.0) # 設(shè)置當(dāng)前顏色為藍(lán)色不透明
glVertex3f(0.0, 0.0, -0.8) # 設(shè)置z軸頂點(diǎn)(z軸負(fù)方向)
glVertex3f(0.0, 0.0, 0.8) # 設(shè)置z軸頂點(diǎn)(z軸正方向)
glEnd() # 結(jié)束繪制線段
# ---------------------------------------------------------------
glBegin(GL_TRIANGLES) # 開始繪制三角形(z軸負(fù)半?yún)^(qū))
glColor4f(1.0, 0.0, 0.0, 1.0) # 設(shè)置當(dāng)前顏色為紅色不透明
glVertex3f(-0.5, -0.366, -0.5) # 設(shè)置三角形頂點(diǎn)
glColor4f(0.0, 1.0, 0.0, 1.0) # 設(shè)置當(dāng)前顏色為綠色不透明
glVertex3f(0.5, -0.366, -0.5) # 設(shè)置三角形頂點(diǎn)
glColor4f(0.0, 0.0, 1.0, 1.0) # 設(shè)置當(dāng)前顏色為藍(lán)色不透明
glVertex3f(0.0, 0.5, -0.5) # 設(shè)置三角形頂點(diǎn)
glEnd() # 結(jié)束繪制三角形
# ---------------------------------------------------------------
glBegin(GL_TRIANGLES) # 開始繪制三角形(z軸正半?yún)^(qū))
glColor4f(1.0, 0.0, 0.0, 1.0) # 設(shè)置當(dāng)前顏色為紅色不透明
glVertex3f(-0.5, 0.5, 0.5) # 設(shè)置三角形頂點(diǎn)
glColor4f(0.0, 1.0, 0.0, 1.0) # 設(shè)置當(dāng)前顏色為綠色不透明
glVertex3f(0.5, 0.5, 0.5) # 設(shè)置三角形頂點(diǎn)
glColor4f(0.0, 0.0, 1.0, 1.0) # 設(shè)置當(dāng)前顏色為藍(lán)色不透明
glVertex3f(0.0, -0.366, 0.5) # 設(shè)置三角形頂點(diǎn)
glEnd() # 結(jié)束繪制三角形
# 交換緩沖區(qū)
self.canvas.SwapBuffers()
if __name__ == '__main__':
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
代碼運(yùn)行界面如下圖所示。
原文鏈接:https://blog.csdn.net/xufive/article/details/124548040
END
成就一億技術(shù)人