在上一篇:
打造自己的天气预报之(四)——实现按钮功能
中,我介绍了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文件就和操纵字典类型一样,非常好用。如作者自己所介绍的
我的设计是,关闭设置窗口的时候,自动保存定时信息。为此,就要绑定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,转载请注明出处。