WxPython——构建你的蓝图

创建你的UI编程思维

众所周知,GUI代码是难于阅读和维护的,并且看上去总是一塌糊涂。本章我们将讨论三个驯服你的UI代码的技术。我们将讨论重构代码以使其易于阅读、管理和维护。另一个方面是显示代码和基本的处理对象之间的处理,这也是UI程序员头痛的地方。MVC(ModełView/Controller,也就是模型-视图-控制器模式)设计模式是这样一种结构,它保持显示和数据分离以便各自的改变相互不影响。最后,我们讨论对你的wxPython代码进行单元测试的技术。尽管本章的所有例子将使用wxPython,但是其中的多数原则是可以应用到任何UI工具的,代码的设计和体系结构就是所谓的UI编程思维。一个深思熟虑的蓝图将使得你的应用程序建造起来更简单和更易维护。本章的建议将帮助你为你的程序设计一个可靠的蓝图。

重构如何帮我改进我的代码?

好的程序员为什么也会写出不好的界面或界面代码?这有很多原因。甚至一个简单的用户界面可能都要求很多行来显示屏幕上的所有元素。程序员通常试图用单一的方法来实现这些,这种方法迅速变得长且难于控制。此外界面代码是很容易受到不断改变的影响的,除非你对管理这些改变训练有素。由于写界面代码可能是很枯燥的,所以界面程序员经常会使用设计工具来生成代码。机器生成的代码相对于手工代码来说是很差。

原则上讲,保持UI代码在控制之下是不难的。关键是重构或不断改进现有代码的设计和结构。重构的目的是保持代码在以后易读和易于维护。下面说明了在重构时需要记住的一些原则。最重要的是要记住,某人以后可能会不得不读和理解你的代码。努力让他人的生活更容易些,毕竟那有可能是你。风水轮流转,我最讨厌的两件事,一个是别人写的代码没有注释和文档,一种是让我写代码的时候加注释和文档。-

重构的一些重要原则

  • 不要重复:你应该避免有多个相同功能的段。当这个功能需要改变时,这维护起来会很头痛。
  • 一次做一件事情:一个方法应该并且只做一件事情。各自的事件应该在各自的方法中。方法应该保持短小。
  • 嵌套的层数要少:尽量使嵌套代码不多于2或3层。对于一个单独的方法,深的嵌套也是一个好的选择。
  • 避免出现硬编码的字符串和数字:字面意义上的字符串和数字应使其出现在代码中的次数最小化。一个好的方法是,把它们从你的代码的主要部分中分离出来,并存储于一个列表或字典中。

这些原则在Python代码中特别重要。因为Python的缩进语法、小而简洁的方法是很容易去读的。然而,长的方法对于理解来说是更困难的,尤其是如果它们在一个屏幕上不能完全显示出来时。类似的,Python中的深的嵌套使得跟踪代码块的开始和结尾很棘手。然而,Python在避免重复方面是十分好的一种语言,特别是因为函数和方法或以作为参数传递。

一个重构的例子

为了展示给你如何在实际工作中应用这些原则,我们将看一个重构的例子。下面显示了一个窗口,它可用作访问微软Access类数据库的前端。

它的布置比之前我们的所见过的那些要复杂一些。但是按现实中的应用程序的标准,它仍然十分简单。例5.1的代码的结构很差。

他的原始代码如下所示:

#!/usr/bin/env python
import wx

class RefactorExample(wx.Frame):
    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, id, 'RefactorExample',size = (340, 200))
        panel = wx.Panel(self, -1)
        panel.SetBackgroundColour("White")
        prevButton = wx.Button(panel, -1, "<< PREV" , pos = (80, 0))
        self.Bind(wx.EVT_BUTTON, self.OnPrev, prevButton)
        nextButton = wx.Button(panel, -1, "NEXT >>", pos = (160, 0))
        self.Bind(wx.EVT_BUTTON, self.OnNext, nextButton)
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
        menuBar = wx.MenuBar()
        menu1 = wx.Menu()
        openMenuItem = menu1.Append(-1, "& Open", "Copy in statusbar")
        self.Bind(wx.EVT_MENU, self.OnOpen, openMenuItem)
        quitMenuItem = menu1.Append(-1, "& Quit", "Quit")
        self.Bind(wx.EVT_MENU, self.OnCloseWindow, quitMenuItem)
        menuBar.Append(menu1,  "& File")
        menu2 = wx.Menu()
        copyItem = menu2.Append(-1, " & Copy", "Copy")
        self.Bind(wx.EVT_MENU, self.OnCopy, copyItem)
        cutItem = menu2.Append(-1, "C & ut", "Cut")
        self.Bind(wx.EVT_MENU, self.OnCut, cutItem)
        pasteItem = menu2.Append(-1, "Paste", "Paste")
        self.Bind(wx.EVT_MENU, self.OnPaste, pasteItem)
        menuBar.Append(menu2, " & Edit")
        self.SetMenuBar(menuBar)
        static = wx.StaticText(panel, wx.NewId(), "FirstName",pos = (10, 50))
        static.SetBackgroundColour("White")
        text = wx.TextCtrl(panel, wx.NewId(), "", size = (100, -1),pos = (80, 50))
        static2 = wx.StaticText(panel, wx.NewId(), "LastName",pos = (10, 80))
        static2.SetBackgroundColour("White")
        text2 = wx.TextCtrl(panel, wx.NewId(), "", size = (100, -1),pos = (80, 80))
        firstButton = wx.Button(panel, -1, "FIRST")
        self.Bind(wx.EVT_BUTTON, self.OnFirst, firstButton)
        menu2.AppendSeparator()
        optItem = menu2.Append(-1, " & Options...", "DisplayOptions")
        self.Bind(wx.EVT_MENU, self.OnOptions, optItem)
        lastButton = wx.Button(panel, -1, "LAST", pos = (240, 0))
        self.Bind(wx.EVT_BUTTON, self.OnLast, lastButton)
    # Just grouping the empty event handlers together
    def OnPrev(self, event): pass


    def OnNext(self, event): pass


    def OnLast(self, event): pass


    def OnFirst(self, event): pass


    def OnOpen(self, event): pass


    def OnCopy(self, event): pass


    def OnCut(self, event): pass


    def OnPaste(self, event): pass


    def OnOptions(self, event): pass


    def OnCloseWindow(self, event):
        self.Destroy()
