wxPython:python 首選的 GUI 庫

概述

跨平臺的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的犀利和簡潔罷了。


importwx

app

= wx.App()

frame = wx.Frame(None, -1,"Hello, World!")

frame.Show

(True)

app.MainLoop()

wxPython:python 首選的 GUI 庫

下面是一個真正實用的窗口程序框架,任何一個窗口程序的開發都可以在這個基礎之上展開。請注意,代碼裡面用到了一個圖標文件,如果你要運行這段代碼,請自備icon文件。


#-*- coding: utf-8 -*-

importwx

importwin32api

importsys,

os

APP_TITLE = u'基本框架'

APP_ICON = 'res/python.ico'# 請更換成你的icon

classmainFrame(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()

# 以下代碼處理圖標

ifhasattr(sys,"frozen")andgetattr(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

classmainApp(wx.App)

:

defOnInit(self):

self.SetAppName(APP_TITLE)

self.Frame

= mainFrame()

self.Frame.Show()

returnTrue

if__name__ == "__main__"

:

app = mainApp(redirect=True,filename="debug.txt")

app.MainLoop()

  • 注意 倒數第2行代碼,是將調試信息定位到了debug.txt文件。如果mainApp()不使用任何參數,則調試信息輸出到控制檯。

wxPython:python 首選的 GUI 庫


通過繼承wx.Frame,我們構造了mainFrame類,可以在mainFrame類的構造函數中任意添加面板、文本、圖片、按鈕等各種控件了。

事件和事件驅動

不同於Qt的信號與槽機制,wx採用的是事件驅動型的編程機制。所謂事件,就是我們的程序在運行中發生的事兒。事件可以是低級的用戶動作,如鼠標移動或按鍵按下,也可以是高級的用戶動作(定義在wxPython的窗口部件中的),如單擊按鈕或菜單選擇。事件可以產生自系統,如關機。你甚至可以創建你自己的對象去產生你自己的事件。事件會觸發相應的行為,即事件函數。程序員的工作就是定義事件函數,以及綁定事件和事件函數之間的關聯關係。

在wxPython中,我習慣把事件分為4類:

  • 控件事件:發生在控件上的事件,比如按鈕被按下、輸入框內容改變等

  • 鼠標事件:鼠標左右中鍵和滾輪動作,以及鼠標移動等事件

  • 鍵盤事件:用戶敲擊鍵盤產生的事件

  • 系統事件:關閉窗口、改變窗口大小、重繪、定時器等事件


事實上,這個分類方法不夠嚴謹。比如,wx.frame作為一個控件,關閉和改變大小也是控件事件,不過這一類事件通常都由系統綁定了行為。基於此,我可以重新定義所謂的控件事件,是指發生在控件上的、系統並未預定義行為的事件。

下面這個例子演示瞭如何定義事件函數,以及綁定事件和事件函數之間的關聯關係。

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

兩個輸入框,一個明文居中,一個密寫右齊,但內容始終保持同步。輸入焦點不在輸入框的時候,敲擊鍵盤,界面顯示對應的鍵值。最上面的按鈕響應鼠標左鍵的按下和彈起事件,中間的按鈕響應所有的鼠標事件,下面的按鈕響應按鈕按下的事件。另外,程序還綁定了窗口關閉事件,閉關重新定義了關閉函數,增加了確認選擇。

菜單欄/工具欄/狀態欄

通常,一個完整的窗口程序一般都有菜單欄、工具欄和狀態欄。下面的代碼演示瞭如何創建菜單欄、工具欄和狀態欄,順便演示了類的靜態屬性的定義和用法。不過,說實話,wx的工具欄有點醜,幸好,wx還有一個 AUI 的工具欄比較漂亮,我會在後面的例子裡演示它的用法。

另外,請注意,代碼裡面用到了4個16×16的工具按鈕,請自備4個圖片文件,保存路徑請查看代碼中的註釋。

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

動態佈局

在“事件和事件驅動”的例子裡,輸入框、按鈕等控件的佈局,使用的是絕對定位,我習慣叫做靜態佈局。靜態佈局非常直觀,但不能自動適應窗口的大小變化。更多的時候,我們使用被稱為佈局管理器的 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 -*-

importwx

importwin32api

importsys,os

APP_TITLE = u'動態佈局'

APP_ICON =

'res/python.ico'

classmainFrame(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()

ifhasattr(sys,"frozen"

)andgetattr(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()

classmainApp(wx.App):

defOnInit(self):

self.

SetAppName(APP_TITLE)

self.Frame = mainFrame(None)

self.Frame.Show()

returnTrue

if__name__ == "__main__":

app = mainApp()

app.

MainLoop()


wxPython:python 首選的 GUI 庫

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佈局可以概括為以下四步:

  1. 創建一個佈局管理器:mgr = aui.AuiManager()

  2. 告訴主窗口由mgr來管理界面:mgr.SetManagedWindow()

  3. 添加界面上的各個區域:mgr.AddPane()

  4. 更新界面顯示:mgr.Update()


下面的代碼演示瞭如何使用AUI佈局管理器創建和管理窗口界面。

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫


wxPython:python 首選的 GUI 庫


DC繪圖


DC 是 Device Context 的縮寫,字面意思是設備上下文——我一直不能正確理解DC這個中文名字,也找不到更合適的說法,所以,我堅持使用DC而不是設備上下文。DC可以在屏幕上繪製點線面,當然也可以繪製文本和圖像。事實上,在底層所有控件都是以位圖形式繪製在屏幕上的,這意味著,我們一旦掌握了DC這個工具,就可以自己創造我們想要的控件了。

DC有很多種,PaintDC,ClientDC,MemoryDC等。通常,我們可以使用 ClientDC 和 MemoryDC,PaintDC 是發生重繪事件(wx.EVT_PAINT)時系統使用的。使用 ClientDC 繪圖時,需要記錄繪製的每一步工作,不然,系統重繪時會令我們前功盡棄——這是使用DC最容易犯的錯誤。

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

wxPython:python 首選的 GUI 庫

定時器和線程

這個例子裡面設計了一個數字式鐘錶,一個秒錶,秒錶顯示精度十分之一毫秒。從代碼設計上來說沒有任何難度,實現的方法有很多種,可想要達到一個較好的顯示效果,卻不是一件容易的事情。請注意體會 wx.CallAfter() 的使用條件。

#-*- coding: utf-8 -*-

importwx

importwin32api

importsys,os

,time

importthreading

APP_TITLE = u'定時器和線程'

APP_ICON = 'res/python.ico'

classmainFrame

(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()

ifhasattr(sys,"frozen")andgetattr(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()

defOnTimer(self,evt):

'''定時器函數'''

t = time.localtime()

ift.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

defOnKeyDown(self,evt):

'''鍵盤事件函數'''

ifevt.GetKeyCode() == wx.WXK_SPACE:

self.is_start = notself.is_start

self.t_start= time.time()

elifevt.GetKeyCode() == wx.WXK_ESCAPE:

self.is_start = False

self.stopwatch.SetLabel('0:00:00.0')

defStopWatchThread(self

):

'''線程函數'''

whileTrue:

ifself.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)

classmainApp

(wx.App):

defOnInit(self):

self.SetAppName(APP_TITLE)

self.Frame = mainFrame(None)

self.Frame.Show()

returnTrue

if__name__ == "__main__":

app = mainApp()

app.MainLoop()

wxPython:python 首選的 GUI 庫


後記

我使用 wxPython 長達十年。它給了我很多的幫助,它讓我覺得一切就該如此。這是我第一次寫關於 wxPython 的話題,寫作過程中,我心存感激。



分享到:


相關文章: