经过前期的设计,我们已经可以从网上获取天气信息,并且可以使用邮件发送。今天我们要给程序增加一个界面,毕竟并不是所有人都习惯命令行操作。先给大家看一下最终效果
这是在Win7下的运行效果。我使用wxPython来实现GUI。wxPython是wxWidgets的Python实现,而wxWidgets是一个开源的跨平台的C++构架库(framework),它可以提供GUI(图形用户界面)和其它工具。
开发任一wxPython程序必须包括五个基本步骤:
1、导入必须的wxPython包
2、子类化wxPyhon应用程序类
3、定义一个应用程序的初始化方法
4、创建一个应用程序类的实例
5、进入这个应用程序的主事件循环
下面我们一步一步地来实现这个界面。
首先,我们需要导入必须的wxPython包。
import wx一旦这个包被导入,你就可以引用wxPython的类、函数和常量(它们以wx为前缀,如wx.Frame)。当然也可以使用from wx import *来导入具体用到的类、函数或者变量,这样使用时就不必加上wx前缀,但是这样可能导致命名空间冲突,所以不建议这样做。
通常情况下,Python中的模块导入顺序无关紧要,但是wxPython中的不同,它是一个复杂的模块,当你第一次导入wx模块时,wxPython需要对别的wxPython模块执行一些初始化工作。所以我们必须将import wx作为第一条导入名句,然后再导入其他的Python模块,其他的Python模块导入顺序可以随意。
接下来,我们要使用应用程序和框架工作。每个wxPython程序必须有一个application对象和至少一个frame对象。Application对象必须是wx.App或其子类的一个实例。
要子类化wxPython的应用程序类,也就是wx.App,代码如下:
class MyApp(wx.App): """应用程序类wx.App的子类""" def __init__(self, title, sText): #重构构造方法,其中title、sText分别是传递给主窗口的标题参数和状态栏参数 wx.App.__init__(self) #调用wx.App的构造方法 frame = MyFrame(title, sText) #创建主窗口对象 frame.Show(True) #显示主窗口
上面的代码定义了一个名为MyApp的子类,__init__方法定义了应用程序的初始化方法,在该方法中我们调用了其父方法wx.App.__init__,然后调用MyFrame方法(后面会实现)创建了一个窗口,再调用Show方法使窗口可见。
为了使应用程序运行,我们需要创建一个应用程序实例并进入它的主事件循环,代码如下:
app = MyApp(wTitle, wTime) app.MainLoop()至此,上述五个基本步骤我们都已经实现了,接下来我们简单说明一下如何实现文章开头的图形用户界面,也就是实现MyFrame类。
MyFrame类是wx.Frame类的子类,创建MyFrame类时需要调用其父类的构造器wx.Frame.__init__(),wx.Frame的构造器所要求的参数如下:
wx.Frame(parent, id=-1, title=””, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name=”frame”)
我们在别的窗口部件的构造器中将会看到类似的参数。参数的说明如下:
parent:框架的父窗口。对于顶级窗口,这个值是None。框架随其父窗口的销毁而销毁。取决于平台,框架可被限制只出现在父窗口的顶部。在多文档界面的情况下,子窗口被限制为只能在父窗口中移动和缩放。 id:关于新窗口的wxPython ID号。你可以明确地传递一个。或传递-1,这将导致wxPython自动生成一个新的ID。 title:窗口的标题。 pos:一个wx.Point对象,它指定这个新窗口的左上角在屏幕中的位置。在图形用户界面程序中,通常(0,0)是显示器的左上角。这个默认的(-1,-1)将让系统决定窗口的位置。 size:一个wx.Size对象,它指定这个窗口的初始尺寸。这个默认的(-1,-1)将让系统决定窗口的初始尺寸。 style:指定窗口的类型的常量。你可以使用或运算来组合它们。 name:框架的内在的名字。以后你可以使用它来寻找这个窗口。
除于parent参数外,其他参数都是可选的。
def __init__(self, title, sText): wx.Frame.__init__(self, None, -1, title=title, style=wx.DEFAULT_FRAME_STYLE ^ (wx.MAXIMIZE_BOX | wx.RESIZE_BORDER))我们这里所用的style是默认类型但是没有最大化按钮和可缩放的边框。
这个窗口创建后内容是空的,我们需要在框架中插入各种对象和控件。首先增加一个wx.Panel的实例,它是其他控件的容器。
self.panel = wx.Panel(self)
我们还需要一个状态栏来显示天气发布的时间
stBar = self.CreateStatusBar()
接下来创建窗口中的各种控件。我们的程序一共需要三种控件:wx.StaticBitmap、wx.StaticText和wx.Button分别表示静态图片控件、静态文本控件以及按钮控件。这三种控件的构造方法都比较类似:
wx.StaticBitmap(parent, id=-1, bitmap=wxNullBitmap, pos=DefaultPosition,size=DefaultSize, style=0, name=StaticBitmapNameStr) wx.StaticText(parent, id, label, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, name=”staticText”) wx.Button(parent, id, label, pos, size=wxDefaultSize, style=0, validator=DefaultValidator, name=”button”)
其中,wx.StaticBitmap构造器中的bitmap参数是一个Bitmap对象,我们可以先从本地图片文件创建图像对象,然后再转换成Bitmap对象,再生成wx.StaticBitmap控件。
创建各种控件比较简单,但是我们不能直接把创建好的控件放置在窗口中,这样的话界面比较丑陋,因此我们需要使用布局管理器——sizer来管理我们的布局。sizer是用于自动布局一组窗口部件的算法。sizer被附加到一个容器,通常是一个框架或面板。在父容器中创建的子窗口部件必须被分别地添加到sizer。当sizer被附加到容器时,它随后就管理它所包含的孩子的布局。
使用sizer的好处是很多的。当子窗口部件的容器的尺寸改变时,sizer将自动计算它的孩子的布局并作出相应的调整。同样,如果其中的一个孩子改变了尺寸,那么sizer能够自动地刷新布局。此外,当你想要改变布局时,sizer管理起来很容易。最大的弊端是sizer的布局有一些局限性。但是,最灵活sizer——grid bag和box,能够做你要它们做的几乎任何事。
Sizer之间也可以互相嵌套,本例一共使用了3种Sizer:GridbagSizer、BoxSizer、StaticBoxSizer。整个窗口使用BoxSizer将内容分为上下两部分;上半部分使用BoxSizer再分为左右两部分,左半部分使用GridBagSizer来布局天气信息,再嵌入到一个StaticBoxSizer中,右半部分使用StaticBoxSizer放置了三个按钮;下半部分使用GridBagSizer来布局每一天的天气信息,再使用StaticBoxSizer来布局未来4天的天气信息。
完整程序如下所示,关键地方我都有注释。
#! /usr/bin/env python #coding=utf-8 import wx from GetWeather import WeatherInfo #todayInfo=u"12月8日星期四 小雨转多云7℃/-1℃ 北风5-6级 img/07.gif img/01.gif".split() #daysInfo=[todayInfo,todayInfo,todayInfo,todayInfo] class MyApp(wx.App): """应用程序类wx.App的子类""" def __init__(self, title, sText): #重构构造方法,其中title、sText分别是传递给主窗口的标题参数和状态栏参数 wx.App.__init__(self) #调用wx.App的构造方法 frame = MyFrame(title, sText) #创建主窗口对象 frame.Show(True) #显示主窗口 class MyFrame(wx.Frame): def __init__(self, title, sText): wx.Frame.__init__(self, None, -1, title=title, style=wx.DEFAULT_FRAME_STYLE ^ (wx.MAXIMIZE_BOX | wx.RESIZE_BORDER)) self.panel = wx.Panel(self) stBar = self.CreateStatusBar() #创建状态栏 stBar.SetStatusText(sText) #状态栏显示发布时间 hdLeft = self.LayoutHeadLeft(todayInfo) hdRight = self.LayoutHeadRight() head = self.LayoutHead(hdLeft, hdRight) days = self.LayoutDaysWeather(daysInfo) sizer = self.LayoutPanel(head, days) self.panel.SetSizer(sizer) sizer.Fit(self) sizer.SetSizeHints(self) def LayoutHeadLeft(self, todayInfo): """窗口上半部分左侧布局,主要是今日天气信息。 todayInfo=[日期,图片1,图片2,天气,风力]""" #一个静态文本控件 tLbl = wx.StaticText(self.panel, -1, label=todayInfo[3]) #从文件载入图像 img1 = wx.Image("img/a_" + todayInfo[1], wx.BITMAP_TYPE_GIF) img2 = wx.Image("img/a_" + todayInfo[2], wx.BITMAP_TYPE_GIF) #转换为静态图像控件 sb1 = wx.StaticBitmap(self.panel, -1, wx.BitmapFromImage(img1)) sb2 = wx.StaticBitmap(self.panel, -1, wx.BitmapFromImage(img2)) #使用GridBagSizer来放置两个图片和两个文本控件 GBSizer = wx.GridBagSizer(vgap=5, hgap=5) #Grid之间水平、竖直方向间距都是5个像素 GBSizer.Add(sb1, pos=(0, 0), span=(1, 1), flag=0) #天气图片1,放置在第1行第1列,占一行一列 GBSizer.Add(sb2, pos=(0, 1), span=(1, 1), flag=0) #天气图片2,放置在第1行第2列,占一行一列 GBSizer.Add(tLbl, pos=(1, 0), span=(1, 2), flag=0) #天气文字信息,放置在第2行第1列,占一行两列 #再使用Static Box Sizer来放置上面的GridBagSizer sbox = wx.StaticBox(self.panel, -1, todayInfo[0]) sbsizer = wx.StaticBoxSizer(sbox, wx.VERTICAL) sbsizer.Add(GBSizer, 0, wx.ALL, 2) return sbsizer def LayoutHeadRight(self): """放置3个按钮""" #三个按钮控件 updateBtn = wx.Button(self.panel, -1, label=u"更新") setupBtn = wx.Button(self.panel, -1, label=u"设置") sendBtn = wx.Button(self.panel, -1, label=u"发送") #使用Static Box Sizer来放置上面的GridBagSizer sbox = wx.StaticBox(self.panel, -1, u"个性化") sbsizer = wx.StaticBoxSizer(sbox, wx.VERTICAL) sbsizer.Add(updateBtn, 0, wx.ALL, 2) sbsizer.Add(setupBtn, 0, wx.ALL, 2) sbsizer.Add(sendBtn, 0, wx.ALL, 2) return sbsizer def LayoutHead(self, headLeft, headRight): box = wx.BoxSizer(wx.HORIZONTAL) #使用水平BoxSizer放置今日天气信息和3个按钮 box.Add(headLeft, 1, flag=wx.EXPAND) box.Add(headRight, flag=wx.EXPAND) return box def LayoutDayWeather(self, dayInfo): """放置未来4天每天的天气信息""" #两个静态文本控件 dLbl = wx.StaticText(self.panel, -1, label=dayInfo[0]) tLbl = wx.StaticText(self.panel, -1, label=dayInfo[3]) #wLbl=wx.StaticText(self.panel,-1,label=todayInfo[2]) #从文件载入图像 img1 = wx.Image("img/" + dayInfo[1], wx.BITMAP_TYPE_GIF) img2 = wx.Image("img/" + dayInfo[2], wx.BITMAP_TYPE_GIF) #转换为静态图像控件 sb1 = wx.StaticBitmap(self.panel, -1, wx.BitmapFromImage(img1)) sb2 = wx.StaticBitmap(self.panel, -1, wx.BitmapFromImage(img2)) #使用GridBagSizer来放置两个图片和两个文本控件 GBSizer = wx.GridBagSizer(vgap=5, hgap=5) GBSizer.Add(dLbl, pos=(0, 0), span=(1, 3), flag=0) GBSizer.Add(sb1, pos=(1, 0), flag=0) GBSizer.Add(sb2, pos=(1, 1), flag=0) GBSizer.Add(tLbl, pos=(1, 2), flag=0) return GBSizer def LayoutDaysWeather(self, daysInfo): """使用竖直方向的StaticBoxSizer放置未来4天的天气信息""" sbox = wx.StaticBox(self.panel, -1, u"未来4日天气") sbsizer = wx.StaticBoxSizer(sbox, wx.VERTICAL) for dayInfo in daysInfo: sz = self.LayoutDayWeather(dayInfo) sbsizer.Add(sz, flag=wx.EXPAND) return sbsizer def LayoutPanel(self, head, days): """使用竖直方向的BoxSizer放置窗口上半部分内容和下半部分内容""" bsizer = wx.BoxSizer(wx.VERTICAL) bsizer.Add(head, flag=wx.EXPAND) bsizer.Add(days, flag=wx.EXPAND) return bsizer if __name__ == "__main__": #南京天气 wInfo = WeatherInfo('http://wap.weather.com.cn/wap/weather/101190101.shtml').getWeather() wTitle = wInfo[0] #南京天气预报 wTime = wInfo[1] #发布时间 todayInfo = wInfo[2] #今日天气信息 daysInfo = wInfo[3:7] #未来4天天气信息 app = MyApp(wTitle, wTime) #应用程序类的一个实例 app.MainLoop() #进入主循环上述程序实现了图形用户界面,当然目前还没实现3个按钮的功能,这将在下一篇中来实现。为了将天气预报信息传递给程序界面框架,我把 第一篇中抓取天气信息的程序做了一些修改,主要是增加了天气图片信息,以及将抓取到的天气信息存在一个列表里方便调用,大家可以下载附件自行查看。程序中用到的天气图标文件也在附件中。