if __name__ == "__main__":
    app = wx.App()
    frame = RefactorExample(parent=None, id=-1)
    frame.Show()
    app.MainLoop()

他的实现效果如下所示:
在这里插入图片描述

根据重构原则,上面这段代码有一点是做到了,就是没有深的嵌套。其它都没有做到。为了让你有一个关于如何调整的一个思想,我们将把所有的按钮代码分别放到各自的方法中。

下面归纳了我们重构原代码应解决的问题:

  • 原则:代码要重构的地方
  • 不要重复:几个模式不断重复,包括“增加按钮,关联一个方法”, “增加菜单项并关联一个方法”,“创建成对的标签/文本条目”
  • 一次只做一件事:代码做了几件事情。除了基本的框架(frame)设置外,它创建了菜单栏,增加了按钮,增加了文本域。更糟糕的是,功能在代码中混在一起。
  • 避免避免字面意义上的字符串和数字:在构造器中每个按钮、菜单项和文本框都有一个文字字符串和坐标常量

开始重构

在之前的项目中,我们发现了一些问题,我们将一步一步的指出这里面的问题,并且尽量的优化他:

第一个问题就是,按钮的创建位置过于散乱,这个问题其实也会出现在其他的地方,我们在编写UI的时候经常会犯一个错误就是想到什么写什么,比如我这里需要一个按钮,就添加一个按钮,或者是我这里需要一个对话框,我就要添加一个对话框,但其实这样是很不合理的,当我们想要修改这个组件的时候,会导致我们找很长时间都找不到这个组件在哪里,那么这个时候,在开始编写软件之前对UI有一个大致的规划,将同类的控件创建封装成一个函数,并且留出一个创建新组件的函数是非常有必要的,比如说,我们在开始编写之前先大致的想一下会需要什么功能,需要几个按钮,或者其他的什么控件,那么提前用一个函数将所有的控件都封装起来,然后一起创建。并且这些控件我们可以封装一下,比如在官方提供的创建控件的API中,其实ID是不需要我们自己管理的,那么我们在封装的时候可以全部都填写-1,让系统帮我们自己决定这个控件的ID,我们只需要关注他的名字,提示词和大小(这里没有位置,是因为之后我们会通过sizer来控制他的位置)。

这样优化之后,我们代码的规范性和可维护性将会提升很大一部分。

进一步重构

上面的例子,已经得到了很多的改善。但是在多处仍有许多常量。其一,就是用于定位的点坐标,当另一个按钮被添加到按钮栏时可能使代码产生错误,尤其是新的按钮被放置在按钮栏的中间。因此让我们再往前进一步,我们把这些字面意义上的数据从处理中分离出来。

这一步要完成的就是控件与文件之间的分离,并且我们在制作的时候,需要手动去计算每个按钮之间的位置,如果出现一个新的按钮,那么我们就需要计算这个新的按钮之间的位置,但是我们可以通过代码的优化,让他去不这么做。

用于不同按钮的数据被存储在内嵌于buttonData()方法的元组中。所选的数据结构及常量方法的使用不是必然的。数据也可以被存储在一个类级的变量或模块级的变量中,而非一个方法的结果,或存储于一个外部的文件中。使用方法的好处就是,如果你的按钮数据存储在另一个地方而不是方法中的话,只需要改变这个方法而使它返回外部的数据。

createButtonBar()方法遍历buttonData()返回的列表并创建相关数据的按钮。这个方法集依次根据列表自动计算按钮的x坐标。这是很有帮助的,因为它保证了代码中按钮的次序与将显示在屏幕中的次序一样,使得代码更清晰并减少出错的机会。如果你需要将一个按钮添加到按钮栏的中间的话,你只需把数据添加到这个列表的中间,这个代码确保了所加按钮被放置在中间。

