wxPython 入门 Python 如何帮助您工作
Michael Roberts
(
[email=michael@vivtek.com?subject=wxPython%20%E5%85%A5%E9%97%A8&cc=michael@vivtek.com]michael@vivtek.com[/email]
), 业主, Vivtek
您可以在几分钟内编写一段 Python脚本和让桌面拥有令人难以置信的相当漂亮的 GUI应用程序。这篇文章向您展示如何使用一 Python-著称的 GUI 库wxPython,来做到这一点的。向您的朋友和邻居介绍!
这篇文章是关于 wxPython,但 wxPython
实际是两件事物的组合体:Python 脚本语言和 GUI 功能的 wxWindows
库(关于 wxWindows 的介绍,请参阅
developerWorks上的
“细述 wxWindows”
)。wxWindows
库是为了最大可移植性的 C/C++ 库,而抽取 GUI 功能。所以 wxWindows
应用程序与生俱来地可以运行在 Windows、带 X、KDE 或 Gnome 的 UNIX
或者 wxWindows 已移植到的平台上(很不幸,还不包括 Macintosh)。当然
Python, 作为脚本引擎,具有很强的移植性(
可以 运行在
Macintosh 上,但如果您想要编写桌面 GUI 代码,它是不行的)。把
wxWindows 与 Python 脚本语言组合起来,意味着:wxPython
应用程序不仅快速和易于编写,而且可以在不作任何更改情况下,运行在
Windows 或 UNIX 环境下。
您可能想,“但是那也是我有 Java 的原因,Java
也是可移植的。”没错,如果您曾试过在 Windows 上安装 Java
应用程序,您就可能认识到完全不是这么回事。Java
虚拟机是
大的 ,它并不总是以您所想的方式工作,最糟糕的是,恕我直言,Java
窗口不是真正意义上的窗口,所以 Java
虚拟机与主机系统之间的交互总是有点力不从心。
另一方面,Python 占有相对小的空间。wxPython
库的窗口是真正实在的本地窗口,它可以做本地窗口能做的任何事情,使您的
wxPython 程序如同窗口的程序一样。wxPython
的全部家当可以打包成一个易于安装的软件包。也许我是一个固执的人,但我发现做同样的事情,wxPython
要比 Java 容易得多。
但是您可能没有听说过桌面上的
Python,它是服务器端编程社区的一员,作为脚本语言这一块的新生儿(特别是与应用程序服务器框架连接,如
Zope)。现在人们正赶上 Python 的热潮。Python
的好处在于,不象其它脚本语言,它从一开始就是面向对象的语言。所以您不会忽视
Java 由于喜爱 Python 而失去品尝 OO 好处。
世界上最小的 wxPython
程序,剖析!
听起来很酷,不是吗?让我们看一些代码,您将会明白我所说的。为了易于讨论,我在示例中插入了一些行标签。它们
不是代码中的一部分;这就是为什么它们以
蓝色斜体
表示。
清单 1. 一段很小的代码样本
[1]
import sys, os
[2]
from wxPython.wx import *
[3]
class main_window(wxFrame):
[4]
def __init__(self, parent, id, title):
[5]
wxFrame.__init__(self, parent, -1, title, size = (200, 100),
style=wxDEFAULT_FRAME_STYLE|wxNO_FULL_REPAINT_ON_RESIZE)
[6]
self.control = wxTextCtrl(self, -1, style=wxTE_MULTILINE)
[7]
self.Show(true)
[8]
class App(wxApp):
def OnInit(self):
frame =
main_window(None, -1, "wxPython: (A Demonstration)")
self.SetTopWindow(frame)
return true
[9]
app = App(0)
app.MainLoop()
我们一行行地看,这样容易明白该代码的工作原理。这就是为什么它是世界上最小的
wxPython
程序(我已经把所有无关的细节剔除了)。这段代码只能创建了一带有一个编辑字段的窗口。您可以在这个字段中编辑,但很明显没有办法保存。该应用程序看上去就象在
Windows 下一样(我在里面输入了一些东西):
回页首
让我们查看代码
第一行和第二行是很重要的,在后面会需要它们。事实上,在这个示例中,
不 需要
sys 和
os ,但由于几乎每个程序都要用到,所以先把它们放进这段代码,在后面会针对它们提一些问题。第二行比较有趣,导入
wxPython 的 wx 库。当然,wx 库(基本 wxPython
声明)包括基本类,如框架和应用程序。
/注意这些行的结尾没有分号。如果您编写过 Perl
程序,那您要花费一点功夫来熟悉 Python
的输入习惯。但等等,它对您来说是陌生的。
在第三行,定义了第一个 Python 类 --
main_window 。
main_window 类是在 wx
模块中定义的
wxFrame
类的派生类。正如您猜想的,任何窗口都是
wxFrame
类。
在第四行,定义了初始化方法,
main_window
需要定义的唯一方法(当然,其它的在 wxFrame 类中)。初始化方法取参数
parent 、
id 、
title
以及当然还有(作为该对象的引用的)
self 。
self
参数是所有 Python 方法的第一个参数。
到
现在为止,如果您编写过
C/C++,您一定奇怪为什么没有花括号。是很奇怪,Python
把缩进当作重要的句法。任何有子语句的语句以冒号结束,所有该行下的缩进行都属于以该冒号终止的语句。当缩进回复到原来的缩进时,这一块就完成了。更为奇
怪的是,这种安排实际上很好用,刚开始编程的程序员发现,这要比用花括号来说明结构要容易。幸运地是,这还意味着更少的击键次数,所以老程序员也能接受
它。
所以缩进的第 5、6 和 7 行属于在第 4 行中定义的方法。它们分别调用
wxFrame
父类的初始化例程(实际进行设置一个窗口的所有繁重工作),定义一个控件以适合新的主窗口,以及确保窗口是可见的。
wxPython
会重新调整父窗口的大小,除非专门告诉不要这样做。如果您曾编写过
任何
Microsoft Windows
代码,通过重新调整控件大小来匹配父窗口,那您会很快喜欢 wxPython。第
6 行的实际意义是完成一个如同 Notepad 一样的编辑器。这还不算什么。用
C/C++ 代码,则需要
更多 才能实现,不是吗?记住 --
我们在这做的不是固定大小的对话框,它是真正在窗口主体中和可调整大小的带编辑器的
Windows- 化应用程序。
让我们继续。第 8 行定义了由
wxApp 类派生的
App 对象。它指定了应用程序对象,当运行时,创建一个
main_window 对象,并将它设置为顶部窗口。
最后定义完类,创建应用程序对象,开始运行它的主循环。如果您做过任何
C/C++ Windows 编程,会认识到 MainLoop 方法是所有 Windows
程序的正常事件循环。这个启动代码的风格确实与脚本语言保持一致。记住,Python
解释器逐行读取代码,并边读边执行它。所以,一旦定义了类,我们只需在脚本中调用它。
这就是
它 。在这十五行代码中,实现了一个简单的文本编辑器,这段代码无需修改就可以在
Windows 或 UNIX
上运行。而且很容易添加更多特性,这太好了,在下一节将要讲述这些。
回页首
性能怎样?
您可能认为,“解释型语言,效率不高。对于较大的程序,执行起来会很慢。”在一定程度上,这是正确的。事实上,任何影响性能的代码通常用
C/C++ 重新实现,并链接到 Python 解释器,这是很容易做的。所以 Python
通常用作绑定功能性模块和 GUI
显示(或应用程序服务器功能,如果您需要该功能)的粘合剂。但作为粘合剂,Python
是非常有效的。您可以在很短的时间内用 Python
实现真正的程序,由于其面向对象和格式方面有限的创造机会,它们通常可使用好几个星期。
而且,如果您怀疑使用解释型语言将大程序结合在一起的想法,那您不妨考虑一下
Microsoft Word 的早期版本是如何实现的。至少最近的 Windows 的 Word
版本 6.0,其 Word Basic 函数事实上只是编译过的代码;而 GUI 是由
pcode 解释性型语言构建的。(MS Word 6.0 是用 Word Basic
编写的,很有效,这是一个很聪明的设计,其原因之一 --
它是最早用内置解释器的桌面程序。)
这里您所损失的是在性能方面(实际上,很少),但您可以很容易地通过简化实现和(更重要)简化定制来弥补。事实上,通过包含
Python
您已经自动包括了易于展现给您的用户的脚本语言,同时,对于编程的新手来说,该语言也证明了其易于学习。如果钻研它,可以用比您现在花费的要少的功夫创作出世界级的软件。因为,如果该语言能为
Microsoft 服务,那么,它也能为您服务!
回页首
更有趣的事:初步的项目组织器
无论如何,言归正传,这里向您展示一个程序,它事实上做一些值得做的事情。这个程序让您创建一个称之为项目的文本文件列表。您可以编辑和保存它们。更重要的是,您可以很容易地看到如何进一步增强基本组织器。我使用一个扩展版本为
CVS(标准开放源码版本控制系统)的前端。
这里是代码
。我们(仍然相当小)的应用程序从 15
行扩展到大约 300行,但它现在能实现许多事情。
回页首
程序中的趣事
清单 2
中我没有列出行号,因为很明显无论如何您都要月阅读该代码。我只是一般性地讲述一下这个程序是做什么以及它所用的
Python 与 wxPython 的功能是什么。有关Python
更详细说明,请您本地的书店找 Mark Lutz 写的 O'Reilly
这本书,或者阅读随 Python(Guido Rossum 著,Python
的实际作者)一起的文档。
第一个有趣的事是这个应用程序处理命令行。列表
sys.argv 是命令行,为了使用它您需要理解 Python 列表语法。该示例用的语法很基本,但这足够让您理解这个程序了。
接下来,为了在调试时易于使用,定义一个 MsgBox 函数。注意,用于函数声明和类方法声明的语法正好相同。唯一的区别是类方法需要带参数
self ,该参数包含了正在调用的对象的引用。(当然,您不一定要称它为 "self"。但如果不这样做,您会迷惑的。)
真正有趣的是在更为复杂的
__init__
方法。这里,我们没有构建象上例一样的简单的、缺乏控件的菜单,而是构建了一个菜单栏,并附加菜单事件到回调例程,以及构建窗口分割栏、树控件和编辑控
件。您可以撇去这些来看整个工作怎样。(如果您以前未做过 GUI 编程,往下看,您会觉得很困难。)Visual Basic
用一种幕后的方式可以做所有这些,但如果在文本编辑器仅仅打开表格文件,您仍然可以看到它。如果您曾用 C/C++ 做过 GUI
工作,那会觉得很熟悉。
一旦有构造了的窗口,接下来我们就可以来看程序的实际代码。首先,在方法
__init__
后有两个用于载入和保存项目文件的方法。在那可以看到 Python 如何用
open 等等来处理文件
I/O。注意,事实上,
关闭 文件是一种轻松的事 --
正如它所发生的那样,对于 Python
来讲,文件句柄仅仅是内存管理的对象,且该对象由计数器引用。当该引用变为无效时,Python
知道,并会清除它,这时文件会自动关闭。也有不能完全信任的情形(文件不能自动关闭),例如,您将再次打开已写的文件,并读它。这时,您要明确地关闭它。这个问题就是无用信息收集的问题(非
C 的人喜欢讨论的问题)。
载入/保存函数的另一个有趣的特性是他们会碰到由坏文件调用产生的例外(
IOError 例外)。我让代码来说明自己,但那是您如何做的,孩子们。
新代码剩下的问题是事件处理程序,用于处理在程序执行过程中所碰到的。我再次让程序自己来说明大部分。注意,使用一般的对话框(
wxMessageDialog 、
wxFileDialog 和
wxTextEntryDialog )来处理许多常规用户交互。这些调用与“常规”Windows
编程的相应用法有一点不同,这里我只给您一些线索:首先,对话框是一个由合理的调用创建的对象,它用
ShowModal()
显示,用完后并破坏它;其次,用户单击的按钮作为返回值从
ShowModal()
返回,用对话框附属的方法可得到其它值。例如,在文件对话框交互期间,用户选择的路径是作为
dlg.GetPath() 而获得。这与 Windows API
的工作方式相当不同。正是这样您知道的。
回页首
文档状况
让我说,wxWindows API
的文档处于……开发中。这个平台最大的弱点是缺乏好的文档,我正在积极地改变这种状况。下半年情况会有所好转。同时,Python
本身有一些好的书籍,其中 Python 的在线文档就不差。对于
wxWindows,C++ 的文档很好。有时,在 Python
框架中,如何使这些文档合理些,是相当神秘的。在那里,有一些具体的
Python 注释,但在很多情况下,您会发现必须要读随 wxPython
一起的演示代码,或者,必须向邮件列表中的专家询问您的问题。幸运地是,这些专家是“有迹可寻”的。
一旦通过了最初的学习曲线,您会觉得这要比在 Windows API
中做同样的任务要简单的多。相信我,这份材料是很好的。
参考资料
- 关于 wxWindows 的介绍,请参阅
developerWorks上的
“细述 wxWindows”
。
wxPython 主页
链接到该文档、下载和相关站点。该站点的镜像在
SourceForge
,它已经链接到
wxWindows 相关人员、wxPython 邮件列表等等。您也可以在这下载
wyPython。
- 在
Python 语言网站
上找到有关
Python 语言的下载、最新开发和文档。
Programming Python Mark Lutz 著 (O'Reilly, 1996),ISBN
1-56592-197-6,是我使用 Python
的参考书。它与其说是一本参考书还不如说是一份教程,它是我到目前为止发现的最好的一本书。
- 如果您想动动脑筋,可以阅读
Christian Tismer 的 Stackless
Python,它有效实现了协同例程、发生器、延续和微线程。如果承受不了这种脑力运动,请不要轻易尝试。
Vaults of
Parnassu 的 Python 资源 是最好的常规初学者园地,以获关于 Python
中基于文本的用户界面工具的信息。
Python.net
是用于
.NET 的 Python 的开发园地。
- 在三月份举行的
Ninth
International Python Conference。
- 阅读
JPython: the Felicitous union of Python and Java
,
Learning
Python(O'Reilly, 1999) 一书的摘录。
关于作者
Michael Roberts
从事编程已有十三年。这篇文章是他几个月前写的。您可以给他发 e-mail
[email=michael@vivtek.com?cc=michael@vivtek.com]michael@vivtek.com[/email]
或访问他的网站
www.vivtek.com
。 越多越好。
Listing 2. The rudimentary project organizer
#!/bin/python import sys, os from wxPython.wx import * from string import * # Process the command line. Not much to do; # just get the name of the project file if it's given. Simple. projfile = 'Unnamed' if len(sys.argv) > 1: projfile = sys.argv[1] def MsgBox (window, string): dlg=wxMessageDialog(window, string, 'wxProject', wxOK) dlg.ShowModal() dlg.Destroy() class main_window(wxFrame): def __init__(self, parent, id, title): wxFrame.__init__(self, parent, -1, title, size = (500, 500), style=wxDEFAULT_FRAME_STYLE|wxNO_FULL_REPAINT_ON_RESIZE) # ------------------------------------------------------------------------------------ # Set up menu bar for the program. # ------------------------------------------------------------------------------------ self.mainmenu = wxMenuBar() # Create menu bar. mainwindow = self menu=wxMenu() # Make a menu (will be the Project menu) exitID=wxNewId() # Make a new ID for a menu entry. menu.Append(exitID, '', 'Open project') # Name the ID by adding it to the menu. EVT_MENU(self, exitID, self.OnProjectOpen) # Create and assign a menu event. exitID=wxNewId() menu.Append(exitID, '', 'New project') EVT_MENU(self, exitID, self.OnProjectNew) exitID=wxNewId() menu.Append(exitID, 'E', 'Exit program') EVT_MENU(self, exitID, self.OnProjectExit) self.mainmenu.Append (menu, '') # Add the project menu to the menu bar. menu=wxMenu() # Make a menu (will be the File menu) exitID=wxNewId() menu.Append(exitID, '', 'Add file to project') EVT_MENU(self, exitID, self.OnFileAdd) exitID=wxNewId() menu.Append(exitID, '', 'Remove file from project') EVT_MENU(self, exitID, self.OnFileRemove) exitID=wxNewId() menu.Append(exitID, '', 'Open file for editing') EVT_MENU(self, exitID, self.OnFileOpen) exitID=wxNewId() menu.Append(exitID, '', 'Save file') EVT_MENU(self, exitID, self.OnFileSave) self.mainmenu.Append (menu, '') # Add the file menu to the menu bar. self.SetMenuBar (self.mainmenu) # Attach the menu bar to the window. # ------------------------------------------------------------------------------------ # Create the splitter window. # ------------------------------------------------------------------------------------ splitter = wxSplitterWindow (self, -1, style=wxNO_3D|wxSP_3D) splitter.SetMinimumPaneSize (1) # ------------------------------------------------------------------------------------ # Create the tree on the left. # ------------------------------------------------------------------------------------ tID = wxNewId() self.tree = wxTreeCtrl (splitter, tID, style=wxTR_HAS_BUTTONS | wxTR_EDIT_LABELS | wxTR_HAS_VARIABLE_ROW_HEIGHT) EVT_TREE_BEGIN_LABEL_EDIT(self.tree, tID, self.OnTreeLabelEdit) EVT_TREE_END_LABEL_EDIT(self.tree, tID, self.OnTreeLabelEditEnd) EVT_TREE_ITEM_ACTIVATED(self.tree, tID, self.OnTreeItemActivated) # ------------------------------------------------------------------------------------ # Create the editor on the right. # ------------------------------------------------------------------------------------ self.editor = wxTextCtrl(splitter, -1, style=wxTE_MULTILINE) self.editor.Enable (0) # ------------------------------------------------------------------------------------ # Install the tree and the editor. # ------------------------------------------------------------------------------------ splitter.SplitVertically (self.tree, self.editor) splitter.SetSashPosition (180, true) self.Show(true) # Some global state variables. self.projectdirty = false # ---------------------------------------------------------------------------------------- # Some nice little handlers. # ---------------------------------------------------------------------------------------- def project_open(self, project_file): try: input = open (project_file, 'r') self.tree.DeleteAllItems() self.project_file = project_file name = replace (input.readline(), "\n", "") self.SetTitle (name) self.root = self.tree.AddRoot(name) self.activeitem = self.root for line in input.readlines(): self.tree.AppendItem (self.root, replace(line, "\n", "")) input.close self.tree.Expand (self.root) self.editor.Clear() self.editor.Enable (false) self.projectdirty = false except IOError: pass def project_save(self): try: output = open (self.project_file, 'w+') output.write (self.tree.GetItemText (self.root) + "\n") count = self.tree.GetChildrenCount (self.root) iter = 0 child = '' for i in range(count): if i == 0: (child,iter) = self.tree.GetFirstChild(self.root,iter) else: (child,iter) = self.tree.GetNextChild(self.root,iter) output.write (self.tree.GetItemText(child) + "\n") output.close() self.projectdirty = false except IOError: dlg_m = wxMessageDialog (self, 'There was an error saving the project file.', 'Error!', wxOK) dlg_m.ShowModal() dlg_m.Destroy() # ---------------------------------------------------------------------------------------- # Event handlers from here on out. # ---------------------------------------------------------------------------------------- def OnProjectOpen(self, event): open_it = true if self.projectdirty: dlg=wxMessageDialog(self, 'The project has been changed. Save?', 'wxProject', wxYES_NO | wxCANCEL) result = dlg.ShowModal() if result == wxID_YES: self.project_save() if result == wxID_CANCEL: open_it = false dlg.Destroy() if open_it: dlg = wxFileDialog(self, "Choose a project to open", ".", "", "*.wxp", wxOPEN) if dlg.ShowModal() == wxID_OK: self.project_open(dlg.GetPath()) dlg.Destroy() def OnProjectNew(self, event): open_it = true if self.projectdirty: dlg=wxMessageDialog(self, 'The project has been changed. Save?', 'wxProject', wxYES_NO | wxCANCEL) result = dlg.ShowModal() if result == wxID_YES: self.project_save() if result == wxID_CANCEL: open_it = false dlg.Destroy() if open_it: dlg = wxTextEntryDialog (self, "Name for new project:", "New Project", "New project", wxOK | wxCANCEL) if dlg.ShowModal() == wxID_OK: newproj = dlg.GetValue() dlg.Destroy() dlg = wxFileDialog (self, "Place to store new project", ".", "", "*.wxp", wxSAVE) if dlg.ShowModal() == wxID_OK: try: proj = open (dlg.GetPath(), 'w') proj.write (newproj + "\n") proj.close() self.project_open (dlg.GetPath()) except IOError: dlg_m = wxMessageDialog (self, 'There was an error saving the new project file.', 'Error!', wxOK) dlg_m.ShowModal() dlg_m.Destroy() dlg.Destroy() def OnProjectExit(self, event): close = true if self.projectdirty: dlg=wxMessageDialog(self, 'The project has been changed. Save?', 'wxProject', wxYES_NO | wxCANCEL) result = dlg.ShowModal() if result == wxID_YES: self.project_save() if result == wxID_CANCEL: close = false dlg.Destroy() if close: self.Close() def OnFileAdd(self, event): dlg = wxFileDialog (self, "Choose a file to add", ".", "", "*.*", wxOPEN) if dlg.ShowModal() == wxID_OK: path = os.path.split(dlg.GetPath()) self.tree.AppendItem (self.root, path[1]) self.tree.Expand (self.root) self.project_save() def OnFileRemove(self, event): item = self.tree.GetSelection() if item != self.root: self.tree.Delete (item) self.project_save() def OnFileOpen(self, event): item = self.tree.GetSelection() def OnFileSave(self, event): if self.activeitem != self.root: self.editor.SaveFile (self.tree.GetItemText (self.activeitem)) def OnTreeLabelEdit(self, event): item=event.GetItem() if item != self.root: event.Veto() def OnTreeLabelEditEnd(self, event): self.projectdirty = true def OnTreeItemActivated(self, event): go_ahead = true if self.activeitem != self.root: if self.editor.IsModified(): dlg=wxMessageDialog(self, 'The edited file has changed. Save it?', 'wxProject', wxYES_NO | wxCANCEL) result = dlg.ShowModal() if result == wxID_YES: self.editor.SaveFile (self.tree.GetItemText (self.activeitem)) if result == wxID_CANCEL: go_ahead = false dlg.Destroy() if go_ahead: self.tree.SetItemBold (self.activeitem, 0) if go_ahead: item=event.GetItem() self.activeitem = item if item != self.root: self.tree.SetItemBold (item, 1) self.editor.Enable (1) self.editor.LoadFile (self.tree.GetItemText(item)) self.editor.SetInsertionPoint (0) self.editor.SetFocus() else: self.editor.Clear() self.editor.Enable (0) class App(wxApp): def OnInit(self): frame = main_window(None, -1, "wxProject - " + projfile) self.SetTopWindow(frame) if (projfile != 'Unnamed'): frame.project_open (projfile) return true app = App(0) app.MainLoop()