Ross Wan's World!

Python, Ajax, PHP and Linux.

Archive for 2008年2月11日

wxPython 入门指南

Posted by Ross Wan 于 2008/02/11

什么是 wxPython?

       WxPython 是 C++ GUI 库 wxWidgets(以前叫 wxWindows) 的 Python 封装,目前,wxWidgets 是一个相当稳定、高效、面向对象的 GUI 库,而且可以运行在Windows、Unix(GTK/Motif/Lesstif)和 Macintosh 平台上。因此,wxPython 是一个可以开发跨平台 GUI 应用程序的工具集(toolkits),跟 pyQT、pyGTK 或者 Tkinter 相像。在 windows 下,不像 Tkinter 或者 pyGTK 的应用程序,wxPython 拥有跟 microsoft 本地 GUI MFC 开发的应用程序窗口相似的外观,因为 wxPython 是构建在本地 GUI classes 上的。还有,wxPython 非常简单,易于学习和使用。

第一个 wxPython 应用程序:“Hello,World”

       这是一个小巧的 “Hello,World”应应用程序,下面是代码:

#!/usr/bin/env python
import wx
app = wx.PySimpleApp()
frame = wx.Frame(None, wx.ID_ANY, “Hello World”)
frame.Show(True)
app.MainLoop()

       在 GTK 环境下,你将会看到如下效果:

        在引入 wxPython GUI(import wx)后,我们实例化一个新的 wxPySimpleAPP 和 wxFrame 对象。在 wxPython 里,一个 frame 是一个带有标题栏,最大化、最少化和关闭按钮…等等的窗口。然后我们通过 “.Show()”使这个 Frame 显示出来。最后,我们开始这个应用程序的 MainLoop,监视和处理窗口的事件。

       注意: wxFrame 构造函数的形式

wx.Frame(Parent, Id, “Hello World”)

        wxPython 大多数的构造函数都有相同的形式:一个父对象作为第一个传入参数,接着一个 ID 作为第二个参数。正如示例那样,可以传入 None 和 wx.ID_ANY 作为默认的参数(表示这个 Frame 对象没有父对象和系统识别的 ID)。

创建 wxPython 应用程序

       在这个部分,我们将创建一个小型的编辑器。这有助于你了解 wxPython 的潜在能力和使用便捷。

概述

       当人们在讨论 GUI 时,不外乎就是窗口、菜单、鼠标、图标等等。我很可能认为一个 wxWindow 对象表示一个显示在屏幕上的窗口?!但这是错误的想法!在 wxPython 里, 一个 wxWindow 是屏幕上可以显示的任意组件,它是所有可视元件的父类。wxWindow 定义所有可视 GUI 组件的通用的行为,包括定位(positioning)、显示(showing)、取得焦点(giving focus)等等。wxFrame 对象才是代表屏幕上显示的窗口,它继承于 wxWindow,实现了窗口所有的行为。所以,要创建一个窗口,请创建一个 wxFrame 对象(或者一个它的子类的对象,例如 wxDialog)。

一个实例

增加编辑组件(edit component)

       第一步,就是向我们的 “hello world”程序添加一个新的编辑组件:

#!/usr/bin/env python
import wx
class MainWindow(wx.Frame):
    “”” We simply derive a new class of Frame. “””
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(200,100))
        self.control = wx.TextCtrl(self, 1, style=wx.TE_MULTILINE)
        self.Show(True)
app = wx.PySimpleApp()
frame=MainWindow(None, wx.ID_ANY, ‘Small editor’)
app.MainLoop()

       注意:我们继承了 wxFrame 类并且重写了它的 __init__ 方法,在方法里面,定义了一个 一个简单的文本编辑组件 wx.TextCtrl。

增加菜单

import wx
ID_ABOUT=101
ID_EXIT=110
class MainWindow(wx.Frame):
    def __init__(self,parent,id,title):
        wx.Frame.__init__(self,parent,wx.ID_ANY, title, size = (200,100))
        self.control = wx.TextCtrl(self, 1, style=wx.TE_MULTILINE)
        self.CreateStatusBar() # A Statusbar in the bottom of the window 在窗口的下端创建一个状态栏
        # Setting up the menu 创建菜单
        filemenu= wx.Menu()
        filemenu.Append(ID_ABOUT, “&About”,” Information about this program”)
        filemenu.AppendSeparator()
        filemenu.Append(ID_EXIT,“E&xit”,” Terminate the program”)
        # Creating the menubar 创建菜单栏
        menuBar = wx.MenuBar()
        menuBar.Append(filemenu,“&File”) # Adding the “filemenu” to the MenuBar 向菜单栏添加“文件”菜单
        self.SetMenuBar(menuBar)  # Adding the MenuBar to the Frame content 设置 Frame 窗口的菜单栏
        self.Show(True)
app = wx.PySimpleApp()
frame = MainWindow(None, -1, “Sample editor”)
app.MainLoop()

定义事件处理

       灵活的事件处理是 wxPython 的最大优点之一。 一个事件其实就是 wxPython 传递给应用程序的一个信息,表示有“某些事件”发生。通常,你要做的就是定义特定事件的处理方法。通过 EVT_* 来实现,如下:

EVT_MENU(self, ID_ABOUT, self.OnAbout)

       换句话说,现在 ID_ABOUT 菜单事件将绑定到 self.OnAbout 方法上。OnAbout 方法的一般定义如下:

def OnAbout(self, event):

       enent 是 wxEvent 的子类对象。当事件处理方法接收到一个事件时,它可以做两样事情:

  • 跳过这个事件
  • 捕获并处理这个事件

       现在再看看我们的程序:

import os
import wx
ID_ABOUT=101
ID_EXIT=110
class MainWindow(wx.Frame):
    def __init__(self,parent,id,title):
        wx.Frame.__init__(self,parent,wx.ID_ANY, title, size = (200,100))
        self.control = wx.TextCtrl(self, 1, style=wx.TE_MULTILINE)
        self.CreateStatusBar() # A Statusbar in the bottom of the window 在窗口的下端创建一个状态栏
        # Setting up the menu 创建菜单
        filemenu= wx.Menu()
        filemenu.Append(ID_ABOUT, “&About”,” Information about this program”)
        filemenu.AppendSeparator()
        filemenu.Append(ID_EXIT,“E&xit”,” Terminate the program”)
        # Creating the menubar 创建菜单栏
        menuBar = wx.MenuBar()
        menuBar.Append(filemenu,“&File”) # Adding the “filemenu” to the MenuBar 向菜单栏添加“文件”菜单
        self.SetMenuBar(menuBar)  # Adding the MenuBar to the Frame content 设置 Frame 窗口的菜单栏
        wx.EVT_MENU(self, ID_ABOUT, self.OnAbout) # attach the menu-event ID_ABOUT to the method self.OnAbout 绑定事件
        wx.EVT_MENU(self, ID_EXIT, self.OnExit)   # attach the menu-event ID_EXIT to the method self.OnExit 绑定事件
        self.Show(True)
    def OnAbout(self,e):
        d= wx.MessageDialog( self, ” A sample editor \n”
                            ” in wxPython”,“About Sample Editor”, wx.OK)
                            # Create a message dialog box 创建一个消息对话框
        d.ShowModal() # Shows it 显示消息对话框
        d.Destroy() # finally destroy it when finished 完成后销毁
     def OnExit(self,e):
        self.Close(True)  # Close the frame 关闭窗口
app = wx.PySimpleApp()
frame = MainWindow(None, -1, “Sample editor”)
app.MainLoop()

更进一步

       如果一个编辑器不能保存或者打开文档,那么这个编辑器是没有用的。这需要使用到通用的对话框。下面是 OnOpen 方法的实现代码:

def OnOpen(self,e):
        “”” Open a file 打开文件”””
        self.dirname = ”
        dlg = wx.FileDialog(self, “Choose a file”, self.dirname, “”, “*.*”, wx.OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            self.filename=dlg.GetFilename()
            self.dirname=dlg.GetDirectory()
            f=open(os.path.join(self.dirname,self.filename),’r’)
            self.control.SetValue(f.read())
            f.close()
        dlg.Destroy()

       在 OnOpen 方法里面:

  • 通过 wx.FileDialog 构造函数创建一个文件对话框。
  • 调用 ShowModal 方法,将会返回与用户所按按钮(OK 或者 Cancel 按钮)相对应的值。
  • 如果需要,我们可以取得所选文件的文件名和路径,并且将文件的内容输出在编辑器的文本域上。最后我们销毁这个对话框。

延伸

当然,目前这个编辑器跟像样的编辑器还有段距离,但是添加其它高级特性不会太艰难。你或许可以在上面的示例中得到灵感 :)

  • 拖放(Drag and Drop)
  • 多文档界面(MDI)
  • 标签式显示或者打开多个文件(Tab view/multiple files)
  • 查找或者替换对话框(Find/Replace dialog)
  • 打印对话框(Print dialog)
  • 宏命令支持(Macro-commands in Python using the eval function)


关于窗口(Windows)

       在这个部分,我们将讲述 wxPython 的 windows 以及 windows 的 contents。我们将会创建一个计算价格的应用程序的例子。

放置一个可视组件

       在一个 frame 里,我可以用一些 wxWindow 子类附加到 frame contents 上。以下是一些常用的组件:

  • wxMenuBar 菜单栏
  • wxStatusBar 状态栏
  • wxToolBar 工具栏
  • wxControl 的子类。是显示数据或者处理用户输入的组件,常见的 wxControl 包括 wxButton、wxStaticText、wxTextCtrl 和 wxComboBox。
  • wxPanel 可以容纳多种 wxControl 对象。可以按 Tab 键跳转 wxPanel 中的控件的焦点。

       所有的可视组件(wxWindow 对象和子对象)都能容纳子组件(sub-elements)。例如,wxFrame 可以容纳 wxPanel 对象,而 wxPanel 又可以容纳 wxButton、wxStaticText 和 wxTextCtrl 等对象。下面是一个完整的层次图:

       注意,这仅仅是描述某些可视组件的关联,而并不是他们放置在 frame 上的方式。要放置组件在 frame 上,有几个可选的方式:

  1. 通过精确的像素坐标,手工定位每个组件的在父窗口中的位置。但因为会遇到诸如在不同平台字体大小不同等问题,这个方式是不被推荐的。
  2. 使用 wxLayoutConstraints ,但十分复杂。
  3. 通过使用像 Delphi  方式的  LayoutAnchors,可以降低使用  wxLayoutConstraints 的难度。
  4. 使用 wxSizer 的子类。

       下面将集中讲解 Sizers。

Sizers

       一个 sizer(wxSizer 的子类)可以处理组件在窗口(window 或者 frame)中的可视布局:

  • 计算每个组件的合适大小。
  • 基于特定的准则定位组件的位置。
  • 当 frame 改变大小时,可以动态地改变组件的大小或者位置。

       常用的 sizers :

  • wxBoxSizer,顺序地安排组件的放置,可以垂直对齐或者水平对齐(horizontally or vertically)。
  • wxGridSizer,将组件放置在一个 Grid 结构上面。
  • wxFlexGridSizer,类似于 wxGridSizer,不同之处是,它提供更多的灵活性。

       可以通过 sizer.Add(window,options…) 或者 size(AddMany…) 来添加组件到 sizers。sizers 也可以嵌套。如下图:

       注意,上面也可以使用 wxGridSizer 实现,将6个 wxButton 放置在一个2行3列的 grid 上。

       在下面的例子里,我们利用2个嵌套的 sizers,外面的 sizer 是 vertical 布局,嵌套在里面的 sizer 是 horizontal 布局:

import os
import wx
ID_ABOUT=101
ID_OPEN=102
ID_EXIT=110
ID_EXIT=200
class MainWindow(wx.Frame):
    def __init__(self,parent,id,title):
        wx.Frame.__init__(self,parent,wx.ID_ANY, title, size = (200,100))
        self.control = wx.TextCtrl(self, 1, style=wx.TE_MULTILINE)
        self.CreateStatusBar() # A Statusbar in the bottom of the window 在窗口的下端创建一个状态栏
        # Setting up the menu 创建菜单
        filemenu= wx.Menu()
        filemenu.Append(ID_OPEN, “&Open”,” Open a file to edit”)
        filemenu.AppendSeparator()
        filemenu.Append(ID_ABOUT, “&About”,” Information about this program”)
        filemenu.AppendSeparator()
        filemenu.Append(ID_EXIT,“E&xit”,” Terminate the program”)
        # Creating the menubar 创建菜单栏
        menuBar = wx.MenuBar()
        menuBar.Append(filemenu,“&File”) # Adding the “filemenu” to the MenuBar 向菜单栏添加“文件”菜单
        self.SetMenuBar(menuBar)  # Adding the MenuBar to the Frame content 设置 Frame 窗口的菜单栏
        wx.EVT_MENU(self, ID_ABOUT, self.OnAbout) # attach the menu-event ID_ABOUT to the method self.OnAbout 绑定事件
        wx.EVT_MENU(self, ID_EXIT, self.OnExit)   # attach the menu-event ID_EXIT to the method self.OnExit 绑定事件
        wx.EVT_MENU(self, ID_OPEN, self.OnOpen)
        self.sizer2 = wx.BoxSizer(wx.HORIZONTAL)
        self.buttons=[]
        for i in range(0,6):
            self.buttons.append(wx.Button(self, ID_BUTTON1+i, “Button &”+`i`))
            self.sizer2.Add(self.buttons[i],1,wx.EXPAND)
        # Use some sizers to see layout options
        self.sizer=wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.control,1,wx.EXPAND)
        self.sizer.Add(self.sizer2,0,wx.EXPAND)
        #Layout sizers
        self.SetSizer(self.sizer)
        self.SetAutoLayout(1)
        self.sizer.Fit(self)
        self.Show(1)
    def OnAbout(self,e):
        d= wx.MessageDialog( self, ” A sample editor \n”
                            ” in wxPython”,“About Sample Editor”, wx.OK)
                            # Create a message dialog box 创建一个消息对话框
        d.ShowModal() # Shows it 显示消息对话框
        d.Destroy() # finally destroy it when finished 完成后销毁
     def OnExit(self,e):
        self.Close(True)  # Close the frame 关闭窗口
     def OnOpen(self,e):
        “”” Open a file 打开文件”””
        dlg = wx.FileDialog(self, “Choose a file”, self.dirname, “”, “*.*”, wx.OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            self.filename=dlg.GetFilename()
            self.dirname=dlg.GetDirectory()
            f=open(os.path.join(self.dirname, self.filename),‘r’)
            self.control.SetValue(f.read())
            f.close()
        dlg.Destroy()
app = wx.PySimpleApp()
frame = MainWindow(None, -1, “Sample editor”)
app.MainLoop()

       sizer.Add 方法有3个参数:第1个参数指定添加进 sizer 的控件。第2个参数指定一个系数,相对于其它控件的大小比率,0  表示当前控件或者 sizer 不会变大。第3个参数通常是 wxGROW(或者 wxEXPAND),表示当前控件会根据需要被重设大小,如果设置为 wxSHAPED,sizer 里的控件会保持一致的 aspect ratio。

       如果第2个参数被设置为0,表示控件不会被重设大小,这时,第3个参数表示对齐方式,居中、垂直或者水平对齐 可以用 wxALIGN_CENTER_HORIZONTAL, wxALIGN_CENTER_VERTICAL 和 wxALIGN_CENTER。

       也可以选择 wxALIGN_LEFT,wxALIGN_TOP,wxALIGN_RIGHT,and wxALIGN_BOTTOM 的组合值。例如,默认的对齐方式是:wxALIGN_LEFT | wxALIGN_TOP。

       sizer  和 交窗口(parent window)的区别:sizer 是窗口的布局方式,而并不代表窗口本身!在上例中,6个按钮是被创建到父窗口上(frame 或者 window),而不是 sizer 上。如果你尝试创建一个可视的组件到 sizer 上的话(例如 wx.Button(sizer,…),程序将会崩溃!

       一旦你创建了可视的组件并添加到 sizers(或者嵌套的 sizers),你就可以让 frame 或者 window 重设 sizer:

window.SetSizer(sizer)
window.SetAutoLayout(true)
sizer.Fit(window)

       调用 .SetSizer() 设置 window(或者 frame)的 sizer。调用 .SetAutoLayout() 让 window 使用指定的 sizer 来布局里面的组件。最后,调用 .Fit() 让 sizer 计算所有元件的初始大小和位置。注意,你必须在 window 或者 frame 首次显示前对 sizer 进行设置(如果希望使用 sizers 对 content 进行布局的话)。

验证器(Validators)

       对于对话框或者输入表单,你可以设置一个验证器(wxValidator)来简化下面的数据操作:表单的数据加载,验证输入的数据或者从表单中提取数据。 wxValidator 也可以监听键盘或者输入域的其它事件。要使用一个验证器,你必须创建一个 wxValidator 的子类(wxTextValidator 或者 wxGenericalValidator 是 wxPython 的现成类)。然后调用 myInputField.SetValidator(myValidator) ,将 myValidator 与输入域关联。

       注意:你的 wxValidator 子类必须实现 wxValidator.clone() 方法。

一个实例

在 panel 上放置一个 label

import wx
class Form1(wx.Panel):
    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)
        self.quote = wx.StaticText(self, -1, “Your quote :”, wx.Point(20, 30), wx.Size(200, -1))
app = wx.PySimpleApp()
frame = wx.Frame(None, -1, ” Our first Control”)
Form1(frame,-1)
frame.Show(1)
app.MainLoop()

       注意下面这行代码:

        self.quote = wx.StaticText(self, -1, “Your quote :”, wx.Point(20, 30), wx.Size(200, -1))

       wx.Point 用作定位参数,wx.Size 参数是可选的。

增加更多控件

       下面介绍一些最常使用的控件:

  • wxButton 是最基本的控件。现在的代码来用绑定按钮的单击事件:

EVT_BUTTON(Control, Id, callback function)

  • wxTextCtrl 可以让用户输入一小行的文本,它可以触发两个事件:

EVT_TEXT(control, Id ,callback function )

        当文本发生改变时会触发。

EVT_CHAR(control,callback function )

       在文本域上按键时会触发。

  • wxComboBox 是一个组合框,类似于 wxTextCtrl,除了可能触发 wxTestCtrl 的事件外,wxComboBox 也有自己的事件:

EVT_COMBOBOX(control, id, callback function)

  • wxCheckBox 复选框。
  • wxRadioBox 单选框。

       下面是表单1(Form1)的代码:

import wx
class Form1(wx.Panel):
    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)
        self.quote = wx.StaticText(self, -1, “Your quote :”,wx.Point(20, 30))
        # A multiline TextCtrl – This is here to show how the events work in this program, don’t pay too much attention to it
        self.logger = wx.TextCtrl(self,5, “”,wx.Point(300,20), wx.Size(200,300),wx.TE_MULTILINE | wx.TE_READONLY)
        # A button
        self.button =wx.Button(self, 10, “Save”, wx.Point(200, 325))
        wx.EVT_BUTTON(self, 10, self.OnClick)
        # the edit control – one line version.
        self.lblname = wx.StaticText(self, -1, “Your name :”,wx.Point(20,60))
        self.editname = wx.TextCtrl(self, 20, “Enter here your name”, wx.Point(150, 60), wx.Size(140,-1))
        wx.EVT_TEXT(self, 20, self.EvtText)
         wx.EVT_CHAR(self.editname, self.EvtChar)
        # the combobox Control
        self.sampleList = [‘friends’, ‘advertising’, ‘web search’, ‘Yellow Pages’]
        self.lblhear = wx.StaticText(self,-1,“How did you hear from us ?”,wx.Point(20, 90))
        self.edithear=wx.ComboBox(self, 30, “”, wx.Point(150, 90), wx.Size(95, -1),
                   self.sampleList, wx.CB_DROPDOWN)
        wx.EVT_COMBOBOX(self, 30, self.EvtComboBox)
        wx.EVT_TEXT(self, 30, self.EvtText)
        # Checkbox
        self.insure = wx.CheckBox(self, 40, “Do you want Insured Shipment ?”,wx.Point(20,180))
        wx.EVT_CHECKBOX(self, 40,   self.EvtCheckBox)
        # Radio Boxes
        self.radioList = [‘blue’, ‘red’, ‘yellow’, ‘orange’, ‘green’, ‘purple’,
                      ‘navy blue’, ‘black’, ‘gray’]
        rb = wx.RadioBox(self, 50, “What color would you like ?”, wx.Point(20, 210), wx.DefaultSize,
                        self.radioList, 3, wx.RA_SPECIFY_COLS)
        wx.EVT_RADIOBOX(self, 50, self.EvtRadioBox)
    def EvtRadioBox(self, event):
        self.logger.AppendText(‘EvtRadioBox: %d\n’ % event.GetInt())
    def EvtComboBox(self, event):
        self.logger.AppendText(‘EvtComboBox: %s\n’ % event.GetString())
    def OnClick(self,event):
        self.logger.AppendText(” Click on object with Id %d\n” %event.GetId())
    def EvtText(self, event):
        self.logger.AppendText(‘EvtText: %s\n’ % event.GetString())
    def EvtChar(self, event):
        self.logger.AppendText(‘EvtChar: %d\n’ % event.GetKeyCode())
        event.Skip()
    def EvtCheckBox(self, event):
        self.logger.AppendText(‘EvtCheckBox: %d\n’ % event.Checked())
app = wx.PySimpleApp()
frame = wx.Frame(None, -1, ” Quote Control”)
Form1(frame,-1)
frame.Show(1)
app.MainLoop()

       现在,这个类(Form1)包括了很多控件,我们定义了一个 wxTextCtrl 控件来显示控件发送的各种事件。

wxNoteBook

       有时,可能因为表单太大而不能在一页完全显示,这时可以使用 wxNoteBook。wxNoteBook 可以允许用户在多页间跳转,只需要单击相应的标签即可。我们用 wxNoteBook 代替 Form1 放置于 Frame 上,然后调用 wxNoteBook 的 AddPage 方法,将 Form1 放置在 wxNoteBook 上:

app = wx.PySimpleApp()
frame = wx.Frame(None,-1,” Demo with Notebook“)
nb = wx.Notebook(frame,-1)
form1=Form1(nb, -1)
nb.AddPage(form1, “Absolute Positioning”)
frame.Show(1)
app.MainLoop()

使用 Sizers 来改进组件的布局

       使用绝对的定位通常不能满足布局的要求,因为当窗口的大小改变,组件的布局也会随之改变,导致窗口的外观变得丑陋。庆幸的是,wxPython 提供丰富的类来改善组件的布局。

  • wxBoxSizer 是最常用的简单布局对象。它可以使一组组件成行或者成列的排列,有需要的时候会对它们进行重新的排列(例如全局的大小发生变化)。
  • wxGridSizer 和 wxFlexGridSizer 是两个非常重要的工具,使组件按照表格进行布局。

参考资料:

Posted in Uncategorized | 1 Comment »