数据的分离有其它的好处。在一个更精心制作的例子中,数据可以被存储到一个外部的资源或XML文件中。这使得在改变界面的时候不用去关心代码,并且使国际化更容易,很容易改变文本。移除了数据以后,createButtonBar方法现在成了一个公用方法了,它可以容易地在其它框架或项目中被重用。

如何保持模型(Model)与视图(View)分离?

最早可追溯到1970年代后期和Smalltalk-80语言,MVC模式大概是最早明确指出面向对象设计的模式。它是最流行的一种,被几乎所有GUI工具包所采用。MVC模式是结构化程序的标准,包括处理和显示信息。

MVC(Model-View-Controller)系统是什么?

MVC系统有三个子系统。Model包含经常被调用的业务逻辑或由你的系统处理的所有数据和信息。View包含显示数据的对象,Controller管理与用户的交互(Controller处于Model和view中间)。

标准MVC体系的组成
  • Model:包含业务逻辑,包含所有由系统处理的数据。它包括一个针对外部存储(如一个数据库)的接口。通常模型(model)只暴露一个公共的API给其它的组分。
  • View:包含显示代码。这个窗口部件实际用于放置用户在视图中的信息。在wxPython中,处于wx.Window层级中的所有的东西都是视图(view)子系统的一部分。
  • Controller:包含交互逻辑。该代码接受用户事件并确保它们被系统处理。在wxPython中,这个子系统由wx.EvtHandler层级所代表。

在现代的UI工具包中,View和Controller组分是被集成在一起的。这是因为Controller组分自身需要被显示在屏幕上,并且因为经常性的你想让显示数据的窗口部件也响应用户事件。在wxPython中,这种关系实际上已经被放置进去了(所有的wx.Window对象也都是wx.EvtHandler的子类),这意味着它们同时具有View元素和Controller元素的功能。相比之下,大部分web应用架构对于View和Controller有更严格的分离,因为其交互逻辑发生在服务器的后台。

一个事件通知被Controller系统处理(它把事件通知放到一个合适的地方)。如我们在第三章中所看到的,wxPython使用wx.EvtHandler的方法ProcessEvent()管理这个机制。在一个严格的MVC设计中,你的处理器函数可能被声明在一个单独的控制器对象中,而非在框架类自身中。

对于事件的响应,这个模型(model)对象可以对应用程序数据做一些处理。当处理完成时,模型对象发送一个更新通知。如果这儿有一个控制器(controller)对象,那么该通知通常发送回这个控制器,同时这个控制器对象通知视图(view)对象自我更新。在一个较小的系统或一个较简单的体系中,通知通常直接被视图对象所接受。在wxPython中,来自于模型的更新的关键在于你。你的选择包括从模型或控制器显式地引发自定义的wxPython事件,使模型维护的对象的列表接受更新通知,或使与模型关联的视图接受更新通知

一个成功的MVC设计的关键不在于每个对象都彼此了解。相反,一个成功的MVC程序,它的不同部分之间显式地隐藏了一些东西。其目的是使系统最低限度地交互,和方法之间的明确的界定。尤其,这个Model组分应该被完全从View和Controller中脱离出来。你应该只改变那些系统而不改变你的Model类。理想上来讲,你甚至应该能够使用相同Model类来驱动非wxPython的界面,但是那很难。

从View方面,你应该能够在Model对象的实现中做改变而不改变View或Controller。而View依赖于某些公共的方法的存在,它不应该看见Model内私有的东西。无可否认,这在Python中实施是有困难的,但有一个方法可以帮助我们,那就是创建一个抽象的Model类,它定义View可以看见的API。Model的子类可以扮演一个内部的类的代理而被改变,或可以简单地自身包含内部的工作。这第一个方案更结构化些,第二个更容易实现。

下一节,我们将看一看内建于wxPython中的Model类中的一个:wx.grid.PyGridTableBase。这个类使得在一个MVC设计架构中使用grid控件成为可能。这之后,我们将关注一下对于一个定制的窗口部件建造和使用定制的模型类。

本章小结

1、众所周知,GUI代码看起来很乱且难于维护。这一点可以通过一点努力来解决,当代码以后要变动时,我们所付出的努力是值得的。

2、重构是对现存代码的改进。重构的目的有:避免重复、去掉无法理解的字面值、创建短的方法(只做一件事情)。为了这些目标不断努力将使你的代码更容易去读和理解。另外,好的重构也几乎避免了某类错误的发生(如剪切和粘贴导致的错误)。

3、把你的数据从代码中分离出来,使得数据和代码更易协同工作。管理这种分离的标准机制是MVC机制。用wxPython的术语来说,V(View)是wx.Window对象,它显示你的数据;C(Controller)是wx.EvtHandler对象,它分派事件;M(Model)是你自己的代码,它包含被显示的信息。

4、或许MVC结构的最清晰的例子是wxPython的核心类中的wx.grid.PyGridTableBase,它被用于表示数据以在一个wx.grid.Grid控件中显示。表中的数据可以来自于该类本身,或该类可以引用另一个包含相关数据的对象。

5、你可以使用一个简单的机制来创建你自己的MVC设置,以便在模型被更新时通知视图(view)。在wxPython中也有现成的模块可以帮助你做这样的事情。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值