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的犀利和簡潔罷了。
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作為一個控制元件,關閉和改變大小也是控制元件事件,不過這一類事件通常都由系統綁定了行為。基於此,我可以重新定義所謂的控制元件事件,是指發生在控制元件上的、系統並未預定義行為的事件。
下面這個例子演示瞭如何定義事件函式,以及繫結事件和事件函式之間的關聯關係。
#-*- 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(224, 224, 224)) self.SetSize((520, 220)) 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) wx.StaticText(self, -1, u'第一行輸入框:', pos=(40, 50), size=(100, -1), style=wx.ALIGN_RIGHT) wx.StaticText(self, -1, u'第二行輸入框:', 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, u'滑鼠左鍵事件', pos=(350, 50), size=(100, 25)) btn_meb = wx.Button(self, -1, u'滑鼠所有事件', pos=(350, 80), size=(100, 25)) btn_close = wx.Button(self, -1, u'關閉視窗', pos=(350, 110), size=(100, 25)) # 控制元件事件 self.tc1.Bind(wx.EVT_TEXT, self.EvtText) self.tc2.Bind(wx.EVT_TEXT, self.EvtText) self.Bind(wx.EVT_BUTTON, self.OnClose, btn_close) # 滑鼠事件 btn_mea.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) btn_mea.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) btn_mea.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) btn_meb.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse) # 鍵盤事件 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) # 系統事件 self.Bind(wx.EVT_CLOSE, self.OnClose) self.Bind(wx.EVT_SIZE, self.On_size) #self.Bind(wx.EVT_PAINT, self.On_paint) #self.Bind(wx.EVT_ERASE_BACKGROUND, lambda event: None) def EvtText(self, evt): '''輸入框事件函式''' 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): '''改變視窗大小事件函式''' self.Refresh() evt.Skip() # 體會作用 def OnClose(self, evt): '''關閉視窗事件函式''' dlg = wx.MessageDialog(None, u'確定要關閉本視窗?', u'操作提示', wx.YES_NO | wx.ICON_QUESTION) if(dlg.ShowModal() == wx.ID_YES): self.Destroy() def OnLeftDown(self, evt): '''左鍵按下事件函式''' self.tip.SetLabel(u'左鍵按下') def OnLeftUp(self, evt): '''左鍵彈起事件函式''' self.tip.SetLabel(u'左鍵彈起') def OnMouseWheel(self, evt): '''滑鼠滾輪事件函式''' vector = evt.GetWheelRotation() self.tip.SetLabel(str(vector)) def OnMouse(self, evt): '''滑鼠事件函式''' self.tip.SetLabel(str(evt.EventType)) def OnKeyDown(self, evt): '''鍵盤事件函式''' key = evt.GetKeyCode() self.tip.SetLabel(str(key)) 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()
兩個輸入框,一個明文居中,一個密寫右齊,但內容始終保持同步。輸入焦點不在輸入框的時候,敲擊鍵盤,介面顯示對應的鍵值。最上面的按鈕響應滑鼠左鍵的按下和彈起事件,中間的按鈕響應所有的滑鼠事件,下面的按鈕響應按鈕按下的事件。另外,程式還綁定了視窗關閉事件,閉關重新定義了關閉函式,增加了確認選擇。
選單欄/工具欄/狀態列
通常,一個完整的視窗程式一般都有選單欄、工具欄和狀態列。下面的程式碼演示瞭如何建立選單欄、工具欄和狀態列,順便演示了類的靜態屬性的定義和用法。不過,說實話,wx的工具欄有點醜,幸好,wx還有一個 AUI 的工具欄比較漂亮,我會在後面的例子裡演示它的用法。
另外,請注意,程式碼裡面用到了4個16×16的工具按鈕,請自備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''' id_open = wx.NewId() id_save = wx.NewId() id_quit = wx.NewId() id_help = wx.NewId() id_about = wx.NewId() def __init__(self, parent): '''建構函式''' wx.Frame.__init__(self, parent, -1, APP_TITLE) 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) self.Maximize() self.SetWindowStyle(wx.DEFAULT_FRAME_STYLE) self._CreateMenuBar() # 選單欄 self._CreateToolBar() # 工具欄 self._CreateStatusBar() # 狀態列 def _CreateMenuBar(self): '''建立選單欄''' self.mb = wx.MenuBar() # 檔案選單 m = wx.Menu() m.Append(self.id_open, u"開啟檔案") m.Append(self.id_save, u"儲存檔案") m.AppendSeparator() m.Append(self.id_quit, u"退出系統") self.mb.Append(m, u"檔案") self.Bind(wx.EVT_MENU, self.OnOpen, id=self.id_open) self.Bind(wx.EVT_MENU, self.OnSave, id=self.id_save) self.Bind(wx.EVT_MENU, self.OnQuit, id=self.id_quit) # 幫助選單 m = wx.Menu() m.Append(self.id_help, u"幫助主題") m.Append(self.id_about, u"關於...") self.mb.Append(m, u"幫助") self.Bind(wx.EVT_MENU, self.OnHelp,id=self.id_help) self.Bind(wx.EVT_MENU, self.OnAbout,id=self.id_about) self.SetMenuBar(self.mb) def _CreateToolBar(self): '''建立工具欄''' bmp_open = wx.Bitmap('res/open_16.png', wx.BITMAP_TYPE_ANY) # 請自備按鈕圖片 bmp_save = wx.Bitmap('res/save_16.png', wx.BITMAP_TYPE_ANY) # 請自備按鈕圖片 bmp_help = wx.Bitmap('res/help_16.png', wx.BITMAP_TYPE_ANY) # 請自備按鈕圖片 bmp_about = wx.Bitmap('res/about_16.png', wx.BITMAP_TYPE_ANY) # 請自備按鈕圖片 self.tb = wx.ToolBar(self) self.tb.SetToolBitmapSize((16,16)) self.tb.AddLabelTool(self.id_open, u'開啟檔案', bmp_open, shortHelp=u'開啟', longHelp=u'開啟檔案') self.tb.AddLabelTool(self.id_save, u'儲存檔案', bmp_save, shortHelp=u'儲存', longHelp=u'儲存檔案') self.tb.AddSeparator() self.tb.AddLabelTool(self.id_help, u'幫助', bmp_help, shortHelp=u'幫助', longHelp=u'幫助') self.tb.AddLabelTool(self.id_about, u'關於', bmp_about, shortHelp=u'關於', longHelp=u'關於...') #self.Bind(wx.EVT_TOOL_RCLICKED, self.OnOpen, id=self.id_open) self.tb.Realize() def _CreateStatusBar(self): '''建立狀態列''' 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(u'狀態資訊0', 0) self.sb.SetStatusText(u'', 1) self.sb.SetStatusText(u'狀態資訊2', 2) def OnOpen(self, evt): '''開啟檔案''' self.sb.SetStatusText(u'開啟檔案', 1) def OnSave(self, evt): '''儲存檔案''' self.sb.SetStatusText(u'儲存檔案', 1) def OnQuit(self, evt): '''退出系統''' self.sb.SetStatusText(u'退出系統', 1) self.Destroy() def OnHelp(self, evt): '''幫助''' self.sb.SetStatusText(u'幫助', 1) def OnAbout(self, evt): '''關於''' self.sb.SetStatusText(u'關於', 1) 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()
動態佈局
在“事件和事件驅動”的例子裡,輸入框、按鈕等控制元件的佈局,使用的是絕對定位,我習慣叫做靜態佈局。靜態佈局非常直觀,但不能自動適應視窗的大小變化。更多的時候,我們使用被稱為佈局管理器的 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佈局管理器建立和管理視窗介面。
#-*- coding: utf-8 -*- import wx import win32api import sys, os import wx.lib.agw.aui as aui APP_TITLE = u'使用AUI佈局管理器' APP_ICON = 'res/python.ico' class mainFrame(wx.Frame): '''程式主視窗類,繼承自wx.Frame''' id_open = wx.NewId() id_save = wx.NewId() id_quit = wx.NewId() id_help = wx.NewId() id_about = wx.NewId() def __init__(self, parent): '''建構函式''' wx.Frame.__init__(self, parent, -1, APP_TITLE) 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) self.tb1 = self._CreateToolBar() self.tb2 = self._CreateToolBar() self.tbv = self._CreateToolBar('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, u'切換', pos=(30,200), size=(100, -1)) btn.Bind(wx.EVT_BUTTON, self.OnSwitch) text0 = wx.StaticText(p_center0, -1, u'我是第1頁', pos=(40, 100), size=(200, -1), style=wx.ALIGN_LEFT) text1 = wx.StaticText(p_center1, -1, u'我是第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(u"工具條").ToolbarPane().Top().Row(0).Position(0).Floatable(False) ) self._mgr.AddPane(self.tb2, aui.AuiPaneInfo().Name("ToolBar2").Caption(u"工具條").ToolbarPane().Top().Row(0).Position(1).Floatable(True) ) self._mgr.AddPane(self.tbv, aui.AuiPaneInfo().Name("ToolBarV").Caption(u"工具條").ToolbarPane().Right().Floatable(True) ) self._mgr.AddPane(p_left, aui.AuiPaneInfo().Name("LeftPanel").Left().Layer(1).MinSize((200,-1)).Caption(u"操作區").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(u"訊息區").CaptionVisible(False).Resizable(True) ) self._mgr.Update() def _CreateToolBar(self, d='H'): '''建立工具欄''' bmp_open = wx.Bitmap('res/open_16.png', wx.BITMAP_TYPE_ANY) bmp_save = wx.Bitmap('res/save_16.png', wx.BITMAP_TYPE_ANY) bmp_help = wx.Bitmap('res/help_16.png', wx.BITMAP_TYPE_ANY) bmp_about = wx.Bitmap('res/about_16.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, u'開啟', bmp_open, u'開啟檔案') tb.AddSimpleTool(self.id_save, u'儲存', bmp_save, u'儲存檔案') tb.AddSeparator() tb.AddSimpleTool(self.id_help, u'幫助', bmp_help, u'幫助') tb.AddSimpleTool(self.id_about, u'關於', bmp_about, u'關於') tb.Realize() return tb def OnSwitch(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() 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()
DC繪圖
DC 是 Device Context 的縮寫,字面意思是裝置上下文——我一直不能正確理解DC這個中文名字,也找不到更合適的說法,所以,我堅持使用DC而不是裝置上下文。DC可以在螢幕上繪製點線面,當然也可以繪製文字和影象。事實上,在底層所有控制元件都是以點陣圖形式繪製在螢幕上的,這意味著,我們一旦掌握了DC這個工具,就可以自己創造我們想要的控制元件了。
DC有很多種,PaintDC,ClientDC,MemoryDC等。通常,我們可以使用 ClientDC 和 MemoryDC,PaintDC 是發生重繪事件(wx.EVT_PAINT)時系統使用的。使用 ClientDC 繪圖時,需要記錄繪製的每一步工作,不然,系統重繪時會令我們前功盡棄——這是使用DC最容易犯的錯誤。
#-*- coding: utf-8 -*- import wx import win32api import sys, os APP_TITLE = u'使用DC繪圖' 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((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) self.palette = wx.Panel(self, -1, style=wx.SUNKEN_BORDER) self.palette.SetBackgroundColour(wx.Colour(0, 0, 0)) btn_base = wx.Button(self, -1, u'基本方法', 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.OnBase) self.palette.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse) self.palette.Bind(wx.EVT_PAINT, self.OnPaint) self.xy = None self.lines = list() self.img = wx.Bitmap('res/times.png', wx.BITMAP_TYPE_ANY) self.ReDraw() def OnMouse(self, evt): '''移動滑鼠畫線''' if evt.EventType == 10032: self.xy = (evt.x, evt.y) elif evt.EventType == 10033: self.xy = None elif evt.EventType == 10038: 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 OnBase(self, evt): '''DC基本方法演示''' img = wx.Bitmap('res/times.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, 50, 50) 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(u'霜重閒愁起', 100, 500) dc.DrawRotatedText(u'春深風也疾', 250, 500, 30) def OnPaint(self, evt): '''重繪事件函式''' dc = wx.PaintDC(self.palette) self.Paint(dc) def ReDraw(self): '''手工繪製''' dc = wx.ClientDC(self.palette) self.Paint(dc) def Paint(self, dc): '''繪圖''' w, h = self.palette.GetSize() dc.Clear() 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(self.img, 50, 50) 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(u'霜重閒愁起', 100, 500) dc.DrawRotatedText(u'春深風也疾', 250, 500, 30) 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]) 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()
定時器和執行緒
這個例子裡面設計了一個數字式鐘錶,一個秒錶,秒錶顯示精度十分之一毫秒。從程式碼設計上來說沒有任何難度,實現的方法有很多種,可想要達到一個較好的顯示效果,卻不是一件容易的事情。請注意體會 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 的話題,寫作過程中,我心存感激。