经过前期的设计,我们已经可以从网上获取天气信息,并且可以使用邮件发送。今天我们要给程序增加一个界面,毕竟并不是所有人都习惯命令行操作。先给大家看一下最终效果
这是在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个按钮的功能,这将在下一篇中来实现。为了将天气预报信息传递给程序界面框架,我把
第一篇中抓取天气信息的程序做了一些修改,主要是增加了天气图片信息,以及将抓取到的天气信息存在一个列表里方便调用,大家可以下载附件自行查看。程序中用到的天气图标文件也在附件中。