wxPython:python 首選的 GUI 庫
(點擊
上方公號
,快速關注我們)
作者:
許向武blog.csdn.net/xufive/article/details/82665460
概述
跨平台的GUI工具庫,較為有名的當屬GTK+、Qt 和 wxWidgets 了。GTK+是C實現的,由於C語言本身不支持OOP,因而GTK+上手相當困難,寫起來也較為複雜艱澀。Qt 和 wxWidgets 則是C++實現的,各自擁有龐大的用戶群體。雖然我喜歡wxWidgets,但還是儘可能客觀地搜集了關於Qt 和 wxWidgets 的對比評價。
關於LICENSE
Qt最初由芬蘭的TrollTech公司研發,現在屬於Nokia(沒看錯,就是曾經聞名遐邇的手機巨頭諾基亞),它的背後一直由商業公司支持,奉行的是雙 license 策略,一個是商業版,一個是免費版。這個策略嚴重限制了Qt的用戶群體。據說Nokia收購之後意識到了這個問題,自4.5版本之後採用了LGPL,開發人員可以發布基於免費Qt庫的商業軟體了。wxWidgets最開始是由愛丁堡(Edinburgh)大學的人工智慧應用學院開發的,在1992年開源,一直遵循LGPL。wxWidgets從一開始就是程序員的免費午餐。
關於兼容性
由於Qt使用的是非標準C++,與其它庫的兼容性會存在問題,在每個平台的圖形界面也並不完全是原生界面( Native GUI),只是透過 theme 去模擬系統上的標準 GUI,所以看起來很像,有些地方則會明顯看出破綻。 Qt的執行速度緩慢且過於龐大則是另一個問題。wxWidgets使用的是標準C++,與現有各類工具庫無縫連接,在不同平台上也是完全Native GUI,是真正的跨平台。
關於服務和支持
由於Nokia的接盤,Qt提供了一系列完整的文檔和RAD工具,並提供最為完整的平台支持,對於移動終端的支持最為完善。Qt庫也是所有的GUI工具庫中最為面向對象化的,同時也是最為穩定的。wxWidgets因為缺乏很好的商業化支持,開發文檔、資源相對較為匱乏。由於是偏重考慮MFC程序的跨平台遷移,wxWidgets面向對象封裝做得差強人意。
wxWidgets的主體是由C++構建的,但你並不是必需通過C++才能使用它。wxWidgets擁有許多其它語言的綁定(binding),比如 wxPerl,wxJava,wxBasic,wxJavaScript,wxRuby等等,wxPython 就是 Python語言的 wxWidgets 工具庫。
窗口程序的基本框架
不管是py2還是py3,python的世界裡安裝工作已經變得非常簡單了。如果工作在windows平台的話,我建議同時安裝pywin32模塊。pywin32允許你像VC一樣的使用python開發win32應用,更重要的是,我們可以用它直接操控win32程序,捕捉當前窗口、獲取焦點等。
pip install
wxpyhton
只用5行代碼,我們就可以創造一個窗口程序。然並卵,不過是又一次體現了python的犀利和簡潔罷了。
import
wx
app
=
wx
.
App
()
frame
=
wx
.
Frame
(
None
,
-
1
,
"Hello, World!"
)
frame
.
Show
(
True
)
app
.
MainLoop
()
下面是一個真正實用的窗口程序框架,任何一個窗口程序的開發都可以在這個基礎之上展開。請注意,代碼裡面用到了一個圖標文件,如果你要運行這段代碼,請自備icon文件。
#-*- coding: utf-8 -*-
import
wx
import
win32api
import
sys
,
os
APP_TITLE
=
u
"基本框架"
APP_ICON
=
"res/python.ico"
# 請更換成你的icon
class
mainFrame
(
wx
.
Frame
)
:
"""程序主窗口類,繼承自wx.Frame"""
def
__init__
(
self
)
:
"""構造函數"""
wx
.
Frame
.
__init__
(
self
,
None
,
-
1
,
APP_TITLE
,
style
=
wx
.
DEFAULT_FRAME_STYLE
^
wx
.
RESIZE_BORDER
)
# 默認style是下列項的組合:wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | wx.RESIZE_BORDER | wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.CLIP_CHILDREN
self
.
SetBackgroundColour
(
wx
.
Colour
(
224
,
224
,
224
))
self
.
SetSize
((
800
,
600
))
self
.
Center
()
# 以下代碼處理圖標
if
hasattr
(
sys
,
"frozen"
)
and
getattr
(
sys
,
"frozen"
)
==
"windows_exe"
:
exeName
=
win32api
.
GetModuleFileName
(
win32api
.
GetModuleHandle
(
None
))
icon
=
wx
.
Icon
(
exeName
,
wx
.
BITMAP_TYPE_ICO
)
else
:
icon
=
wx
.
Icon
(
APP_ICON
,
wx
.
BITMAP_TYPE_ICO
)
self
.
SetIcon
(
icon
)
# 以下可以添加各類控制項
pass
class
mainApp
(
wx
.
App
)
:
def
OnInit
(
self
)
:
self
.
SetAppName
(
APP_TITLE
)
self
.
Frame
=
mainFrame
()
self
.
Frame
.
Show
()
return
True
if
__name__
==
"__main__"
:
app
=
mainApp
(
redirect
=
True
,
filename
=
"debug.txt"
)
app
.
MainLoop
()
注意
倒數第2行代碼,是將調試信息定位到了debug.txt文件。如果mainApp()不使用任何參數,則調試信息輸出到控制台。
通過繼承wx.Frame,我們構造了mainFrame類,可以在mainFrame類的構造函數中任意添加面板、文本、圖片、按鈕等各種控制項了。
事件和事件驅動
不同於Qt的信號與槽機制,wx採用的是事件驅動型的編程機制。所謂事件,就是我們的程序在運行中發生的事兒。事件可以是低級的用戶動作,如滑鼠移動或按鍵按下,也可以是高級的用戶動作(定義在wxPython的窗口部件中的),如單擊按鈕或菜單選擇。事件可以產生自系統,如關機。你甚至可以創建你自己的對象去產生你自己的事件。事件會觸發相應的行為,即事件函數。程序員的工作就是定義事件函數,以及綁定事件和事件函數之間的關聯關係。
在wxPython中,我習慣把事件分為4類:
控制項事件:發生在控制項上的事件,比如按鈕被按下、輸入框內容改變等
滑鼠事件:滑鼠左右中鍵和滾輪動作,以及滑鼠移動等事件
鍵盤事件:用戶敲擊鍵盤產生的事件
系統事件:關閉窗口、改變窗口大小、重繪、定時器等事件
事實上,這個分類方法不夠嚴謹。比如,wx.frame作為一個控制項,關閉和改變大小也是控制項事件,不過這一類事件通常都由系統綁定了行為。基於此,我可以重新定義所謂的控制項事件,是指發生在控制項上的、系統並未預定義行為的事件。
下面這個例子演示了如何定義事件函數,以及綁定事件和事件函數之間的關聯關係。
兩個輸入框,一個明文居中,一個密寫右齊,但內容始終保持同步。輸入焦點不在輸入框的時候,敲擊鍵盤,界面顯示對應的鍵值。最上面的按鈕響應滑鼠左鍵的按下和彈起事件,中間的按鈕響應所有的滑鼠事件,下面的按鈕響應按鈕按下的事件。另外,程序還綁定了窗口關閉事件,閉關重新定義了關閉函數,增加了確認選擇。
菜單欄/工具欄/狀態欄
通常,一個完整的窗口程序一般都有菜單欄、工具欄和狀態欄。下面的代碼演示了如何創建菜單欄、工具欄和狀態欄,順便演示了類的靜態屬性的定義和用法。不過,說實話,wx的工具欄有點丑,幸好,wx還有一個 AUI 的工具欄比較漂亮,我會在後面的例子里演示它的用法。
另外,請注意,代碼裡面用到了4個16×16的工具按鈕,請自備4個圖片文件,保存路徑請查看代碼中的注釋。
動態布局
在「事件和事件驅動」的例子里,輸入框、按鈕等控制項的布局,使用的是絕對定位,我習慣叫做靜態布局。靜態布局非常直觀,但不能自動適應窗口的大小變化。更多的時候,我們使用被稱為布局管理器的 wx.Sizer 來實現動態布局。wx.Sizer 有很多種,我記不住,所以只喜歡用 wx.BoxSizer,最簡單的一種布局管理器。
和一般的控制項不同,布局管理器就像是一個魔法口袋:它是無形的,但可以裝進不限數量的任意種類的控制項——包括其他的布局管理器。當然,魔法口袋也不是萬能的,它有一個限制條件:裝到裡面的東西,要麼是水平排列的,要麼是垂直排列的,不能排成方陣。好在程序員可以不受限制地使用魔法口袋,當我們需要排成方陣時,可以先每一行使用一個魔法口袋,然後再把所有的行裝到一個魔法口袋中。
創建一個魔法口袋,裝進幾樣東西,然後在窗口中顯示的偽代碼是這樣的:
魔法口袋
=
wx
.
BoxSizer
()
# 默認是水平的,想要垂直放東西,需要加上 wx.VERTICAL 這個參數
魔法口袋
.
add
(
確認按鈕,
0
,
wx
.
ALL
,
0
)
# 裝入確認按鈕
魔法口袋
.
add
(
取消按鈕,
0
,
wx
.
ALL
,
0
)
# 裝入取消按鈕
窗口
.
SetSizer
(
魔法口袋)
# 把魔法口袋放到窗口上
窗口
.
Layout
()
# 窗口重新布局
魔法口袋的 add() 方法總共有4個參數:第1個參數很容易理解,就是要裝進口袋的物品;第2個參數和所有 add() 方法的第2個參數之和的比,表示裝進口袋的物品佔用空間的比例,0表示物品多大就佔多大地兒,不額外佔用空間;第3個參數相對複雜些,除了約定裝進口袋的物品在其佔用的空間裡面水平垂直方向的對齊方式外,還可以指定上下左右四個方向中的一個或多個方向的留白(padding);第4個參數就是留白像素數。
下面是一個完整的例子。
#-*- coding: utf-8 -*-
import
wx
import
win32api
import
sys
,
os
APP_TITLE
=
u
"動態布局"
APP_ICON
=
"res/python.ico"
class
mainFrame
(
wx
.
Frame
)
:
"""程序主窗口類,繼承自wx.Frame"""
def
__init__
(
self
,
parent
)
:
"""構造函數"""
wx
.
Frame
.
__init__
(
self
,
parent
,
-
1
,
APP_TITLE
)
self
.
SetBackgroundColour
(
wx
.
Colour
(
240
,
240
,
240
))
self
.
SetSize
((
800
,
600
))
self
.
Center
()
if
hasattr
(
sys
,
"frozen"
)
and
getattr
(
sys
,
"frozen"
)
==
"windows_exe"
:
exeName
=
win32api
.
GetModuleFileName
(
win32api
.
GetModuleHandle
(
None
))
icon
=
wx
.
Icon
(
exeName
,
wx
.
BITMAP_TYPE_ICO
)
else
:
icon
=
wx
.
Icon
(
APP_ICON
,
wx
.
BITMAP_TYPE_ICO
)
self
.
SetIcon
(
icon
)
preview
=
wx
.
Panel
(
self
,
-
1
,
style
=
wx
.
SUNKEN_BORDER
)
preview
.
SetBackgroundColour
(
wx
.
Colour
(
0
,
0
,
0
))
btn_capture
=
wx
.
Button
(
self
,
-
1
,
u
"拍照"
,
size
=
(
100
,
-
1
))
btn_up
=
wx
.
Button
(
self
,
-
1
,
u
"↑"
,
size
=
(
30
,
30
))
btn_down
=
wx
.
Button
(
self
,
-
1
,
u
"↓"
,
size
=
(
30
,
30
))
btn_left
=
wx
.
Button
(
self
,
-
1
,
u
"←"
,
size
=
(
30
,
30
))
btn_right
=
wx
.
Button
(
self
,
-
1
,
u
"→"
,
size
=
(
30
,
30
))
tc
=
wx
.
TextCtrl
(
self
,
-
1
,
""
,
style
=
wx
.
TE_MULTILINE
)
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
)
#sizer_arrow = wx.BoxSizer(wx.VERTICAL)
sizer_arrow
=
wx
.
StaticBoxSizer
(
wx
.
StaticBox
(
self
,
-
1
,
u
"方向鍵"
),
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
(
preview
,
1
,
wx
.
EXPAND
|
wx
.
LEFT
|
wx
.
TOP
|
wx
.
BOTTOM
,
5
)
sizer_max
.
Add
(
sizer_right
,
0
,
wx
.
EXPAND
|
wx
.
ALL
,
0
)
self
.
SetAutoLayout
(
True
)
self
.
SetSizer
(
sizer_max
)
self
.
Layout
()
class
mainApp
(
wx
.
App
)
:
def
OnInit
(
self
)
:
self
.
SetAppName
(
APP_TITLE
)
self
.
Frame
=
mainFrame
(
None
)
self
.
Frame
.
Show
()
return
True
if
__name__
==
"__main__"
:
app
=
mainApp
()
app
.
MainLoop
()
AUI布局
Advanced User Interface,簡稱AUI,是 wxPython 的子模塊,使用 AUI 可以方便地開發出美觀、易用的用戶界面。從2.8.9.2版本之後,wxPython 增加了一個高級通用部件庫 Advanced Generic Widgets,簡稱 AGW 庫。我發先 AGW 庫也提供了 AUI 模塊 wx.lib.agw.aui,而 wx.aui 也依然保留著。
AUI布局可以概括為以下四步:
創建一個布局管理器:mgr = aui.AuiManager()
告訴主窗口由mgr來管理界面:mgr.SetManagedWindow()
添加界面上的各個區域:mgr.AddPane()
更新界面顯示:mgr.Update()
下面的代碼演示了如何使用AUI布局管理器創建和管理窗口界面。
DC繪圖
DC 是 Device Context 的縮寫,字面意思是設備上下文——我一直不能正確理解DC這個中文名字,也找不到更合適的說法,所以,我堅持使用DC而不是設備上下文。DC可以在屏幕上繪製點線面,當然也可以繪製文本和圖像。事實上,在底層所有控制項都是以點陣圖形式繪製在屏幕上的,這意味著,我們一旦掌握了DC這個工具,就可以自己創造我們想要的控制項了。
DC有很多種,PaintDC,ClientDC,MemoryDC等。通常,我們可以使用 ClientDC 和 MemoryDC,PaintDC 是發生重繪事件(wx.EVT_PAINT)時系統使用的。使用 ClientDC 繪圖時,需要記錄繪製的每一步工作,不然,系統重繪時會令我們前功盡棄——這是使用DC最容易犯的錯誤。
定時器和線程
這個例子裡面設計了一個數字式鐘錶,一個秒錶,秒錶顯示精度十分之一毫秒。從代碼設計上來說沒有任何難度,實現的方法有很多種,可想要達到一個較好的顯示效果,卻不是一件容易的事情。請注意體會 wx.CallAfter() 的使用條件。
#-*- coding: utf-8 -*-
import
wx
import
win32api
import
sys
,
os
,
time
import
threading
APP_TITLE
=
u
"定時器和線程"
APP_ICON
=
"res/python.ico"
class
mainFrame
(
wx
.
Frame
)
:
"""程序主窗口類,繼承自wx.Frame"""
def
__init__
(
self
,
parent
)
:
"""構造函數"""
wx
.
Frame
.
__init__
(
self
,
parent
,
-
1
,
APP_TITLE
)
self
.
SetBackgroundColour
(
wx
.
Colour
(
224
,
224
,
224
))
self
.
SetSize
((
320
,
300
))
self
.
Center
()
if
hasattr
(
sys
,
"frozen"
)
and
getattr
(
sys
,
"frozen"
)
==
"windows_exe"
:
exeName
=
win32api
.
GetModuleFileName
(
win32api
.
GetModuleHandle
(
None
))
icon
=
wx
.
Icon
(
exeName
,
wx
.
BITMAP_TYPE_ICO
)
else
:
icon
=
wx
.
Icon
(
APP_ICON
,
wx
.
BITMAP_TYPE_ICO
)
self
.
SetIcon
(
icon
)
#font = wx.Font(24, wx.DECORATIVE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Comic Sans MS")
font
=
wx
.
Font
(
30
,
wx
.
DECORATIVE
,
wx
.
FONTSTYLE_NORMAL
,
wx
.
FONTWEIGHT_NORMAL
,
False
,
"Monaco"
)
self
.
clock
=
wx
.
StaticText
(
self
,
-
1
,
u
"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
,
u
"0:00:00.0"
,
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
.
OnTimer
,
self
.
timer
)
self
.
timer
.
Start
(
50
)
self
.
Bind
(
wx
.
EVT_KEY_DOWN
,
self
.
OnKeyDown
)
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
OnTimer
(
self
,
evt
)
:
"""定時器函數"""
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
OnKeyDown
(
self
,
evt
)
:
"""鍵盤事件函數"""
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.0"
)
def
StopWatchThread
(
self
)
:
"""線程函數"""
while
True
:
if
self
.
is_start
:
n
=
int
(
10
*
(
time
.
time
()
-
self
.
t_start
))
deci
=
n
%
10
ss
=
int
(
n
/
10
)
%
60
mm
=
int
(
n
/
600
)
%
60
hh
=
int
(
n
/
36000
)
wx
.
CallAfter
(
self
.
stopwatch
.
SetLabel
,
"%d:%02d:%02d.%d"
%
(
hh
,
mm
,
ss
,
deci
))
time
.
sleep
(
0.02
)
class
mainApp
(
wx
.
App
)
:
def
OnInit
(
self
)
:
self
.
SetAppName
(
APP_TITLE
)
self
.
Frame
=
mainFrame
(
None
)
self
.
Frame
.
Show
()
return
True
if
__name__
==
"__main__"
:
app
=
mainApp
()
app
.
MainLoop
()
後記
我使用 wxPython 長達十年。它給了我很多的幫助,它讓我覺得一切就該如此。這是我第一次寫關於 wxPython 的話題,寫作過程中,我心存感激。
【關於作者】
許向武:山東遠思信息科技有限公司CEO,網名牧碼人,齊國土著,太公之後。少小離家,獨闖江湖,後歸隱於華不注山。素以敲擊鍵盤為業,偶爾遊戲於各網路對局室,擅長送財送分,深為眾棋友所喜聞樂見。
【關於投稿】
如果大家有原創好文投稿,請直接給公號發送留言。
① 留言格式:
【投稿】+《 文章標題》+ 文章鏈接
② 示例:
【投稿】《不要自稱是程序員,我十多年的 IT 職場總結》:
http://blog.jobbole.com/94148/
③ 最後請附上您的個人簡介哈~
看完本文有收穫?請轉
發分享給更多人
關注「P
ython開發者」,提升Python技能
※面向對象:找個人閑暇時間一起看街頭的繁花似錦,享受生活
※面向對象:願無歲月可回首,且以深情共白頭
TAG:Python開發者 |