`

打造自己的天气预报之(五)——实现按钮功能之设置窗口

阅读更多

在上一篇:

打造自己的天气预报之(四)——实现按钮功能

中,我介绍了wxPython的事件处理机制、如何绑定按钮事件,并且实现了“更新”按钮的功能。在本篇中,我将会实现“设置”按钮的功能。“设置”按钮的功能比较多,实现起来比较复杂,而且如何使“设置”面板即美观又方便,考虑过很多种方法,最终确定了一种自认为比较简洁方便的方案。本篇还涉及到wx.ListCtrl(列表)控件的使用、弹出菜单、SQLite数据库等方面的知识。这些我也是边学边用,现学现卖,不足之处大家勿喷~本文出自三思之旅博客http://think3t.iteye.com,转载请注明出处。

首先,在上一篇中,我们已经给“设置”按钮绑定了处理器方法。

self.Bind(wx.EVT_BUTTON,self.OnConfig,self.setupBtn)    #设置按钮绑定事件处理器

def OnConfig(self,event): 
    '''处理器方法'''
    cfgFrame=CfgFrame(self) #打开配置窗口  
    cfgFrame.Show(True)     #显示配置窗口  

 我的设计是,点击“设置”按钮,打开“设置”窗口。先给大家看下设置窗口实际效果,再一步步来实现它。

设置窗口

先说下整体布局:整体是个垂直方向的BoxSizer,共有3行,第一行是一个StaticText控件,显示“当前用户”4个字;第二行是一个列表控件(ListCtrl),列出了当前所有用户的信息;第三行是一个水平方向StaticBoxSizer,就是“定时发送”复选框(CheckBox)、3个下拉框(Choice)表示时分秒以及2个StaitcText显示两个冒号。下图我的把3行内容分别用红框圈住,看起来更加直观。至于如何实现这种布局,我就不详说了,前面的篇幅里有。

设置窗口布局

从图中我们还可以看到,列表中还有右键菜单,有3个菜单项,分别是“增加”,“删除”,“设为主城市”,功能分别是增加用户、删除用户、把所选用户的城市设为主城市。主城市的作用其实就是软件运行时显示主城市的天气信息。主城市用户项在列表中的文字颜色是红色,以区别于其他的用户项。StaticText控件很简单,前边也用到过,这里不再赘述。下面重点说一下之前没用过的控件:列表、复选框、下拉框。

首先是列表,其构造方法是

wx.ListCtrl(parent, id, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.LC_REPORT, validator=wx.DefaultValidator, name='listCtrl')

各参数的意义和之前的控件一样,这里说一下style。一共有4种类型的列表,分别是:

  • wx.LC_ICON:图标模式,使用大图标
  • wx.LC_LIST:列表模式
  • wx.LC_REPORT:报告模式
  • wx.LC_SMALL_ICON:图标模式,使用小图标

此处我们使用的是报告模式,报告模式还有3种显示样式:

  • wx.LC_HRULES:在列表的行与行间显示网格线(水平分隔线)
  • wx.LC_NO_HEADER:不显示列标题
  • wx.LC_VRULES:显示列与列之间的网格线(竖直分隔线)

本程序中我们构造的是显示水平、垂直分隔线并显示列标题的报告模式的列表,构造方法如下:

# 显示水平、竖直分隔线的报告模式的列表
self.list = wx.ListCtrl(self.panel, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)

构造了列表之后就要往列表里面插入内容。首先创建4个列,依次为序号、邮箱、城市、备注,然后依次插入用户信息即行,每行是一个用户的信息。创建列的语法是

InsertColumn(col, heading, format=wx.LIST_FORMAT_LEFT, width=-1) 

其中col是新列的索引,最左边是0,接下来依次为1、2、3、……;heading是列标题;format控制列中文本的对齐方式,取值有:wx.LIST_FORMAT_CENTER、wx.LIST_FORMAT_LEFT和wx.LIST_FORMAT_RIGHT;width是列的初始显示宽度(以像素为单件),默认-1表示根据内容自动控制。本程序中创建的4个列的具体实现是:

# 创建4个列
self.list.InsertColumn(0, u'序号', format=wx.LIST_FORMAT_LEFT, width=50)
self.list.InsertColumn(1, u'邮箱', format=wx.LIST_FORMAT_LEFT, width=150)
self.list.InsertColumn(2, u'城市', format=wx.LIST_FORMAT_LEFT)
self.list.InsertColumn(3, u'备注', format=wx.LIST_FORMAT_LEFT)

要增加一个新行,使用InsertItem()这类的一种方法。具体所用的方法依赖于你所插入的项目的类型。如果你仅仅插入一个字符串到列表中,使用InsertStringItem(index, label),其中的index是要插入并显示新项目的行的索引,label是显示的字符串,这里实际上只是插入了一个新行并把第一列的文本设置为label。要在另外的列中设置字符串,可以使用方法SetStringItem(index, col,label,imageId=-1)。参数index和col是你要设置的单元格的行和列的索引。你可以设定col为0来设置第一列,但是参数index必须对应列表控件中已有的行——换句话说,这个方法只能对已有的行使用。参数label是显示在单元格中文本,参数imageId是图像列表中的索引(如果你想在单元格中显示一个图像的话可以设置这个参数,我们不需要显示图像,所以不用管这个参数)。本文出自三思之旅博客http://think3t.iteye.com,转载请注明出处。

本程序中插入用户信息是这样实现的:先从SQLite数据库中获取用户信息,然后每个用户信息增加一行,并依次把第一列设置为行序号,然后把具体用户信息插入其他列。具体代码如下:

# 获取用户信息,得到一个三元组列表
userInfos = self.searcher.getUserInfo()
# 从第一行开始,第一行的索引为0
row = 0
# 依次插入用户信息
for (mail, city, note) in userInfos:
    # 每个用户信息增加一个行,并把首列显示为行序号,从1开始
    self.list.InsertStringItem(row, str(row + 1))
    # 再把接下来3列依次插入用户信息(邮箱、城市、备注)
    self.list.SetStringItem(row, 1, mail)
    self.list.SetStringItem(row, 2, city)
    self.list.SetStringItem(row, 3, note)
    # 如果当前用户是主城市,则显示为红色,并记录行号
    if self.searcher.isMainCity(mail, city):
        self.list.SetItemTextColour(row, wx.RED)
        self.main_city = row
    # 下一用户行号加1
    row += 1
# 全部用户信息插入完成后记录最后一行索引
self.totaluser = row
# 第二列(邮箱)根据内容自动调整列宽
self.list.SetColumnWidth(1, wx.LIST_AUTOSIZE)
# 默认会选中第一行内容,此处我们使第一行不被选中
self.list.SetItemState(0, 0, wx.LIST_STATE_SELECTED)

代码中用到了searcher这个类,这个类是我自己写的从数据库中读写用户信息的类,以后会有介绍。

现在列表中有信息了。接下来我们给列表增加右键菜单,首先绑定弹出菜单事件处理器。

# 给列表增加右键菜单
self.list.Bind(wx.EVT_CONTEXT_MENU, self.OnShowPopup)

该右键菜单有3个菜单项,“增加”、“删除”、“设为主城市”,其中“删除”、“设为主城市”我希望在选中某行的时候才显示出来。事件处理器代码如下:

def OnShowPopup(self, event):
    # 创建一介菜单
    self.popupmenu = wx.Menu()
    # 添加“增加”菜单项,这个菜单项一直都有
    item_add = self.popupmenu.Append(-1, u'增加')
    # “增加”菜单项绑定处理器方法
    self.list.Bind(wx.EVT_MENU, self.OnAdd, item_add)
    # 当选中某行时才显示“删除”、“设置主城市”菜单
    if self.list.GetFirstSelected() != -1:
        # 添加“删除”菜单项
        item_del = self.popupmenu.Append(-1, u'删除')
        # “删除”菜单项绑定处理器方法
        self.list.Bind(wx.EVT_MENU, self.OnDel, item_del)
        # 添加“设为主城市”菜单项
        item_setMain = self.popupmenu.Append(-1, u'设为主城市')
        # “设为主城市”菜单项绑定处理器方法
        self.list.Bind(wx.EVT_MENU, self.OnSetMain, item_setMain)
    # 获取事件发生的坐标,即点击右键的地方,这个坐标是相对于整个屏幕来计算的
    pos = event.GetPosition()
    # 把坐标转换为以本程序界面为基准的坐标
    pos = self.list.ScreenToClient(pos)
    # 在点击右键的地方显示右键菜单

接下来实现各菜单项的功能。单击“增加”菜单项打开一个增加用户对话框,用户根据提示填写相关信息后点击确实即往数据库中增加用户信息,并将新用户信息显示在列表中,点击取消则直接回到“设置”界面。增加用户对话框如下图所示:

这个对话框是我自己用Frame实现的,整体布局是一个竖直方向的BoxSizer,一共3行,每行都是一个水平方向的BoxSizer。说到这里不得不赞一句,wxPython的BoxSizer实在是太好用了!一般几个嵌套的BoxSizer就可以实现比较复杂的布局,因此推荐布局首选BoxSizer。此处用到的新的控件是下拉框(Choice)和复选框(CheckBox),重点说一下。本文出自三思之旅博客http://think3t.iteye.com,转载请注明出处。

下拉框的构造方法为:

wx.Choice(parent, id, pos=wx.DefaultPosition, size=wx.DefaultSize, choices=None, style=0, validator=wx.DefaultValidator, name=”choice”)

参数choices是个列表,用于初始化下拉框的各项。wx.Choice没有专门的样式,但是它有独特的命令事件:EVT_CHOICE。常用的方法有GetSelection():返回当前选中项的索引;GetStringSelection():返回当前选中项的文本内容;SetSelection(n):设置索引为n的项被选中,不会触发EVT_CHOICE事件。

我已经把全国各省市县区的相关信息存入数据库中了,实现“增加用户”对话框中的三个下拉框(依次为省、市、区/县时,先从数据库中导出相关信息,并生成省、市、区/县的列表,再用相应的列表初始化相应的下拉框。省、市的下拉框都绑定了EVT_CHOICE事件,这样更改省份的时候,市和区/县的下拉框内容随之更改;更改市的时候,区/县的下拉框内容随之更改。默认显示城市为北京。具体代码如下:

# 从数据库中导出省、市、县信息并存在3个列表中
provList = self.searcher.listProvs()
cityList = self.searcher.listCityOfProv(u'北京')
zoonList = self.searcher.listZoonOfCity(u'北京', u'北京')        
# 创建省份下拉框,默认显示北京,绑定EVT_CHOICE事件处理器
self.provCho = wx.Choice(self.panel, -1, choices=provList)
self.provCho.SetSelection(0)
self.Bind(wx.EVT_CHOICE, self.OnProvSel, self.provCho)
# 创建市下拉框,默认显示北京,绑定EVT_CHOICE事件处理器
self.cityCho = wx.Choice(self.panel, -1, choices=cityList)
self.cityCho.SetSelection(0)
self.Bind(wx.EVT_CHOICE, self.OnCitySel, self.cityCho)
# 创建区/县下拉框,默认显示北京
self.zoonCho = wx.Choice(self.panel, -1, choices=zoonList)
self.zoonCho.SetSelection(0)

def OnProvSel(self, event):
    '''省份下拉框EVT_CHOICE事件处理器'''
    prov = event.GetString()  # 获取当前选中的省份名称
    cityList = self.searcher.listCityOfProv(prov)  # 获取当前省份的市信息列表
    self.cityCho.SetItems(cityList)  # 更新市下拉框内容
    self.cityCho.SetSelection(0)  # 默认显示省会城市
    city = self.cityCho.GetStringSelection()  # 获取当前市的名称
    zoonList = self.searcher.listZoonOfCity(prov, city)  # 获取当前市的区/县信息列表
    self.zoonCho.SetItems(zoonList)  # 更新区/县下拉框内容
    self.zoonCho.SetSelection(0)  # 默认显示市区
    
def OnCitySel(self, event):
    '''市下拉框EVT_CHOICE事件处理器'''        
    prov = self.provCho.GetStringSelection()  # 获取当前选中的省份名称
    city = event.GetString()  # 获取当前市的名称
    zoonList = self.searcher.listZoonOfCity(prov, city)  # 获取当前市的区/县信息列表
    self.zoonCho.SetItems(zoonList)  # 更新区/县下拉框内容
    self.zoonCho.SetSelection(0)  # 默认显示市区

接下来说一下复选框(CheckBox)。CheckBox的构造方法为:

wx.CheckBox(parent, id, label, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, name=”checkBox”)

label参数是复选框的标签文本。复选框没有样式标记,但是它们产生属于自己的独一无二的命令事件:EVT_CHECKBOX。wx.CheckBox的开关状态可以使用GetValue()和SetValue(state)方法来访问,并且其值是一个布尔值。IsChecked()方法等同于GetValue()方法,只是为了让代码看起来更易明白。本程序中实现复选框的代码为:

self.mcityChk = wx.CheckBox(self.panel, -1, label=u'主城市')   

接下来讲一下”确定”和“取消”两个按钮的功能实现。“确实”按钮的功能即将用户信息添加进数据库并显示在列表中,其中还对邮箱地址格式进行了简单判断,邮箱为空或格式不合法不会添加用户信息并提示用户重新输入。“取消”按钮功能更简单,直接关闭“增加用户”对话框即可。具体代码如下:

self.OKBtn = wx.Button(self.panel, wx.ID_OK, label=u'确定')
self.cancelBtn = wx.Button(self.panel, wx.ID_CANCEL, label=u'取消')

self.Bind(wx.EVT_BUTTON, self.OnOK, self.OKBtn)
self.Bind(wx.EVT_BUTTON, self.OnCancel, self.cancelBtn)

def OnOK(self, event):
    '''确定按钮功能实现'''
    prov = self.provCho.GetStringSelection()  # 获取当前选中的省份名称
    city = self.cityCho.GetStringSelection()  # 获取当前市的名称
    zoon = self.zoonCho.GetStringSelection()  # 获取当前区/县的名称
    cityCode = self.searcher.getCityCode(prov, city, zoon)  # 从数据库中获取当前选中的城市代码
    mailAddr = self.mailTxt.GetValue()  # 获取邮箱地址
    # 判断邮箱地址格式是否合法
    if mailAddr == '':  # 如果邮箱为空,则状态栏提示'请输入邮箱地址!'
        self.stBar.SetStatusText(u'请输入邮箱地址!')
    elif re.match(r'[\w\.]+@\w+\.\w+', mailAddr):  # 邮箱地址合法,则执行添加用户动作            
        if self.mcityChk.IsChecked():  # 如果勾选“主城市”复选框,则新增加的用户为主城市
            self.searcher.clearMainCity()  # 数据库中只能有一个主城市,所以要先清除原主城市标记
            self.searcher.addItem(table='userInfo', values=(mailAddr, cityCode, 1, zoon))  # 把用户信息写入数据库
        else:
            self.searcher.addItem(table='userInfo', values=(mailAddr, cityCode, 0, zoon))
        self.stBar.SetStatusText(u'用户%s添加成功!' % mailAddr)  # 状态栏提示用户添加成功
    
        row = self.Parent.totaluser  # 获取原来列表中最后一行索引号
        # 将新增加的用户信息添加在列表里
        self.Parent.list.InsertStringItem(row, str(row + 1))
        self.Parent.list.SetStringItem(row, 1, mailAddr)
        self.Parent.list.SetStringItem(row, 2, cityCode)
        self.Parent.list.SetStringItem(row, 3, zoon)
        if self.mcityChk.IsChecked():  # 如果勾选“主城市”复选框,则新增加的用户为主城市
            # 将原主城市文字颜色设置为黑色
            self.Parent.list.SetItemTextColour(self.Parent.main_city, wx.BLACK)
            # 将新主城市文字颜色设置为红色
            self.Parent.list.SetItemTextColour(row, wx.RED)
            # 记录新主城市所在行
            self.Parent.main_city = row
        # 将新增加的行设置为选中状态
        currentSelected = self.Parent.list.GetFirstSelected()
        if currentSelected != -1:
            self.Parent.list.SetItemState(currentSelected, 0, wx.LIST_STATE_SELECTED)
        self.Parent.list.SetItemState(row, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED)
        self.Parent.totaluser += 1  # 当前用户总数加1        
        self.Close()  # 操作完成,关闭“增加用户”对话框
    else:  # 如果邮箱地址格式合法,则状态栏提示'邮箱地址格式错误!'
        self.stBar.SetStatusText(u'邮箱地址格式错误!')
    
def OnCancel(self, event):
    '''点击取消按钮则关闭对话框'''
    self.Close()

至此为止,列表的“增加”菜单项的功能已经实现了。接下来实现”删除“菜单项的功能。点击”删除“菜单项,要执行以下动作:获取选中的行的用户信息,然后从数据库中删除该用户信息;还要从列表中删除相应的行,同时为保持行序号的连续性,被删除行以下的行序号要重新调整;此外,如果被删除的行是主城市,还要重新指定主城市,我这里把第一行设置为主城市。同时,由于删除操作不可撤销,所以弹出警告框,给用户确认。具体代码如下:

def OnDel(self, event):
    '''“删除”菜单项的功能实现'''
    # 弹出警告框,供用户确认
    retCode = wx.MessageBox(u'确定要删除该用户?\n请注意:该操作不可撤销!', u'请确认删除', wx.YES_NO | wx.ICON_QUESTION)
    # 用户点击“是”才执行删除动作
    if retCode == wx.YES:
        row = self.list.GetFirstSelected()  # 获取当前选中的行索引
        mail = self.list.GetItemText(row, 1)  # 从选中行中取得邮箱信息
        city = self.list.GetItemText(row, 2)  # 从选中行中取得城市信息
        self.list.DeleteItem(row)  # 删除列表中的行
        if self.searcher.isMainCity(mail, city):  # 如果被删除的是主城市,则设置删除之后列表第一行为主城市
            self.list.SetItemTextColour(0, wx.RED)
            self.searcher.setMainCity(self.list.GetItemText(0, 1), self.list.GetItemText(0, 2))
        self.searcher.delItem(mail, city)  # 清除数据库中相应信息
        self.totaluser -= 1  # 总用户数量减1
        for i in range(row, self.totaluser):  # 重新调整被删除行以后的行序号
            self.list.SetItemText(i, unicode(i + 1))

最后还剩下“设为主城市”菜单项的功能实现。这个其实就更简单了,无非就是删除原主城市标记,设置新主城市标记。当然,数据库和列表都要进行处理。具体代码如下:

def OnSetMain(self, event):
    self.searcher.clearMainCity()  # 清除原主城市
    row = self.list.GetFirstSelected()  # 获取选中的行索引
    mail = self.list.GetItemText(row, 1)  # 获取邮箱地址
    city = self.list.GetItemText(row, 2)  # 获取城市信息
    self.searcher.setMainCity(mail, city)  # 设置新的主城市
    self.list.SetItemTextColour(self.main_city, wx.BLACK)  # 原主城市行字体回归黑色
    self.list.SetItemTextColour(row, wx.RED)  # 新主城市字体设置为红色
    self.main_city = row  # 更新主城市行号全局变量

至此,列表部分的相关功能都已经完成。目前还剩下最下边设定定时发送部分的功能。这一部分也很简单,一个复选框,3个下拉框。复选框、下拉框前边都已介绍过,这里也无需详述。复选框绑定了wx.EVT_CHECKBOX事件,当勾选复选框时,3个下拉框可用;否则不可用。

self.setTimeChk = wx.CheckBox(self.panel, -1, label=u'定时发送')
self.Bind(wx.EVT_CHECKBOX, self.OnSetTimeChk, self.setTimeChk)

def OnSetTimeChk(self, event):
    # 勾选复选框时,3个下拉框可用
    if self.setTimeChk.IsChecked():
        self.hourCho.Enable()
        self.minuteCho.Enable()
        self.secondCho.Enable()
    # 未勾选复选框时,3个下拉框不可用
    else:
        self.hourCho.Disable()
        self.minuteCho.Disable()
        self.secondCho.Disable()

这样,整个设置窗口的内容基本介绍完成了。这里还有个问题,如何保存定时信息?我打算使用cfg.ini文件保存定时信息。python自带读写ini文件的库,不过不太好用,我使用dict4ini库(项目主页:http://code.google.com/p/dict4ini/),用这个库操纵ini文件就和操纵字典类型一样,非常好用。如作者自己所介绍的

limodou 写道
''This module is used to process ini format configuration file. It acts just like a dict, but you can also access it's sections and options with attribute syntax, just like x.test.''

我的设计是,关闭设置窗口的时候,自动保存定时信息。为此,就要绑定CfgFrame的wx.EVT_CLOSE事件。代码很简单,如下:

self.Bind(wx.EVT_CLOSE, self.OnClose)  # 配置窗口关闭时保存定时信息

def OnClose(self, event):
    '''关闭配置窗口时都会保存定时信息'''
    if self.setTimeChk.IsChecked():  # 如果勾选定时发送复选框,就将Timer设置为1
        self.myIni.Config.Timer = 1
    else:  # 如果未勾选定时发送复选框,就将Timer设置为0
        self.myIni.Config.Timer = 0
    # 保存时分秒信息
    self.myIni.Config.Hour = self.hourCho.GetSelection()
    self.myIni.Config.Minute = self.minuteCho.GetSelection()
    self.myIni.Config.Second = self.secondCho.GetSelection()
    # 保存ini文件
    self.myIni.save()
    # 销势设置窗口
    self.Destroy()

cfg.ini文件的内容如下:

[Config]
Timer = 0
Hour = 8
Minute = 5
Second = 0

Timer=0不启用定时,Timer=1启用定时发送。有了cfg.ini文件,定时信息就可以保存起来。当然,每次程序运行时都要读取这个定时信息,并根据定时信息设置定时发送复选框的状态和时分秒下拉框的值。

# 从cfg.ini文件中读取定时信息
isTimerOn = self.myIni.Config.get('Timer', 0)
timer_hour = self.myIni.Config.get('Hour', 8)
timer_minute = self.myIni.Config.get('Minute', 5)
timer_second = self.myIni.Config.get('Second', 0)
# 根据定时信息设置时分秒下拉框的值
self.hourCho.SetSelection(timer_hour)
self.minuteCho.SetSelection(timer_minute)
self.secondCho.SetSelection(timer_second)
# 根据Timer的值设置定时发送复选框的状态以及时分秒下拉框是否可用
if isTimerOn:
    self.setTimeChk.SetValue(True)
    self.hourCho.Enable()
    self.minuteCho.Enable()
    self.secondCho.Enable()
else:
    self.setTimeChk.SetValue(False)
    self.hourCho.Disable()
    self.minuteCho.Disable()
    self.secondCho.Disable()

到此为止,本篇文章可以结束了。本篇中用到了searcher这个我自己写的用来读写SQLite数据库的类,下一篇中我会进行介绍。还有虽然现在已经可以保存定时信息,但程序还不能定时发送,这一部分的功能也还没实现,我会在以后的篇幅中实现它。请大家继续关注~ 本文出自三思之旅博客http://think3t.iteye.com,转载请注明出处。

  • 大小: 34.1 KB
  • 大小: 102.9 KB
  • 大小: 20.5 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics