簡介: 由兩部分組成的此系列文章將探索如何使用 Python 建立指令碼,以使用 KVM 來管理虛擬機器。在本期中,您將學習如何添加一個 GUI 來擴充簡單的狀態和顯示工具。
本系列的
使用 Python 為 KVM 編寫指令碼,第 1 部分:libvirt 介紹了使用 libvirt
和 Python 編寫基於核心的虛擬機器 (KVM) 指令碼的基礎知識。本期使用了上一期介紹的概念構建一些工具 + 生產力應用程式,添加一個圖形化使用者介面 (GUI)。主要有兩種既具有 Python 綁定又可跨平台的 GUI 工具包。第一個是 Qt,它現在歸諾基亞所有;第二個是 wxPython。二者都具有大量支援者,許多開源項目都在使用它們。
出於個人偏好,我在本文重點介紹 wxPython。首先將簡短介紹 wxPython 和正確設定的基本知識。再提供一些簡短的樣本程式,然後再與 libvirt
整合。此方法應該提供了足夠的 wxPython 基礎知識,供您構建簡單的程式,以及擴充該程式來添加功能。希望您能掌握這些概念,並擴充它們以滿足您的具體需要。
wxPython 基礎知識
一個不錯的起點是從一些基本的定義入手。wxPython 庫實際上是基於 C++
的 wxWidgets 上的一個封裝器。在建立 GUI 的上下文中,一個組件 在本質上就是一個構建塊。組件分層結構的最頂層包含 5 個獨立的組件:
wx.Frame
、
wx.Dialog
、
wx.PopupWindow
、
wx.MDIParentFrame
和
wx.MDIChildFrame
。
這裡的大部分樣本都基於 wx.Frame
,因為它在本質上實現了一個單一模態的視窗。
在 wxPython 中,您可以按原樣執行個體化 Frame
類,或者繼承它以添加或增強功能。一定要理解組件在一個架構中的顯示方式,這樣您在知道如何正確放置它們。布局通過絕對位置或使用調整器來確定。調整器 是一個方便的工具,在使用者單擊並拖動一邊或一角來更改視窗大小時,它會調整組件大小。
wxPython 程式的最簡單形式必須有一些程式碼進行設定。一種典型的主要常式可能類似於
清單 1。
清單 1. 裝置 XML 定義
if __name__ == "__main__": app = wx.App(False)frame = MyFrame()frame.Show()app.MainLoop() |
每個 wxPython 應用程式是 wx.App()
的一個執行個體,必須如清單 1 所示進行執行個體化。當將 False
傳遞到
wx.App
時,它表明 “不要將 stdout 和 stderr 重新導向到一個視窗”。下一行通過執行個體化 MyFrame()
類來建立一個架構。接著顯示架構並將控制權轉交給
app.MainLoop()
。MyFrame()
類通常包含一個 __init__
函數,以使用您選擇的組件初始化架構。您還會在這裡將任何組件事件串連到他們的正確處理函數。
現在有必要提一下 wxPython 隨帶的一個方便的調試工具。此工具稱為組件檢查工具(參見
圖 1),僅需要兩行代碼即可使用。首先,您必須使用下述代碼匯入它:
接著,要使用時,您只需調用 Show()
函數:
wx.lib.inspectin.InspectionTool().Show() |
單擊菜單工具列上的 Events 表徵圖會在啟用事件時動態地顯示事件。如果您不確定特定組件支援哪些事件,這是一種在事件發生時查看事件的真正快捷的方式。當應用程式正在運行時,它還會讓您更好地瞭解幕後發生的情況。
圖 1. wxPython 組件檢查工具
回頁首
向命令列工具添加 GUI
本系列的
使用 Python 為 KVM 編寫指令碼,第 1 部分:libvirt 提供了一個簡單工具來顯示所有啟動並執行虛擬機器 (VM) 的狀態。使用 wxPython,可以輕鬆地將該工具更改為 GUI 工具。wx.ListCtrl
組件提供了您以列表形式顯示資訊所需的功能。要使用
wx.ListCtrl
組件,您必須使用以下文法將它添加到您的架構中:
self.list=wx.ListCtrl(frame,id,style=wx.LC_REPORT|wx.SUNKEN_BORDER) |
您可以從多種不同樣式中選擇,包括前面使用的 wx.LC_REPORT
和 wx.SUNKEN_BORDER
選項。第一個選項將
wx.ListCtrl
設定為報告模式,這是四種可用模式之一。其他選項包括表徵圖、小表徵圖和列表。要添加 wx.SUNKEN_BORDER
這樣的樣式,您只需使用豎杠 (|
)。一些樣式是相互排斥的,比如不同的邊框樣式,所以如果您有任何疑慮,請查閱 wxPython wiki(參見
參考資料)。
執行個體化 wx.ListCtrl
組件之後,您就可以開始向它新增內容了,比如欄位標題。InsertColumn
方法有兩個強制性參數和兩個選擇性參數。第一個是列索引,它從 0 開始,接下來是一個設定標題的字串。第三個用于格式化,應該類似於
LIST_FORMAT_CENTER
、_LEFT
或 _RIGHT
。最後,您可以傳入一個整數來設定固定寬度,或者使用
wx.LIST_AUTOSIZE
自動調整列。
現在您已配置了 wx.ListCtrl
組件,您可以使用 InsertStringItem
和
SetStringItem
方法向它填充資料了。wx.ListCtrl
組件中的每一個新行都必須使用 InsertStringItem
方法添加。兩個強制性參數指定在何處執行插入,包含表示在列表頂部插入的值 0 和要插入在該位置的字串。InsertStringItem
返回一個整數,表示插入字串的行數。您可以為列表調用
GetItemCount()
,使用傳回值供索引附加到底部,如
清單 2 所示。
清單 2. 命令列工具的 GUI 版本
import wximport libvirtconn=libvirt.open("qemu:///system")class MyApp(wx.App): def OnInit(self): frame = wx.Frame(None, -1, "KVM Info") id=wx.NewId() self.list=wx.ListCtrl(frame,id,style=wx.LC_REPORT|wx.SUNKEN_BORDER) self.list.Show(True) self.list.InsertColumn(0,"ID") self.list.InsertColumn(1,"Name") self.list.InsertColumn(2,"State") self.list.InsertColumn(3,"Max Mem") self.list.InsertColumn(4,"# of vCPUs") self.list.InsertColumn(5,"CPU Time (ns)") for i,id in enumerate(conn.listDomainsID()): dom = conn.lookupByID(id) infos = dom.info() pos = self.list.InsertStringItem(i,str(id)) self.list.SetStringItem(pos,1,dom.name()) self.list.SetStringItem(pos,2,str(infos[0])) self.list.SetStringItem(pos,3,str(infos[1])) self.list.SetStringItem(pos,4,str(infos[3])) self.list.SetStringItem(pos,5,str(infos[2])) frame.Show(True) self.SetTopWindow(frame) return Trueapp = MyApp(0)app.MainLoop() |
圖 2 顯示了這些工作的結果。
圖 2. GUI KVM 資訊工具
您可以改善這個表的外觀。一種明顯的改進可能是重新調整列。為此,可以將 width =
參數添加到 InsertColumn
調用中,或者使用一行代碼,比如:
self.ListCtrl.SetColumnWidth(column,wx.LIST_AUTOSIZE) |
您可以做的另一件事是添加一個調整器,以便控制項能根據父視窗進行調整。為此,可以在幾行代碼中使用一個 wxBoxSizer
。首先,建立調整器,然後向它添加您希望針對主視窗進行調整的組件。可能的代碼如下所示:
self.sizer = wx.BoxSizer(wx.VERTICAL)self.sizer.Add(self.list, proportion=1,flag=wx.EXPAND | wx.ALL, border=5)self.sizer.Add(self.button, flag=wx.EXPAND | wx.ALL, border=5)self.panel.SetSizerAndFit(self.sizer) |
最後對 self.panel.SetSizerAndFit()
的調用要求 wxPython 基於嵌入組件調整器的最小尺寸,設定窗格的初始大小。這有助於基於螢幕內容為您提供大小合理的啟動顯示畫面。
回頁首
基於使用者操作的控制流程
關於 wx.ListCtrl
組件的一個好處是,您可以檢測使用者何時單擊了組件的具體部分,並基於該資訊執行某種操作。此功能允許您基於使用者對欄位標題的單擊,按字母順序對列進行正向或反向排列。完成此任務的技術使用了一種回調機制。您必須提供一個函數,通過將組件與處理方法綁定在一起來處理您希望處理的每個操作。為此,使用
Bind
方法。
每個組件有一定數量關聯事件。還有與滑鼠等實體關聯的事件。滑鼠事件具有 EVT_LEFT_DOWN
、EVT_LEFT_UP
和
EVT_LEFT_DCLICK
這樣的名稱,以及與其他按鈕相同的命名規範。您可以通過附加到 EVT_MOUSE_EVENTS
類型來處理所有滑鼠事件。痛點在於在您感興趣的應用程式或視窗上下文中捕獲事件。
當控制項傳遞到事件處理函數時,它必須執行必要的步驟來處理該操作,然後將控制項返回之前所在的地方。這是一種事件驅動的編程模型,每個 GUI 都必須實現它來及時地處理使用者操作。許多現代 GUI 應用程式實現了多線程來避免讓使用者感覺程式沒有響應。本文後面將簡短介紹這一主題。
計時器代表著程式可能必須處理的另一種事件類型。例如,您可能希望以使用者定義的間隔執行定期監視功能。您將需要提供一個螢幕,使用者可在該螢幕上指定間隔,接著啟動一個計時器以在它到期時觸發一個事件。計時器到期會觸發一個事件,您可以使用該事件啟用一段代碼。再次依據使用者偏好,您可能需要設定或重新開始計時。可以輕鬆地使用此技術開發 VM 監視工具。
清單 3 提供了一個簡單的示範應用程式,它包含一個按鈕和靜態文本行。使用
wx.StaticText
是一種將字串輸出到視窗的輕鬆方式。它的理念是單擊該按鈕一次會啟動一個計時器並記錄開始時間,同時將標籤更改為
Stop。再次單擊該按鈕會填入結束時間文字框,將按鈕更改回 Start。
清單 3. 包含一個按鈕和靜態文本的簡單應用程式
import wxfrom time import gmtime, strftimeclass MyForm(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, wx.ID_ANY, "Buttons") self.panel = wx.Panel(self, wx.ID_ANY) self.button = wx.Button(self.panel, id=wx.ID_ANY, label="Start") self.button.Bind(wx.EVT_BUTTON, self.onButton) def onButton(self, event): if self.button.GetLabel() == "Start": self.button.SetLabel("Stop") strtime = strftime("%Y-%m-%d %H:%M:%S", gmtime()) wx.StaticText(self, -1, 'Start Time = ' + strtime, (25, 75)) else: self.button.SetLabel("Start") stptime = strftime("%Y-%m-%d %H:%M:%S", gmtime()) wx.StaticText(self, -1, 'Stop Time = ' + stptime, (25, 100)) if __name__ == "__main__": app = wx.App(False) frame = MyForm() frame.Show() app.MainLoop() |
回頁首
增強監視 GUI
現在,您可以添加前面介紹的簡單監視 GUI 了。在擁有建立應用程式所需的一切內容之前,還需要理解 wxPython 的另一個方面。向 wx.ListCtrl
組件的第一行添加一個複選框,可實現基於複選框的狀態在多行上執行操作。為此,您可以使用 wxPython 所稱的
mixin。在本質上,mixin 是一個協助器類,它向父組件添加某種類型的功能。要添加複選框 mixin,只需使用以下代碼來執行個體化它:
listmix.CheckListCtrlMixin.__init__(self) |
也可以利用事件來添加單擊欄位標題來選擇或清除所有複選框的功能。這樣,只需幾次單擊即可執行啟動或停止所有 VM 等操作。您需要編寫一些事件處理函數,以與之前更改按鈕標籤相同的方式來響應合適的事件。以下是設定列單擊事件處理函數所需的程式碼:
self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColClick, self.list) |
wx.EVT_LIST_COL_CLICK
在單擊任何欄位標題時觸發。要確定單擊了哪一列,可以使用 event.GetColumn()
方法。以下是
OnColClick
事件的一個簡單的處理函數:
def OnColClick(self, event): print "column clicked %d\n" % event.GetColumn()event.Skip() |
如果需要將事件傳播給其他處理函數,event.Skip()
調用非常重要。儘管在此執行個體中可能不需要它,但當多個處理函數需要處理相同事件時,不使用它可能會出現問題。wxPython wiki 網站上對事件傳播進行了很好的討論,這比我在這裡的介紹詳細得多。
最後,向兩個按鈕處理函數添加代碼來啟動或停止所有選擇的 VM。只需幾行代碼,即可迭代 wx.ListCtrl
中的各行並擷取 VM ID,如
清單 4 所示。
清單 4. 啟動和停止選擇的 VM
#!/usr/bin/env pythonimport wximport wx.lib.mixins.listctrl as listmiximport libvirtconn=libvirt.open("qemu:///system")class CheckListCtrl(wx.ListCtrl, listmix.CheckListCtrlMixin, listmix.ListCtrlAutoWidthMixin): def __init__(self, *args, **kwargs): wx.ListCtrl.__init__(self, *args, **kwargs) listmix.CheckListCtrlMixin.__init__(self) listmix.ListCtrlAutoWidthMixin.__init__(self) self.setResizeColumn(2)class MainWindow(wx.Frame): def __init__(self, *args, **kwargs): wx.Frame.__init__(self, *args, **kwargs) self.panel = wx.Panel(self) self.list = CheckListCtrl(self.panel, style=wx.LC_REPORT) self.list.InsertColumn(0, "Check", width = 175) self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColClick, self.list) self.list.InsertColumn(1,"Max Mem", width = 100) self.list.InsertColumn(2,"# of vCPUs", width = 100) for i,id in enumerate(conn.listDefinedDomains()): dom = conn.lookupByName(id) infos = dom.info() pos = self.list.InsertStringItem(1,dom.name()) self.list.SetStringItem(pos,1,str(infos[1])) self.list.SetStringItem(pos,2,str(infos[3])) self.StrButton = wx.Button(self.panel, label="Start") self.Bind(wx.EVT_BUTTON, self.onStrButton, self.StrButton) self.sizer = wx.BoxSizer(wx.VERTICAL) self.sizer.Add(self.list, proportion=1, flag=wx.EXPAND | wx.ALL, border=5) self.sizer.Add(self.StrButton, flag=wx.EXPAND | wx.ALL, border=5) self.panel.SetSizerAndFit(self.sizer) self.Show() def onStrButton(self, event): if self.StrButton.GetLabel() == "Start": num = self.list.GetItemCount() for i in range(num): if self.list.IsChecked(i): dom = conn.lookupByName(self.list.GetItem(i, 0).Text) dom.create() print "%d started" % dom.ID() def OnColClick(self, event): item = self.list.GetColumn(0) if item is not None: if item.GetText() == "Check": item.SetText("Uncheck") self.list.SetColumn(0, item) num = self.list.GetItemCount() for i in range(num): self.list.CheckItem(i,True) else: item.SetText("Check") self.list.SetColumn(0, item) num = self.list.GetItemCount() for i in range(num): self.list.CheckItem(i,False) event.Skip() app = wx.App(False)win = MainWindow(None)app.MainLoop() |
在 KVM 中的 VM 狀態方面,這裡有兩個亮點需要指出:當使用 libvirt
中的 listDomainsID()
方法時,正在啟動並執行 VM 會顯示出來。要查看沒有啟動並執行機器,必須使用
listDefinedDomains()
。必須保持這兩部分獨立,才能知道可以啟動哪些 VM 和可以停止哪些 VM。
回頁首
結束語
本文主要介紹了使用 wxPython 構建 GUI 封裝器所需的步驟,該封裝器使用 libvirt
來管理 KVM。wxPython 庫功能豐富,提供了許多組件來支援構建具有專業型式的基於 GUI 的應用程式。本文僅介紹其中的一小部分功能,希望您能夠進一步探索。務必查閱更多
參考資料,以有助於應用程式正常運行。
參考資料
學習