17、Python GUI开发:Tkinter、PythonWin与wxPython详解

Python GUI开发:Tkinter、PythonWin与wxPython详解

1. 引言

在Python中开发图形用户界面(GUI)有多种选择。本文将详细介绍三种在Windows平台上运行的GUI工具包:Tkinter、PythonWin和wxPython。这些工具包都为创建用户界面提供了丰富的功能,但每一个都值得用一本书来深入探讨,本文旨在让你对它们有一个基本的了解,以便在不同的场景中做出合适的选择。

2. Tkinter

2.1 概述

Tkinter是Python与Tk GUI工具包的接口,由Scriptics维护。由于其跨平台能力,它已成为Python事实上的标准GUI工具包,可在Windows、Macintosh和大多数Unix及Linux系统上运行。

2.2 术语

  • Tk :一个用C例程库实现的GUI工具包,用于管理和操作窗口,处理GUI事件和用户交互。
  • Tkinter :Python的Tk接口模块,提供了一系列Python类和方法,用于从Python内部访问Tk工具包。
  • Tcl :Tk和Tkinter用于与Tk工具包通信的(大部分隐藏的)语言。
  • Widget :用户界面元素,如文本框、组合框或顶级窗口,在Windows上通常称为控件或窗口。

2.3 优缺点

  • 优点
    • 简洁 :Python程序使用Tkinter可以非常简洁,部分原因是Python的强大功能,也得益于Tk为创建和布局小部件提供了合理的默认值。
    • 跨平台 :Tk在Windows、Mac和大多数Unix系统上提供小部件,且对特定平台的依赖很小。
    • 成熟 :自1990年首次发布以来,核心部分已经非常成熟和稳定。
    • 可扩展性 :Tk有许多扩展,并且在网络上经常有新的扩展发布。通过Tkinter可以方便地访问这些扩展。
  • 缺点
    • 速度 :Tkinter的速度可能会受到一些关注。大多数对Tkinter的调用会被格式化为Tcl命令(字符串),并由Tcl解释执行,理论上会导致速度减慢,但在实际应用中这种影响很少被察觉。
    • Tcl依赖 :Python纯粹主义者可能会对需要安装另一种脚本语言(Tcl)来执行GUI任务感到不满。虽然有人尝试直接使用Tk的C语言API来消除对Tcl的依赖,但尚未成功。
    • 缺乏现代小部件 :Tk提供的基本小部件集较少,缺乏一些现代的高级小部件,如分割窗口、标签窗口、进度条和树状结构小部件。不过,可以通过组合基本小部件来创建新的小部件。
    • 原生外观 :Windows上的Tkinter应用程序可能看起来不像Windows应用程序,但当前版本的Tkinter提供的界面已经可以被大多数人接受,未来版本有望与Windows应用程序几乎无差别。

2.4 运行GUI应用程序

Tkinter应用程序是普通的Python脚本,但在Windows上运行图形应用程序时需要注意一些问题。标准的Python.exe是一个控制台应用程序,使用它运行Tkinter程序会关联一个Windows控制台,可能会带来一些副作用。为了解决这个问题,Python提供了一个特殊的GUI版本Pythonw.exe。不过,.py文件默认与Python.exe关联,为了使用Pythonw.exe,可以给GUI Python应用程序一个.pyw扩展名。另外,由于Pythonw.exe没有控制台,Python打印的回溯信息通常无法看到,因此可以在开发时使用Python.exe,在最终运行时使用Pythonw.exe。

2.5 “Hello World”示例

from sys import exit
from Tkinter import *
root = Tk()
Button(root, text='Hello World!', command=exit).pack()
root.mainloop()

这个示例展示了Tkinter的基本使用方法。除了导入语句,主要有三行代码:创建一个顶级窗口,创建一个按钮并将其放置在窗口中,最后进入主事件处理循环。

2.6 核心小部件

Tkinter实现了一组相对较小的核心小部件,其他小部件或完整的应用程序可以基于这些核心小部件构建。以下是一些核心小部件及其描述:
| Widget Name | Description |
| — | — |
| Toplevel | 顶级小部件,没有主小部件,不支持几何管理方法。所有其他小部件直接或间接与顶级小部件关联。 |
| Frame | 用作其他子小部件的容器。 |
| Label | 显示文本或图像。 |
| Message | 显示具有自动换行和对齐功能的文本。 |
| Text | 显示具有高级格式、编辑和高度交互功能的文本。 |
| Canvas | 从显示列表中显示图形项,具有高度交互功能。 |
| Button | 标准的简单输入小部件,也称为控件小部件。 |
| Checkbox | 复选框。 |
| Entry | 输入框。 |
| Scale | 滑块。 |
| Radiobutton | 单选按钮。 |
| List box | 列表框。 |
| Scrollbar | 滚动条。 |
| Menu | 用于实现和响应菜单的小部件。 |
| Menubutton | 菜单按钮。 |

2.7 文本和画布小部件

2.7.1 文本小部件

文本小部件不仅可以显示和编辑文本,还支持嵌入图像和子窗口。其真正的强大之处在于对索引、标签和标记的支持:
- 索引 :提供了丰富的模型来描述文本控件中的位置,可以用行和列位置(相对或绝对)、像素位置、特殊系统索引名称等方式指定。
- 标签 :将一个名称与一个或多个文本区域关联起来,区域可以重叠,标签可以动态创建和销毁。与标签关联的文本可以有多种显示特性,如字体、颜色等。结合Tkinter的事件模型,可以轻松构建高度交互的应用程序。
- 标记 :表示文本中的一个位置(更准确地说,是两个字符之间的位置)。标记会随着周围文本的插入和删除而自然移动,适合实现书签或断点等概念。

2.7.2 画布小部件

画布小部件可以显示图形项,如线条、弧线、位图、图像、椭圆、多边形、矩形、文本字符串或任意Tkinter小部件。它也实现了一个强大的标记系统,允许将画布上的任何项与一个名称关联起来。

2.8 对话框和其他非核心小部件

许多有用的小部件实际上是由核心小部件构建而成的。最常见的例子是对话框小部件,Tkinter的最新版本提供了一些类似于Windows通用对话框的高级对话框小部件。以下是一些常见的对话框模块及其功能:
| Module Name | Description |
| — | — |
| tkMessageBox | 简单的消息框相关对话框,如“是/否”、“中止/重试/忽略”等。 |
| tkSimpleDialog | 包含用于构建自定义对话框的基类,还包括一些简单的输入对话框,如询问字符串、整数或浮点值。 |
| tkFileDialog | 功能与Windows通用文件对话框非常接近的对话框。 |
| tkColorChooser | 用于选择颜色的对话框。 |

2.9 小部件属性和方法

Tkinter为所有小部件提供了灵活而强大的属性集。几乎所有属性都可以在小部件创建时或创建并显示后进行设置。在指定小部件属性时,Tkinter大量使用Python关键字参数。例如:

label = Label(parent, background='white',
              text='Hello World!',
              relief=RAISED,
              borderwidth=3)

创建一个标签后,可以随时使用 configure 方法重新配置其属性:

label.configure(background='red')
label.configure(background='white')

以下是一些常见的小部件属性:
| Property | Description |
| — | — |
| height, width | 小部件的高度和宽度(像素)。 |
| background, foreground | 小部件的颜色(字符串)。可以通过名称(如“red”或“light blue”)或十六进制表示法(如“#ffffff”表示白色)指定颜色。 |
| relief | 对象的3D外观(RAISED、SUNKEN、RIDGE或GROOVE)或2D外观(FLAT或SOLID)。 |
| borderwidth | 边框的宽度(像素)。 |
| text wrap, justify | 小部件的窗口文本(即标题)和多个小部件的附加格式选项。 |
| font | 显示文本的字体。可以有多种格式,最常见的是包含字体名称、字号和样式的元组(如 ("Times", 10, "bold") )。 |
| command, variable | 控件小部件用于与应用程序通信的技术。 command 选项允许指定一个Python函数,当指定的操作发生时调用该函数; variable 选项可以指定一个 StringVar IntVar DoubleVar Booleanvar 类的实例,小部件的变化会立即反映在该对象上,反之亦然。 |

Tkinter还提供了许多方法,其中 bind 方法是Tkinter事件模型的核心。它允许将一个GUI事件绑定到一个Python函数,接受两个参数:要绑定的事件(以字符串形式指定)和事件触发时要调用的Python对象。Tkinter提供了丰富的事件集,涵盖了键盘和鼠标操作、窗口焦点或状态变化等20多种基本事件类型。

2.10 几何管理

Tkinter提供了一种在Windows GUI工具包中通常找不到的强大概念——几何管理。它用于在父小部件中布局子小部件,而传统的Windows环境通常要求指定每个控件的绝对位置。Tkinter小部件提供了三种几何管理方法: pack() grid() place()
- place() :最简单的机制,类似于大多数Windows用户习惯的方式,需要显式指定每个小部件的位置,可以是绝对坐标或相对坐标。
- grid() :自动将小部件排列成网格模式。
- pack() :最强大和最常用的方法。当小部件被打包时,它们会根据父小部件的大小和已放置的其他小部件自动定位。

这些几何管理功能允许定义不依赖于特定屏幕分辨率的用户界面,并且可以在窗口大小变化时自动调整控件的大小和布局。

2.11 Tkinter示例代码

本文包含了一个用Tkinter编写的Doubletalk浏览器示例( tkBrowser.py ),这是一个功能齐全的交易查看和编辑应用程序。还提供了 TkDemo.py 示例,用于演示所有Tkinter核心小部件的基本操作。

2.12 Tkinter总结

Tkinter非常适合小型、快速的GUI应用程序,并且由于它可以在比其他Python GUI工具包更多的平台上运行,因此在需要考虑可移植性时是一个不错的选择。要了解更多关于Tkinter的信息,可以参考标准Python文档、PythonWare和Fredrik Lundh提供的资源、Scriptics的Tcl和Tk文档以及Python megawidgets(PMW)包等。

3. PythonWin

3.1 概述

PythonWin是一个将Microsoft Foundation Classes(MFC)的大部分功能暴露给Python的框架。MFC是一个C++框架,为Windows GUI API提供了基于对象的模型,并提供了一些对应用程序有用的服务。

3.2 MFC简介

Microsoft Foundation Classes是一个用于在C++中开发完整应用程序的框架,主要提供两个功能:
- 面向对象的包装 :将许多Windows API函数的“句柄”参数封装在对象中,提供了 CWnd CDC 等类,这些类包含了相关的方法。
- 框架设施 :减少了创建一个完整的、独立的Windows应用程序所需的繁琐工作。

3.3 PythonWin对象模型

PythonWin由两部分组成:提供原始MFC功能的Python模块和使用这些模块的Python代码。要理解PythonWin的工作原理,需要了解MFC的关键概念。 win32ui 模块提供了对原始MFC类的访问,对于许多MFC对象,都有对应的 win32ui 对象。为了能够覆盖MFC对象层次结构中的默认方法, win32ui 模块提供了一种机制,将Python类实例对象“附加”到 win32ui 类型上。当MFC需要调用覆盖的方法时,会调用附加的Python对象上的方法。

3.4 开发PythonWin示例应用程序

以MFC的Scribble示例为基础,开发一个用Python编写的版本。

3.4.1 定义简单框架

定义三个对象: ScribbleTemplate ScribbleDocument ScribbleView

# scribble1.py
import win32ui
import win32con
import pywin.mfc.docview

class ScribbleTemplate(pywin.mfc.docview.DocTemplate):
    pass

class ScribbleDocument(pywin.mfc.docview.Document):
    def OnNewDocument(self):
        """Called whenever the document needs initializing.
        For most MDI applications, this is only called as the document
        is created.
        """
        self.strokes = []
        return 1

class ScribbleView(pywin.mfc.docview.ScrollView):
    def OnInitialUpdate(self):
        self.SetScrollSizes(win32con.MM_TEXT, (0, 0))

# Now we do the work to create the document template, and
# register it with the framework.
# For debugging purposes, we first attempt to remove the old template.
# This is not necessary once our app becomes stable!
try:
    win32ui.GetApp().RemoveDocTemplate(template)
except NameError:
    # haven't run this before - that's ok
    pass

# Now create the template object itself…
template = ScribbleTemplate(None, ScribbleDocument, None, ScribbleView)

# Set the doc strings for the template.
docs='\nPyScribble\nPython Scribble Document\nScribble documents (*.psd)\n.psd'
template.SetDocStrings(docs)

# Then register it with MFC.
win32ui.GetApp().AddDocTemplate(template)

这个示例代码注册了 ScribbleTemplate ,使得MFC能够创建新的文档。要在PythonWin中注册模板,可以按照以下步骤操作:
1. 启动PythonWin。
2. 使用“文件”菜单打开示例代码。
3. 从“文件”菜单中选择“导入”,在PythonWin环境中执行该模块。

3.4.2 增强文档模板

由于MFC和PythonWin支持多个文档模板,当MFC被要求打开一个文档文件时,它会依次询问每个注册的 DocumentTemplate 是否可以处理该文档类型。为了确保 ScribbleTemplate 能够正确处理Scribble文档,需要重写 MatchDocType() 方法:

class ScribbleTemplate(pywin.mfc.docview.DocTemplate):
    def MatchDocType(self, fileName, fileType):
        doc = self.FindOpenDocument(fileName)
        if doc: return doc
        ext = string.lower(os.path.splitext(fileName)[1])
        if ext =='.psd':
            return win32ui.CDocTemplate_Confidence_yesAttemptNative
        return win32ui.CDocTemplate_Confidence_noAttempt
3.4.3 增强文档

ScribbleDocument 对象负责处理文档数据,需要添加一些公共方法来处理笔画:

class ScribbleDocument(pywin.mfc.docview.Document):
    def AddStroke(self, start, end, fromView):
        self.strokes.append((start, end))
        self.SetModifiedFlag()
        self.UpdateAllViews( fromView, None )

    def GetStrokes(self):
        return self.strokes

    def OnOpenDocument(self, filename):
        file = open(filename, "rb")
        self.strokes = pickle.load(file)
        file.close()
        win32ui.AddToRecentFileList(filename)
        return 1

    def OnSaveDocument(self, filename):
        file = open(filename, "wb")
        pickle.dump(self.strokes, file)
        file.close()
        self.SetModifiedFlag(0)
        win32ui.AddToRecentFileList(filename)
        return 1
3.4.4 定义视图

视图对象是示例中最复杂的对象,负责与用户的所有交互。它需要收集用户绘制的笔画,并在窗口需要重绘时绘制整个笔画列表。

class ScribbleView(pywin.mfc.docview.ScrollView):
    def OnInitialUpdate(self):
        self.SetScrollSizes(win32con.MM_TEXT, (0, 0))
        self.HookMessage(self.OnLButtonDown,win32con.WM_LBUTTONDOWN)
        self.HookMessage(self.OnLButtonUp,win32con.WM_LBUTTONUP)
        self.HookMessage(self.OnMouseMove,win32con.WM_MOUSEMOVE)
        self.bDrawing = 0

    def OnLButtonDown(self, params):
        assert not self.bDrawing, "Button down message while still drawing"
        startPos = params[5]
        # Convert the startpos to Client coordinates.
        self.startPos = self.ScreenToClient(startPos)
        self.lastPos = self.startPos
        # Capture all future mouse movement.
        self.SetCapture()
        self.bDrawing = 1

    def OnLButtonUp(self, params):
        assert self.bDrawing, "Button up message, but not drawing!"
        endPos = params[5]
        endPos = self.ScreenToClient(endPos)
        self.ReleaseCapture()
        self.bDrawing = 0
        # And add the stroke to the document.
        self.GetDocument().AddStroke( self.startPos, endPos, self )

    def OnMouseMove(self, params):
        # If Im not drawing at the moment, I don't care
        if not self.bDrawing:
            return
        pos = params[5]
        dc = self.GetDC()
        # Setup for an inverting draw operation.
        dc.SetROP2(win32con.R2_NOT)
        # "undraw" the old line
        dc.MoveTo(self.startPos)
        dc.LineTo(self.lastPos)
        # Now draw the new position
        self.lastPos = self.ScreenToClient(pos)
        dc.MoveTo(self.startPos)
        dc.LineTo(self.lastPos)

    def OnDraw(self, dc):
        # All we need to is get the strokes, and paint them.
        doc = self.GetDocument()
        for startPos, endPos in doc.GetStrokes():
            dc.MoveTo(startPos)
            dc.LineTo(endPos)
3.4.5 创建应用程序对象

创建一个简单的应用程序对象,继承自 pywin.framework.app.CApp

# scribbleApp.py
from pywin.framework.app import CApp

class ScribbleApplication(CApp):
    def InitInstance(self):
        # All we need do is call the base class,
        # then import our document template.
        CApp.InitInstance(self)
        import scribble2

# And create our application object.
ScribbleApplication()

要运行这个应用程序,可以使用以下命令行:

C:\Scripts> start pythonwin /app scribbleApp.py

也可以使用资源编辑器(如Microsoft Visual C++)来避免使用命令行。

3.5 PythonWin和资源

MFC的框架架构很大程度上依赖于资源ID,这些整数用于在DLL或可执行文件中标识Windows资源。例如,定义 DocumentTemplate 时需要指定一个资源ID,MFC在创建文档时会使用该资源ID来加载菜单、图标和加速器等。这种架构有一些优点,如方便将应用程序的各个部分连接在一起、易于本地化等,但也存在一些缺点,如Python缺乏定义资源的技术,手动管理资源可能会很繁琐。PythonWin可以使用任意DLL中的资源,通过 win32ui.LoadLibrary() 加载DLL后,PythonWin可以定位和使用其中的资源。

3.6 PythonWin总结

对于大多数Windows上的Python用户来说,PythonWin可能只是一个有趣的IDE环境。但对于一些Windows开发者来说,它可以用于开发Windows应用程序。在比较本文介绍的三种GUI工具包时,PythonWin可能不太适合简单的小型GUI应用程序,但在特定情况下(如已有MFC投资或需要使用PythonWin提供的特定用户界面功能)可能是一个不错的选择。不过,PythonWin的文档不够完善,需要参考MFC相关的书籍和Microsoft的资料。

4. wxPython

4.1 概述

wxPython是一个Python扩展模块,封装了wxWindows C++类库,为Python提供了一个跨平台的GUI框架,在Windows平台上已经相当成熟。

4.2 wxWindows

wxWindows是一个免费的C++框架,旨在使跨平台编程变得简单。它支持Windows 3.1/95/98/NT、Unix(使用GTK/Motif/Lesstif),并且正在开发Mac版本。wxWindows提供了一组库,允许C++应用程序在不同类型的计算机上编译和运行,只需对源代码进行最小的更改。它不仅为GUI功能提供了通用的API,还提供了访问一些常用操作系统设施的功能,如复制或删除文件。在支持的平台上,使用本地版本的控件、通用对话框和其他窗口类型,对于其他平台,则使用wxWindows本身创建合适的替代方案。

4.3 wxWindows + Python = wxPython

wxPython将wxWindows库与Python语言绑定在一起,允许Python程序员创建wxWindows类的实例并调用这些类的方法。wxPython尽可能地镜像了wxWindows的类层次结构,大部分wxPython文档实际上是对C++文档的注释,描述了wxPython与C++版本的不同之处。

4.4 获取wxPython

可以从http://alldunn.com/wxPython/ 下载最新版本的wxPython,该网站提供了Win32系统的自安装程序,包括预构建的扩展模块、HTML帮助格式的文档和一组演示程序。如果想自己从源代码构建wxPython,还需要从http://www.wxwindows.org/ 下载wxWindows源代码。

4.5 简单示例

from wxPython.wx import *

class MyApp(wxApp):
    def OnInit(self):
        frame = wxFrame(NULL, -1, "Hello from wxPython")
        frame.Show(true)
        self.SetTopWindow(frame)
        return true

app = MyApp(0)
app.MainLoop()

这个示例展示了一个基本的wxPython应用程序的结构。首先导入整个wxPython库,然后创建一个继承自 wxApp 的类,并提供 OnInit 方法。在 OnInit 方法中创建一个框架窗口并显示它,最后调用 SetTopWindow 方法将该框架设置为应用程序的主框架。最后创建应用程序类的实例并调用 MainLoop 方法,该方法是应用程序的核心,负责处理和分发事件,直到最后一个窗口关闭。

4.6 wxPython中的事件处理

要使菜单等元素能够响应事件,可以使用wxPython提供的辅助函数将方法或独立函数绑定到事件上。wxPython还提供了 wxEvent 类和一系列派生类来包含事件的详细信息。例如,为了使菜单的“退出”选项能够正常工作,可以对之前的示例进行修改:

from wxPython.wx import *

ID_ABOUT = 101
ID_EXIT  = 102

class MyFrame(wxFrame):
    def __init__(self, parent, ID, title):
        wxFrame.__init__(self, parent, ID, title,
                         wxDefaultPosition, wxSize(200, 150))
        self.CreateStatusBar()
        self.SetStatusText("This is the statusbar")
        menu = wxMenu()
        menu.Append(ID_ABOUT, "&About",
                    "More information about this program")
        menu.AppendSeparator()
        menu.Append(ID_EXIT, "E&xit", "Terminate the program")
        menuBar = wxMenuBar()
        menuBar.Append(menu, "&File");
        self.SetMenuBar(menuBar)
        EVT_MENU(self, ID_ABOUT, self.OnAbout)
        EVT_MENU(self, ID_EXIT, self.TimeToQuit)

    def OnAbout(self, event):
        dlg = wxMessageDialog(
            self, "This sample program shows off\n"
                  "frames, menus, statusbars, and this\n"
                  "message dialog.",
            "About Me", wxOK | wxICON_INFORMATION)
        dlg.ShowModal()
        dlg.Destroy()

    def TimeToQuit(self, event):
        self.Close(true)

class MyApp(wxApp):
    def OnInit(self):
        frame = MyFrame(NULL, -1, "Hello from wxPython")
        frame.Show(true)
        self.SetTopWindow(frame)
        return true

app = MyApp(0)
app.MainLoop()

这里使用 EVT_MENU 函数将菜单选项的事件绑定到相应的方法上。以下是一些常见的wxPython事件函数及其描述:
| Event Function | Event Description |
| — | — |
| EVT_SIZE | 当窗口大小发生变化时发送,无论是用户交互还是程序控制。 |
| EVT_MOVE | 当窗口被移动时发送,无论是用户交互还是程序控制。 |
| EVT_CLOSE | 当框架被请求关闭时发送。除非关闭是强制的,否则可以通过调用 event.Veto(true) 取消关闭。 |
| EVT_PAINT | 当窗口的一部分需要重绘时发送。 |
| EVT_CHAR | 当窗口具有焦点时,每个非修饰键(如Shift键)的按键事件都会发送。 |
| EVT_IDLE | 当系统没有处理其他事件时定期发送。 |
| EVT_LEFT_DOWN | 左鼠标按钮被按下。 |
| EVT_LEFT_UP | 左鼠标按钮被释放。 |
| EVT_LEFT_DCLICK | 左鼠标按钮被双击。 |
| EVT_MOTION | 鼠标移动。 |
| EVT_SCROLL | 滚动条被操作。实际上是一组事件,如果需要可以单独捕获。 |
| EVT_BUTTON | 按钮被点击。 |
| EVT_MENU | 菜单项被选中。 |

4.7 构建Doubletalk浏览器

接下来将构建一个基于Doubletalk类库的小型应用程序,用于浏览和编辑交易记录。

4.7.1 MDI框架
class DoubleTalkBrowserApp(wxApp):
    def OnInit(self):
        frame = MainFrame(NULL)
        frame.Show(true)
        self.SetTopWindow(frame)
        return true

app = DoubleTalkBrowserApp(0)
app.MainLoop()

class MainFrame(wxMDIParentFrame):
    title = "Doubletalk Browser - wxPython Edition"
    def __init__(self, parent):
        wxMDIParentFrame.__init__(self, parent, -1, self.title)
        self.bookset = None
        self.views = []
        if wxPlatform == '__WXMSW__':
            self.icon = wxIcon('chart7.ico', wxBITMAP_TYPE_ICO)
            self.SetIcon(self.icon)
        # create a statusbar that shows the time and date on the right
        sb = self.CreateStatusBar(2)
        sb.SetStatusWidths([-1, 150])
        self.timer = wxPyTimer(self.Notify)
        self.timer.Start(1000)
        self.Notify()
        menu = self.MakeMenu(false)
        self.SetMenuBar(menu)
        menu.EnableTop(1, false)
        EVT_MENU(self, ID_OPEN,  self.OnMenuOpen)
        EVT_MENU(self, ID_CLOSE, self.OnMenuClose)
        EVT_MENU(self, ID_SAVE,  self.OnMenuSave)
        EVT_MENU(self, ID_SAVEAS,self.OnMenuSaveAs)
        EVT_MENU(self, ID_EXIT,  self.OnMenuExit)
        EVT_MENU(self, ID_ABOUT, self.OnMenuAbout)
        EVT_MENU(self, ID_ADD,   self.OnAddTrans)
        EVT_MENU(self, ID_JRNL,  self.OnViewJournal)
        EVT_MENU(self, ID_DTAIL, self.OnViewDetail)
        EVT_CLOSE(self, self.OnCloseWindow)

使用 wxMDIParentFrame 作为主框架的基类,可以自动实现多文档界面(MDI)的功能,而无需担心背后的具体实现。

4.7.2 图标

通过指定 .ico 文件的完整路径来创建图标,并使用 SetIcon 方法将图标关联到框架上。

4.7.3 定时器

使用 wxPyTimer 对象来更新状态栏中的日期和时间。 CreateStatusBar 方法可以创建指定数量的状态栏部分, SetStatusWidths 方法可以指定每个部分的宽度。

# Time-out handler
def Notify(self):
    t = time.localtime(time.time())
    st = time.strftime(" %d-%b-%Y %I:%M:%S", t)
    self.SetStatusText(st, 1)
4.7.4 主菜单

将菜单的创建逻辑封装在 MakeMenu 方法中,根据需要添加或删除菜单项。通过 EnableTop 方法可以禁用整个子菜单, Enable 方法可以启用或禁用单个菜单项。

def MakeMenu(self, withEdit):
    fmenu = wxMenu()
    fmenu.Append(ID_OPEN, "&Open BookSet", "Open a BookSet file")
    fmenu.Append(ID_CLOSE, "&Close BookSet",
                 "Close the current BookSet")
    fmenu.Append(ID_SAVE, "&Save", "Save the current BookSet")
    fmenu.Append(ID_SAVEAS,  "Save &As", "Save the current BookSet")
    fmenu.AppendSeparator()
    fmenu.Append(ID_EXIT, "E&xit",   "Terminate the program")
    dtmenu = wxMenu()
    dtmenu.Append(ID_ADD, "&Add Transaction",
                  "Add a new transaction")
    if withEdit:
        dtmenu.Append(ID_EDIT, "&Edit Transaction",
                      "Edit selected transaction in current view")
    dtmenu.Append(ID_JRNL, "&Journal view",
                  "Open or raise the journal view")
    dtmenu.Append(ID_DTAIL, "&Detail view",
                  "Open or raise the detail view")
    hmenu = wxMenu()
    hmenu.Append(ID_ABOUT, "&About",
                 "More information about this program")
    main = wxMenuBar()
    main.Append(fmenu, "&File")
    main.Append(dtmenu,"&Bookset")
    main.Append(hmenu, "&Help")
    return main
4.7.5 wxFileDialog

使用 wxFileDialog 来打开BookSet文件,这是一个与Windows通用文件打开对话框类似的类。

def OnMenuOpen(self, event):
    # This should be checking if another is already open,
    # but is left as an exercise for the reader…
    dlg = wxFileDialog(self)
    dlg.SetStyle(wxOPEN)
    dlg.SetWildcard("*.dtj")
    if dlg.ShowModal() == wxID_OK:
        self.path = dlg.GetPath()
        self.SetTitle(self.title + ' - ' + self.path)
        self.bookset = BookSet()
        self.bookset.load(self.path)
        self.GetMenuBar().EnableTop(1, true)
        win = JournalView(self, self.bookset, ID_EDIT)
        self.views.append((win, ID_JRNL))
    dlg.Destroy()
4.7.6 wxListCtrl

JournalView 类使用 wxListCtrl 来显示交易记录的摘要信息。在初始化时,插入列并设置事件处理程序。

class JournalView(wxMDIChildFrame):
    def __init__(self, parent, bookset, editID):
        wxMDIChildFrame.__init__(self, parent, -1, "")
        self.bookset = bookset
        self.parent = parent
        tID = wxNewId()
        self.lc = wxListCtrl(self, tID, wxDefaultPosition,
                             wxDefaultSize, wxLC_REPORT)
        ## Forces a resize event to get around a minor bug…
        self.SetSize(self.GetSize())
        self.lc.InsertColumn(0, "Date")
        self.lc.InsertColumn(1, "Comment")
        self.lc.InsertColumn(2, "Amount")
        self.currentItem = 0
        EVT_LIST_ITEM_SELECTED(self, tID, self.OnItemSelected)
        EVT_LEFT_DCLICK(self.lc, self.OnDoubleClick)
        menu = parent.MakeMenu(true)
        self.SetMenuBar(menu)
        EVT_MENU(self, editID, self.OnEdit)
        EVT_CLOSE(self, self.OnCloseWindow)
        self.UpdateView()

    def OnItemSelected(self, event):
        self.currentItem = event.m_itemIndex

    def OnDoubleClick(self, event):
        self.OnEdit()

    def UpdateView(self):
        self.lc.DeleteAllItems()
        for x in range(len(self.bookset)):
            trans = self.bookset[x]
            self.lc.InsertStringItem(x, trans.getDateString())
            self.lc.SetStringItem(x, 1, trans.comment)
            self.lc.SetStringItem(x, 2, str(trans.magnitude()))
            self.lc.SetColumnWidth(0, wxLIST_AUTOSIZE)
            self.lc.SetColumnWidth(1, wxLIST_AUTOSIZE)
            self.lc.SetColumnWidth(2, wxLIST_AUTOSIZE)
            self.SetTitle("Journal view - %d transactions" %
                          len(self.bookset))

    def OnEdit(self, *event):
        if self.currentItem:
            trans = self.bookset[self.currentItem]
            dlg = EditTransDlg(self, trans,
                               self.bookset.getAccountList())
            if dlg.ShowModal() == wxID_OK:
                trans = dlg.GetTrans()
                self.bookset.edit(self.currentItem, trans)
                self.parent.UpdateViews()
            dlg.Destroy()
4.7.7 wxDialog和相关控件

构建一个对话框来编辑交易记录,使用了 wxStaticText wxTextCtrl wxComboBox 等控件,并使用 EVT_BUTTON 等事件处理程序来处理用户操作。

class EditTransDlg(wxDialog):
    def __init__(self, parent, trans, accountList):
        wxDialog.__init__(self, parent, -1, "")
        self.item = -1
        if trans:
            self.trans = copy.deepcopy(trans)
            self.SetTitle("Edit Transaction")
        else:
            self.trans = Transaction()
            self.trans.setDateString(dates.ddmmmyyyy(self.trans.date))
            self.SetTitle("Add Transaction")
        # Create some controls
        wxStaticText(self, -1, "Date:", wxDLG_PNT(self, 5,5))
        self.date = wxTextCtrl(self, ID_DATE, "",
                               wxDLG_PNT(self, 35,5), wxDLG_SZE(self, 50, -1))
        wxStaticText(self, -1, "Comment:", wxDLG_PNT(self, 5,21))
        self.comment = wxTextCtrl(self, ID_COMMENT, "",
                                  wxDLG_PNT(self, 35, 21), wxDLG_SZE(self, 195,-1))
        self.lc = wxListCtrl(self, ID_LIST,
                             wxDLG_PNT(self, 5,34), wxDLG_SZE(self, 225,60),
                             wxLC_REPORT)
        self.lc.InsertColumn(0, "Account")
        self.lc.InsertColumn(1, "Amount")
        self.lc.SetColumnWidth(0, wxDLG_SZE(self, 180,-1).width)
        self.lc.SetColumnWidth(1, wxDLG_SZE(self, 40,-1).width)
        wxStaticText(self, -1, "Balance:", wxDLG_PNT(self, 165,100))
        self.balance = wxTextCtrl(self, ID_BAL, "",
                                  wxDLG_PNT(self, 190,100),
                                  wxDLG_SZE(self, 40, -1))
        self.balance.Enable(false)
        wxStaticLine(self, -1, wxDLG_PNT(self, 5,115),
                     wxDLG_SZE(self, 225,-1))
        wxStaticText(self, -1, "Account:", wxDLG_PNT(self, 5,122))
        self.account = wxComboBox(self, ID_ACCT, "",
                                  wxDLG_PNT(self, 30,122), wxDLG_SZE(self, 130,-1),
                                  accountList, wxCB_DROPDOWN | wxCB_SORT)
        wxStaticText(self, -1, "Amount:", wxDLG_PNT(self, 165,122))
        self.amount = wxTextCtrl(self, ID_AMT, "",
                                 wxDLG_PNT(self, 190,122),
                                 wxDLG_SZE(self, 40, -1))
        btnSz = wxDLG_SZE(self, 40,12)
        wxButton(self, ID_ADD, "&Add Line", wxDLG_PNT(self, 52,140), btnSz)
        wxButton(self, ID_UPDT, "&Update Line", wxDLG_PNT(self, 97,140),
                 btnSz)
        wxButton(self, ID_DEL, "&Delete Line", wxDLG_PNT(self, 142,140),
                 btnSz)
        self.ok = wxButton(self, wxID_OK, "OK", wxDLG_PNT(self, 145,5),
                           btnSz)
        self.ok.SetDefault()
        wxButton(self, wxID_CANCEL, "Cancel", wxDLG_PNT(self, 190,5), btnSz)
        # Resize the window to fit the controls
        self.Fit()
        # Set some event handlers
        EVT_BUTTON(self, ID_ADD, self.OnAddBtn)
        EVT_BUTTON(self, ID_UPDT, self.OnUpdtBtn)
        EVT_BUTTON(self, ID_DEL, self.OnDelBtn)
        EVT_LIST_ITEM_SELECTED(self,   ID_LIST, self.OnListSelect)
        EVT_LIST_ITEM_DESELECTED(self,   ID_LIST, self.OnListDeselect)
        EVT_TEXT(self, ID_DATE, self.Validate)
        # Initialize the controls with current values
        self.date.SetValue(self.trans.getDateString())
        self.comment.SetValue(self.trans.comment)
        for x in range(len(self.trans.lines)):
            account, amount, dict = self.trans.lines[x]
            self.lc.InsertStringItem(x, account)
            self.lc.SetStringItem(x, 1, str(amount))
        self.Validate()

    def Validate(self, *ignore):
        bal = self.trans.balance()
        self.balance.SetValue(str(bal))
        date = self.date.GetValue()
        try:
            dateOK = (date == dates.testasc(date))
        except:
            dateOK = 0
        if bal == 0 and dateOK:
            self.ok.Enable(true)
        else:
            self.ok.Enable(false)

    def OnAddBtn(self, event):
        account = self.account.GetValue()
        amount = string.atof(self.amount.GetValue())
        self.trans.addLine(account, amount)
        # update the list control
        idx = len(self.trans.lines)
        self.lc.InsertStringItem(idx-1, account)
        self.lc.SetStringItem(idx-1, str(amount))
        self.Validate()
        self.account.SetValue("")
        self.amount.SetValue("")

4.8 wxPython总结

wxPython的功能非常强大,本文只是触及了它的表面。它提供了比本文展示的更多的窗口和控件类型,其高级功能适用于构建高度灵活和动态的GUI应用程序,并且可以在多个平台上运行。结合Python的灵活性,wxPython是快速创建世界级应用程序的强大工具。要了解更多关于wxPython的信息,可以访问其官方网站http://alldunn.com/wxPython/ ,以及wxWindows的官方网站http://www.wxwindows.org/ 。

综上所述,Tkinter适合小型、可移植的应用程序;PythonWin在特定的Windows开发场景中有其优势;而wxPython则提供了一个强大的跨平台解决方案。在选择GUI工具包时,需要根据项目的具体需求和特点来做出决策。

5. 三种GUI工具包的对比

5.1 功能特性对比

工具包 跨平台能力 小部件丰富度 事件处理 布局管理
Tkinter 强,支持Windows、Mac、Unix和Linux 基本小部件集,缺乏现代高级小部件 通过 bind 方法绑定事件,事件类型丰富 提供 pack() grid() place() 三种几何管理方法
PythonWin 主要针对Windows平台 依赖MFC,有一定的小部件支持 结合MFC的事件处理机制 基于MFC的布局方式
wxPython 跨平台,支持多种操作系统 丰富的窗口和控件类型 提供多种 EVT_* 辅助函数绑定事件 提供布局约束、布局算法、Sizers等多种布局方式

5.2 开发难度对比

  • Tkinter :相对简单,语法简洁,适合初学者快速上手。其文档资源丰富,有大量的示例代码可供参考。
  • PythonWin :需要对MFC有一定的了解,开发难度较大。MFC的概念和机制较为复杂,对于没有C++和Windows开发经验的开发者来说,学习曲线较陡。
  • wxPython :有一定的学习成本,但由于其类层次结构与C++版本的wxWindows相似,对于有C++开发经验的开发者来说,容易上手。其文档和示例代码也比较丰富。

5.3 性能对比

  • Tkinter :理论上由于需要通过Tcl解释执行,可能会有一定的性能损失,但在实际应用中,大多数情况下性能表现良好。
  • PythonWin :基于MFC,性能表现与MFC应用程序类似,但由于涉及Python和C++的交互,可能会有一些性能开销。
  • wxPython :封装了wxWindows C++类库,性能相对较好,能够满足大多数应用程序的需求。

5.4 适用场景对比

  • Tkinter :适合小型、快速开发的GUI应用程序,尤其是对跨平台性要求较高的场景。例如,简单的脚本工具、教学示例等。
  • PythonWin :适合已有MFC投资或需要使用MFC特定功能的Windows开发场景。例如,与Windows系统深度集成的应用程序、需要使用Windows特定API的应用程序等。
  • wxPython :适合开发跨平台的大型GUI应用程序,尤其是需要丰富的窗口和控件类型、高级布局管理和事件处理的场景。例如,桌面应用程序、图形编辑器、IDE等。

6. 选择合适的GUI工具包

6.1 考虑因素

  • 项目需求 :明确项目的功能需求、平台需求、性能需求等。例如,如果项目需要在多个平台上运行,那么Tkinter或wxPython可能是更好的选择;如果项目需要与Windows系统深度集成,那么PythonWin可能更合适。
  • 开发经验 :考虑开发团队的技术栈和开发经验。如果团队成员熟悉Python但缺乏Windows开发经验,那么Tkinter或wxPython可能更容易上手;如果团队成员有C++和MFC开发经验,那么PythonWin可能是一个不错的选择。
  • 学习成本 :评估学习新工具包所需的时间和精力。如果项目时间紧迫,那么选择一个简单易用的工具包可以提高开发效率。
  • 社区支持 :选择一个有活跃社区支持的工具包,可以更容易地获取帮助和资源。例如,Tkinter和wxPython都有庞大的社区,提供了丰富的文档、示例代码和论坛。

6.2 决策流程

graph TD;
    A[明确项目需求] --> B{是否需要跨平台};
    B -- 是 --> C{对小部件丰富度要求高吗};
    B -- 否 --> D{是否已有MFC投资};
    C -- 是 --> E[选择wxPython];
    C -- 否 --> F[选择Tkinter];
    D -- 是 --> G[选择PythonWin];
    D -- 否 --> H{对开发难度要求低吗};
    H -- 是 --> F;
    H -- 否 --> E;

7. 总结与展望

7.1 总结

本文详细介绍了Python中三种常用的GUI工具包:Tkinter、PythonWin和wxPython。Tkinter是Python的标准GUI工具包,具有跨平台能力强、语法简洁等优点,适合小型、快速开发的应用程序;PythonWin基于MFC,为Windows开发提供了强大的功能,但开发难度较大,适合已有MFC投资的场景;wxPython封装了wxWindows C++类库,提供了丰富的窗口和控件类型、高级布局管理和事件处理功能,适合开发跨平台的大型GUI应用程序。

7.2 展望

随着Python的不断发展,GUI工具包也在不断完善和创新。未来,我们可以期待这些工具包在性能、功能和易用性方面取得更大的进步。例如,Tkinter可能会进一步优化性能,减少对Tcl的依赖;PythonWin可能会提供更完善的文档和开发工具,降低开发难度;wxPython可能会增加更多的高级功能和控件类型,提高开发效率。同时,新的GUI工具包也可能会不断涌现,为Python开发者提供更多的选择。

在选择GUI工具包时,开发者应根据项目的具体需求和自身的开发经验,综合考虑各种因素,做出合适的决策。希望本文能够为Python开发者在GUI开发方面提供有价值的参考。

【最优潮流】直流最优潮流(OPF)课设(Matlab代码实现)内容概要:本文档主要围绕“直流最优潮流(OPF)课设”的Matlab代码实现展开,属于电力系统优化领域的教学科研实践内容。文档介绍了通过Matlab进行电力系统最优潮流计算的基本原理编程实现方法,重点聚焦于直流最优潮流模型的构建求解过程,适用于课程设计或科研入门实践。文中提及使用YALMIP等优化工具包进行建模,并提供了相关资源下载链接,便于读者复现学习。此外,文档还列举了大量电力系统、智能优化算法、机器学习、路径规划等相关的Matlab仿真案例,体现出其服务于科研仿真辅导的综合性平台性质。; 适合人群:电气工程、自动化、电力系统及相关专业的本科生、研究生,以及从事电力系统优化、智能算法应用研究的科研人员。; 使用场景及目标:①掌握直流最优潮流的基本原理Matlab实现方法;②完成课程设计或科研项目中的电力系统优化任务;③借助提供的丰富案例资源,拓展在智能优化、状态估计、微电网调度等方向的研究思路技术手段。; 阅读建议:建议读者结合文档中提供的网盘资源,下载完整代码工具包,边学习理论边动手实践。重点关注YALMIP工具的使用方法,并通过复现文中提到的多个案例,加深对电力系统优化问题建模求解的理解。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值