项目前的准备工作
cnblogs采集工具的相关介绍:
项目名称: CnblogsFan
简介: 用来采集cnblogs随笔的一个开源工具。
开发语言: Python
图形库: WxPython
开发环境: PyScripter
版本控制: Git
项目托管: GitHub
项目地址: https://github.com/mrwid/CnblogsFan
适用平台: Windows/Linux
作者: Wid
项目类型: 开源
开源协议: GNU GPL
对项目需求的分析
在对需求进行分析之前我们先来再次熟悉下一个完整项目的开发流程, 如图所示:
这里简略的图示了一下软件开发的一般过程, 对于详细流程查阅相关书籍或者到互联网上查看。
现在我们开始进入了这个项目的第一步, 需求调研分析。
一、确定用户层次
由于不同的用户对电脑的使用熟练程度不同, 我们可以把用户分为5个层次:
- "小白"用户,刚接触电脑, 对电脑没有任何基础, 仅能完成对电脑的基本操作。
- 普通用户,对电脑有一定了解, 能够熟练的使用一些基本的常用软件, 能够顺利的完成对软件的安装与卸载。
- 高级用户,能够熟练的使用电脑, 使用过一些较为专业的处理软件。
- 专业用户 对电脑有全面的了解, 自身的职业与电脑操作有关或接受过相关的专业培训, 能够熟练的使用办公软件。
- IT从业者及其以上
对电脑的操作毫无压力
在设计软件时必须要考虑软件面向的用户属于哪一层次, 利于我们队软件开发的过程中找准侧重点, 对于普通用户我们往往可以在软件的操作方式以及界面上小做文章, 而对于相对于对电脑使用比较熟练的高级用户, 我们则更应该在软件的功能实现上多费一些笔墨, 当然, 在条件允许的情况下我们自然要选择功能更强, 界面更美观, 使用更简单!
对于CnblogsFan这个项目针对的用户群, 对电脑都有较为熟练的操作, 属于高级用户以上, 因此在界面的设计上我们可以使用一些较为专业的术语, 增强软件的专业性。
二、用户的需求
正如项目简介中那样, CnblogsFan目的是为了方便采集收藏博客园中的一些随笔"。采集随笔"便是整个项目要实现的功能范围, 具体如何采集是编码人员要解决的事情, 但是现在Wid是一个人在做这个项目, 因此Wid只要既充当用户, 也要充当软件分析设计人员以及编码人员。
1>. 用户的角度:
从用户的角度来考虑: 如果我想要对博客园随笔进行采集, 我会希望有更多更灵活的采集方式供我选择, 我所希望的采集方式如下:
1. 蜘蛛模式
模拟蜘蛛对cnblogs随笔的进行大范围的采集;
2. 限制随笔发表时间
可以指定一个时间段, 对于一些较旧的随笔我不想采集;
3. 指定采集某个用户
我只想采集某些用户的随笔;
4. 过滤一些用户
我不想采集某些用户的随笔;
5. 只采集首页/精华/候选区的随笔
我只对这些随笔有兴趣;
6. 其他想要的功能还没想好, 想好了随时通知你们; ( Wid : -_-||| )
除了这些基本功能外, 你们再添加一些尽可能灵活的采集方式。
2>. 博客园的角度:
1>. 采集时必须保留博客作者的相关信息。
2>. 在每篇采集到的随笔中要注明"该文章通过CnblogsFan博客园随笔采集工具采集自XXX用户的博客, 原文地址:http://"一句。
3>. 其他想要的功能还没想好, 想好了随时通知你们; ( Wid : ...... )
三、分析需求
通过用户与博客园提交的需求可以两点基本需求:
①. 软件要有灵活的采集方式;
②. 要保留作者的相关信息。
除此之外还有一个不确定因素, 就是需求可能会随时变动.(o(╯□╰)o)
概要设计
一、需求规定
(1). 功能需求规定
1>. 能够模拟蜘蛛方式采集博客园中的随笔;
2>. 能够采集指定用户的随笔;
3>. 能够采集首页/精华/候选中的随笔;
4>. 能够采集推荐博客中用户的随笔;
5>. 对采集有一定的过滤机制, 过滤需求如下:
①. 仅采集某个日期时间段内发表的随笔;
②. 能够根据输入的关键字进行采集;
③. 能够根据输入过滤掉不感兴趣的随笔;
④. 对于内容过短的随笔不采集;
6>. 必须保留随笔作者的相关信息;
7>. 能够有相关的采集日志。
(2). 采集质量规定
1>. 能够采集完整的随笔内容;
2>. 尽可能完整的保留随笔格式;
(3). 运行环境
能够在Windows/Linux系统上运行。
二、软件模块设计
如图所示:
三、需求与模块间的关系
如表格所示:
UI模块 | 采集模块 | 分析模块 | 声明模块 | 保存模块 | 过滤模块 | 配置管理 | 初始化 | |
需求1 | √ | |||||||
需求2 | √ | |||||||
需求3 | √ | |||||||
需求4 | √ | |||||||
需求5 | √ | |||||||
需求6 | √ | |||||||
需求7 | √ |
所设计的模块满足功能需求。
四、项目文件的组织
对于项目文件的组织, 如图所示:
完成了概要设计后就是对各模块的详细设计, 详细设计内容将在下一篇随笔中进行叙述
详细设计
在完成对项目的概述设计后, 就可以根据概述设计的内容对项目各个组成模块逐步细化, 也就是我们所说的详细设计。详细设计在项目开发的整个步骤中是十分重要的一步, 好的设计是项目成功的前提, 详细设计直接决定着软件的质量以及软件在以后维护过程中的难易程度。更多关于详细设计的介绍请点击这里。
同概述设计中介绍的那样, 由于项目的侧重点在对功能实现的分析上, 所以这里的详细设计也是属于简略版, 不涉及某个功能的具体算法, 而在规范的详细设计当中, 这些都是必要的, 对于功能模块的具体算法将在下一步的编码过程中配合代码以随笔的形式介绍。
在这里我们主要介绍项目各模块的结构、工作流程, 以及编码过程中标识符命名协议、注释协议以及开发文档的描述协议, 同时, 对开发过程中可能使用到的参考资料进行说明。
一、模块的结构
1>. UI模块
UI(User Interface, 用户界面), 是用户最能直接感受到的模块, UI模块设计的质量决定着软件操作难易程度, 以及用户对该软件的第一印象。在本项目中, 对UI模块的初步设计如下:
1>. 采用单文档界面(SDI, Single Document Interface);
2>. 允许调整窗口大小, 但有最小尺寸限制;
3>. 能将窗口最小化到系统托盘。
UI模块结构图示如下:
关于CnblogsFan主界面的初步设计草图如下:
2>. 随笔采集模块
随笔采集模块是对网页信息收集的过程, 是从整体角度来说, 整个随笔采集模块是对随笔过滤模块、信息收集模块、数据分析模块、版权声明模块以及数据存储模块的一个组合。 下面对一个采集最小单位进行图示描述:
3>. 数据分析处理模块
数据分析处理模块用来处理网页上的随笔信息, 对随笔内容进行整理加工, 使其满足需求规定中的要求, 数据分析处理的流程为, 从符合采集的任务队列中获取网页 --> 分析随笔正文格式以及相关资源的链接 --> 得到采集结果 --> 调用版权声明模块对采集结果进行版权声明 --> 写入本地。相关图示如下:
4>. 配置管理模块
关于CnblogsFan配置管理的配置存储将使用Python自带的数据存储模块进行, 配置管理的相关图示如下:
5>. 初始化模块
初始化模块用于通过配置管理模块读取软件配置信息完成对软件的初始化, 这个过程相对较于简单, 不再进行图示说明。
二、标识符命名协议
1>. 局部变量命名
采用*类Java*式风格命名法:首单词小写,其余单词首字母大写。
> editBlack
badClock
isFile
2>. 全局变量命名
采用去掉类型前缀的*Windows式*命名法:单词首字母大写。
> AppName
TimerCount
IsGameStarted
3>. 成员变量名
①. 静态成员变量
采用**全局变量命名**的方式。
②. 普通成员变量
采用**局部变量命名**的方式,开头加一个下划线。
> _editBlack
_isFile
_give
4>. 控件变量名
控件变量名采用控件缩写作为前缀的命名法。
各个控件的缩写:
控件 | 缩写 |
按钮 | btn |
文本框 | txt |
标签 | lbl |
列表框 | lst |
组合框 | cbo |
复选框 | chk |
单选框 | rdo |
列表控件 | lstctl |
树形控件 | tree |
框 | box |
进度条 | gue |
动画控件 | ani |
5>. 方法/函数命名
①. 独立函数
采用*传统C风格*命名。
> get_files()
set_global_text()
②. 成员方法
采用*类Java式*命名。
> getFiles()
setGlobalText()
③. 事件响应方法
同成员方法相同,前面加上'on'。
> onClick()
onDocumentComplete()
6>. 类型名
采用单词首字母大写式命名。
> class DirectUI:
class AbstractBase:
7>. 模块名/文件名
采用单词首字母大写式命名。
> SearchingDlg
KumquatRoot
MainDlg
三、注释协议
由于是对项目实现过程进行分析, 所以关于注释部分尽可能做到详细, 在规范上遵循一般的注释规范, 这里就不在重复。
四、开发文档描述协议
遵循 Markdown 语法。
五、可能使用到的参考资料
1>. Python基础教程(第2版)
作者 : 赫特兰(MagnusLieHetland)
2>. wxPython in Action
作者:Harri Pasanen、Robin Dunn
3>. Python v2.6 documentation
4>. wxPython reference
经过详细设计的步骤后, 下一步就是进入项目的编码阶段了, 欢迎继续关注CnblogsFan开源项目的最新进展。
实现主界面布局
按照项目实现的一般流程, 在对项目完成详细设计后的下一步就是进入编码阶段了。 由于目前依然是一个人在在每天得空闲时间负责这个小项目, 在编码上, wid采用的是由易到难, 逐步深入的方式。 所以, 今天第一步要实现的就是在主界面的布局。
在继续阅读以下随笔之前, 你应该具备的知识:
1>. Python的基本语法
2>. 能够使用WxPython创建一个窗口
如果你还没有接触过Python语言并且想要了解它, 点击这里;
我们知道, 在WxPython中, 可以使用尺寸器sizer对窗口控件进行智能布局, 这是在WxPython中实现对窗口控件布局管理的常用方法, 但是wid最近在学习C语言Windows程序设计, 不知在这里有没有能够对窗口控件实现智能布局的API, 经过一番考虑后, 所以在这个小项目中, 决定不使用sizer对窗口进行智能布局, 而是根据取得上一个控件的RECT结构对下一个控件进行布局, 对于这种布局管理, 首先有个弊端, 在进行调整窗口大小时, 窗口中的控件位置以及大小不能根据窗口的大小而变化, 如果要实现像sizer尺寸器中那样能够根据窗口的大小自动调整控件位置以及大小的话, 可以根据一个wx.SIZE消息来调整控件的大小以及相对坐标。为了避免将这个弊端体现出来, 所以在窗口的样式上调整为不可调整大小的窗口。
声明: CnblogsFan中所使用的所有图标文件均来自互联网, 并遵循相关的使用协议, 关于图标的来源记录以及使用协议如下, 如果您也要使用以下图标请认真阅读:
图标来源记录及使用协议
*该项目中使用的所有图标资源均来自互联网, 通过图标搜索引擎**http://www.easyicon.cn**获取, 图标有关记录如下: ****** *文件名: CnblogsFan_Spider.png >尺寸: 48x48 >作者: Alessandro Rei >作者网站: http://www.kde-look.org/usermanager/search.php?username=mentalrey >使用协议: GPL ****** *文件名: CnblogsFan_Single.png >尺寸: 48x48 >作者: Oliver Scholtz (and others) >作者网站: http://linux.softpedia.com/developer/Oliver-Scholtz-93.html >使用协议: GPL ****** *文件名: CnblogsFan_Classify.png >尺寸: 48x48 >作者: codefisher >作者网站: http://codefisher.org >使用协议: Creative Commons (Attribution-Noncommercial-Share Alike 3.0 Unported) ****** *文件名: CnblogsFan_Setting.png >尺寸: 48x48 >作者: Pavel InFeRnODeMoN >作者网站: http://www.kde-look.org/usermanager/search.php?username=InFeRnODeMoN >使用协议: GPL ****** *文件名: ICON_CnblogsFan.ico >尺寸: 48x48 >作者: Kyo Tux >作者网站: http://kyo-tux.deviantart.com >使用协议: Creative Commons (Attribution-Noncommercial-Share Alike 3.0 Unported) |
在对UI设计部分进行介绍之前, 为了能够对设计中提到的各个控件的位置有个大致的把握, 首先预览下该代码在Windows XP下与Linux (Ubuntu)下运行的实际效果图:
一、在Windows XP下运行:
二、在Linux (Ubuntu)下运行
接下来开始开始对项目的UI设计部分进行介绍, 采用贴出完整代码的形式, 说明均在注释中。
#!/usr/bin/python #coding:utf-8 #------------------------------------------------------------------------------- # Name: CnblogsFan_MainFrame.py # Purpose: # # Author: Mr.Wid # # Created: 13-10-2012 # Copyright: (c) Mr.Wid 2012 # Licence: GNU GPL #------------------------------------------------------------------------------- import wx class MainFrame(wx.Frame): #从wx.Frame类得到继承 def __init__(self): #初始化窗口 wx.Frame.__init__( self, parent = None, #无父窗口 title = u'CnblogsFan', #窗口标题:'CnblogsFan', size = ( ( 900, 600 ) ), #窗口大小900x600 style = wx.SYSTEM_MENU|wx.CAPTION|wx.MINIMIZE_BOX|wx.CLOSE_BOX #带有最小化与最大化按钮的窗口样式 ) self.Center() #令窗口在屏幕中居中显示 #-----加载程序图标----- self.AppLogo = wx.Icon('src//ICON_CnblogsFan.ico', wx.BITMAP_TYPE_ICO) self.SetIcon(self.AppLogo) #-----创建窗口面板----- self.panel = wx.Panel(self) #-----创建状态栏----- self.userStatus = self.CreateStatusBar() self.userStatus.SetFieldsCount(4) #将状态栏分为4部分 self.userStatus.SetStatusWidths( [-1, -1, -1, -1] ) #划分比例为4等分 #-- #状态栏上待显示的文字 statusLabel = [ u' 当前状态:', u' 采集速度:', u' 采集统计:', u' 任务统计:', ] #将文字标签显示在状态栏上 for i in range( len(statusLabel) ): self.userStatus.SetStatusText( statusLabel[i], i ) #-----创建菜单栏外框StaticBox----- #这个StaticBox控件为首个控件 self.groupMenuBox = wx.StaticBox( self.panel, label = u'菜单', pos = (15, 10), #在首个控件处使用绝对坐标 size = (80, 400), #框大小为80x400 ) #--以下为菜单图标在本地的文件名 #资源文件在src文件夹下, 使用代码示例时请将src资源文件夹与该.py文件放在同一目录 self.localImgSrc = [ 'CnblogsFan_Spider.png', 'CnblogsFan_Single.png', 'CnblogsFan_Classify.png', 'CnblogsFan_Setting.png', 'CnblogsFan_About.png' ] self.lstMenu = [] #菜单列表, 用来记录菜单按钮控件 menuTip = [ u'采集整个Cnblogs上的随笔.', #当鼠标放在按钮上的相关提示文字 u'采集指定博客上的随笔.', u'采集Cnblogs首页分类上的随笔.', u'设置软件的相关参数.', u'关于CnblogsFan的一些信息.' ] #菜单按钮下方的文字说明 menuLabel = [ u'蜘蛛模式', u'指定采集', u'分类采集', u'软件设置', u'关于软件' ] rect = self.groupMenuBox.Rect #获取第一个控件self.groupMenuBox的RECT结构 #x, y用来决定菜单按钮的位置 #x = 上个控件的x坐标 + (上个控件的x方向宽度 - 一个按钮的宽度) / 2, 这样按钮控件就能够在groupMenuBox框中居中显示了 #y = 上个控件在y方向上的坐标的三倍 x, y = rect[0] + ( rect[2]-48 )/2, rect[1] * 3 for i in range( len(self.localImgSrc) ): #for 循环生成按钮控件 tempImg = wx.Image( 'src//'+ self.localImgSrc[i], wx.BITMAP_TYPE_ANY ) #从本地加载图标文件 w, h = tempImg.GetSize() #获取加载到的图标尺寸 img = tempImg.Scale( w*0.8, h*0.8 ) #将图像缩放至80% self.lstMenu.append( #创建一个菜单按钮并将其加入到菜单按钮列表中 wx.BitmapButton( self.panel, bitmap = img.ConvertToBitmap(), #将缩放后的按钮图片转换为位图 pos = ( x, y ) ) ) #--为每个按钮增加标签 wx.StaticText( self.panel, label = menuLabel[i], pos = ( x, y + 50 ) #之所以令y再加50是为了能够让每个标签显示在按钮的下方, 而不是上方, 50这个值是经过测量按钮RECT结构的值得到 ) y += self.lstMenu[i].Rect[0] + 45 #--为每个按钮增加按钮提示信息 self.lstMenu[i].SetToolTipString(menuTip[i]) #在完成一个控件的创建之后下面的创建算法就同上面的了 #------创建当前采集用户信息栏----- rect = self.groupMenuBox.Rect #获取上一个控件RECT结构 self.groupBlogsUserInfoBox = wx.StaticBox( self.panel, label = u'当前所在博客博主信息', pos = ( rect[0] + rect[2]+ 20, rect[1] ), size = ( 500, 100 ) ) #--用户信息标签 adminInfoLabel = [ u'昵称:', u'园龄:', u'粉丝:', u'关注:', u'随笔:', u'文章:', u'评论:', u'地址:' ] self.lstAdminInfo = [] #当前采集用户信息列表 rect = self.groupBlogsUserInfoBox.Rect #获取self.groupBlogsUserInfoBox的RECT结构 x, y = rect[0] + 20, rect[1] + 30 for i in range(len(adminInfoLabel)): #生成标签控件 self.lstAdminInfo.append( #将标签控件增添到lstAdminInfo列表当中 wx.StaticText( self.panel, label = adminInfoLabel[i], pos = ( x, y ) ) ) x += 150 #每个用户信息标签直接间隔150个单位 if x > 450: #当放够3个标签后换行放置另外3个标签 x = rect[0] + 20 y += 20 #-----创建任务控制栏----- #用来控制在任务进行中的暂停/停止动作 rect = self.groupBlogsUserInfoBox.Rect self.groupControlBox = wx.StaticBox( #创建静态框StaticBox self.panel, label = u'任务控制', pos = ( rect[0] + rect[2]+ 20, rect[1] ), #位置在当前采集用户的标签的左侧 size = ( 230, 100 ) ) #--控制按钮 self.btnPauseContinue = wx.Button( #创建暂停按钮, 当在任务过程中按下"暂停"后, 暂停标签还要能够变成"继续" self.panel, label = u'暂停', size = ( 60, 60 ), #按钮大小 pos = ( rect[0] + rect[2]+ 50, rect[1] + 25 ) #位置 ) self.btnPauseContinue.Disable() #在未进行任务前将按钮设为不可用 rect = self.btnPauseContinue.Rect self.btnStop = wx.Button( #创建"停止"按钮, 用来中途中断任务的进行 self.panel, label = u'停止', size = ( 60, 60 ), pos = ( rect[0] + rect[2]+ 50, rect[1] ) ) self.btnStop.Disable() #按钮不可用 #-----成功采集信息栏----- #用于输出成功采集到的随笔信息 rect = self.groupBlogsUserInfoBox.Rect self.groupSucceedBox = wx.StaticBox( #静态框 self.panel, label = u'成功采集', pos = ( rect[0], rect[1] + rect[3] + 20 ), size = ( 750, 280 ) ) #--成功采集列表 rect = self.groupSucceedBox.Rect self.lstSucceedResults = wx.ListCtrl( #创建成功采集列表框 self.panel, pos = ( rect[0] + 10, rect[1] + 20 ), style = wx.LC_REPORT|wx.LC_HRULES|wx.LC_VRULES, size = ( rect[2] - 20, rect[3] - 30 ) ) w = self.lstSucceedResults.Rect[2] #获取列表框x方向宽度 self.lstSucceedResults.InsertColumn( col = 0, heading = u'随笔名称', width = w * 0.3 ) #创建是三个纵列, 分割比例为3:5:1.5, 为了美观留下0.5给竖直滚动条 self.lstSucceedResults.InsertColumn( col = 1, heading = u'来源地址', width = w * 0.5 ) self.lstSucceedResults.InsertColumn( col = 2, heading = u'发布时间', width = w * 0.15 ) #用来告知用户当前正在进行的动作 #-----当前动作信息栏----- rect = self.groupSucceedBox.Rect self.groupActionBox = wx.StaticBox( self.panel, label = u'当前动作', pos = ( rect[0], rect[1] + rect[3] + 20 ), size = ( 750, 110 ) ) #--动作输出文本框, 使用文本框进行当前动作输出 rect = self.groupActionBox.Rect self.txtFeedback = wx.TextCtrl( self.panel, size = ( rect[2] - 20, rect[3] - 30 ), pos = ( rect[0] +10, rect[1] + 20 ), style = wx.TE_MULTILINE | wx.TE_READONLY #带有竖直方向的滚动条并且将文本框设为只读模式 ) #在菜单创建栏的下方还剩一个比较小的角落, 用来作为用户反馈意见的位置 #-----意见反馈栏----- rect = self.groupMenuBox.Rect self.groupFeedbackBox = wx.StaticBox( self.panel, label = u'告诉作者', pos = ( rect[0], rect[1] + rect[3] + 20 ), size = ( rect[2], 110 ), ) #--创建意见输入文本框 rect = self.groupFeedbackBox.Rect self.txtFeedback = wx.TextCtrl( self.panel, size = ( rect[2] - 10, rect[3] - 50 ), pos = ( rect[0] + 5, rect[1] + 20 ), style = wx.TE_MULTILINE ) #--创建提交按钮 self.txtFeedback.SetMaxLength(1024) rect = self.txtFeedback.Rect self.btnFeedback = wx.Button( self.panel, label = u'提交', pos = ( rect[0], rect[1] + rect[3] + 5 ), size = (rect[2], 20) ) def test(): cnblogsFan = wx.PySimpleApp() mainFrame = MainFrame() mainFrame.Show() cnblogsFan.MainLoop() if __name__ == '__main__': test() |
实现辅助对话框
在前几篇关于CnblogsFan项目的随笔中, 有朋友希望wid能够加快下项目的进度, 并且给出了一些令wid很受用的建议, 在这里, wid向所有关注和支持CnblogsFan开源项目的朋友们表示衷心的感谢。
在继续阅读以下随笔之前, 你应该具备的知识:
1>. Python的基本语法
2>. 能够使用WxPython创建一个窗口
3>.了解WxPython中的"事件
4>.了解WxPython中控件的用法
如果你还没有接触过Python语言并且想要了解它, 点击这里;
今天介绍的是CnblogsFan参数采集第一步的对话框、设置对话框、关于软件对话框的设计, 对于参数采集第二步的对话框, 实际上是一个过滤设置的对话框, 今天由于时间问题还没有能够来得及实现, 对于这个对话框的设计将合并到下一篇关于外围功能的实现的随笔中。
一、参数采集第一步对话框
首先看下3个对话框的效果图:
同样, 在代码开头部分做下相关的声明:
#!/usr/bin/python #coding:utf-8 #------------------------------------------------------------------------------- # Name: CnblogsFan_GetArgumentsDlg.py # Purpose: # # Author: Mr.Wid # # Created: 15-10-2012 # Copyright: (c) Mr.Wid 2012 # Licence: GNU GPL #------------------------------------------------------------------------------- |
1>. 蜘蛛模式对话框
首先看一下"蜘蛛模式"对话框部分的代码:
class SpiderModeDlg(wx.Dialog): #定义一个SpiderModeDlg对话框类, 从wx.Dialog得到继承 def __init__( self, parent = None ): #parent为父窗口 wx.Dialog.__init__( self, parent = parent, title = u'蜘蛛模式', #标题 size = ( 400, 300 ) #尺寸 ) #-----cnblogs地址标签----- self.lblCnblogsUrl = wx.StaticText( #"采集地址标签" self, label = u'采集地址:', pos = ( 60, 30 ) ) rect = self.lblCnblogsUrl.Rect #上一个控件的RECT结构 self.txtCnblogsUrl = wx.TextCtrl( self, size = ( 200, -1 ), #-1, 单行的一个文本框 pos = ( rect[0] + rect[2] + 10, rect[1] - 3 ), value = u'http://www.cnblogs.com', style = wx.TE_READONLY #文本框设为只读模式 ) self.txtCnblogsUrl.Disable() 文本框不可用 #-----爬行方式选择----- rect = self.lblCnblogsUrl.Rect self.groupWorkMode = wx.StaticBox( #建立一个StaticBox静态框 self, label = u'遍历方式选择', pos = ( rect[0] - 20, rect[1] + 40 ), #计算位置 size = ( rect[0] + self.txtCnblogsUrl.Rect[2] + 50, 80 ) ) rect = self.groupWorkMode.Rect self.rdoboxWorkMode = wx.RadioBox( #建立单选按钮 self, choices = [ u'使用深度优先', u'使用广度优先' ], style = wx.RA_HORIZONTAL, ) self.rdoboxWorkMode.Position = ( rect[0] + (rect[2] - self.rdoboxWorkMode.Rect[2]) / 2 , rect[1] + (rect[3] - self.rdoboxWorkMode.Rect[3]) / 2 ) #计算坐标, 使其能够居中显示 #-----下一步按钮----- self.btnNextStep = wx.Button( #建立"下一步"按钮 self, label = u'下一步', size = (80, 50), ) self.btnNextStep.Position = ( (self.ClientRect[2] - self.btnNextStep.Rect[2]) / 2 , rect[1] + rect[3] + 50 ) |
这样, 一个自定义的对话框就完成了, 在完成之后我们暂时所做的是对下一步按钮的事件响应:
#-----事件绑定----- self.Bind( wx.EVT_BUTTON, self.OnNextStep, self.btnNextStep ) #绑定"下一步"按钮事件 #"下一步"按钮的事件响应, 销毁对话框并返回选择的遍历方式 def OnNextStep( self, evt ): self.Destroy() return self.rdoboxWorkMode.GetSelection() |
这样, 当点击下一步按钮时对话框就会销毁自身并且返回所选择的遍历方式的下标。
二、指定采集对话框该对话框自定义了个SelectUserBlogDlg对话框类, 同样是从wx.Dialog类得到的继承, 相关代码如下, 由于与"蜘蛛模式"对话框类似, 这里就不再详细注释:
View Code - SelectUserBlogDlg class SelectUserBlogDlg(wx.Dialog): def __init__( self, parent = None ): wx.Dialog.__init__( self, parent = parent, title = u'指定采集', size = (400, 300) ) #-----cnblogs地址标签----- self.lblCnblogsUrl = wx.StaticText( self, label = u'采集地址:', pos = ( 30, 30 ) ) rect = self.lblCnblogsUrl.Rect self.txtCnblogsUrl = wx.TextCtrl( self, size = ( 260, 150 ), pos = ( rect[0] + rect[2] + 10, rect[1] - 3 ), value = u'每行一个博客地址', style = wx.TE_MULTILINE ) self.tipValue = True #-----"下一步"按钮----- self.btnNextStep = wx.Button( self, label = u'下一步', size = (80, 50) ) self.btnNextStep.Position = ( (self.ClientRect[2] - self.btnNextStep.Rect[2]) / 2 , rect[1] + rect[3] + 150 ) #-----事件绑定----- #--绑定鼠标在文本框按下事件, 响应方法self.OnClearTipValue self.txtCnblogsUrl.Bind( wx.EVT_LEFT_DOWN, self.OnClearTipValue ) #--绑定"下一步"按钮方法 self.Bind( wx.EVT_BUTTON, self.OnNextStep, self.btnNextStep ) #清除文本框中的提示文字 def OnClearTipValue( self, evt ): if self.tipValue: self.txtCnblogsUrl.SetValue(u'') self.tipValue = False def OnNextStep( self, evt ): self.Destroy() return self.txtCnblogsUrl.GetValue() |
由上面的图片示例可知, 当对话框被建立时, 在多行文本输入框中有个初始提示:"每行一个博客地址":
相关的代码如下:
self.txtCnblogsUrl = wx.TextCtrl( self, size = ( 260, 150 ), pos = ( rect[0] + rect[2] + 10, rect[1] - 3 ), value = u'每行一个博客地址', style = wx.TE_MULTILINE ) |
如果我们就这样放在对话框里, 会发现一个问题, 当鼠标点击到文本框中后提示文字还在, 如果用户不手动将这几个文字去掉, 那么在该步的参数采集上获取到的就不仅仅是博客地址了, 或许有的朋友会说可以在获取之后用正则对获取的文字进行匹配, 仅获取URL, 这样当然也是可以的, 不过这样做会影响到用户对软件的体验, 通常我们所见到的都是当鼠标点击到文本输入框后里面的提示文字会立即消失, 所以我们现在也要达到这样的效果。
首先我们定义一个变量tipValue, 这个值是用来记录当前文本框中的提示文字状态的, 当提示文字还存在的时候此值为真, 当提示文字不存在的时候此值为假, 相关代码:
self.txtCnblogsUrl = wx.TextCtrl( self, size = ( 260, 150 ), pos = ( rect[0] + rect[2] + 10, rect[1] - 3 ), value = u'每行一个博客地址', style = wx.TE_MULTILINE ) self.tipValue = True #当首次被创建时值为真 |
如何才能知道鼠标已经进入到文本框中, 我们可以通过捕获鼠标消息, 当鼠标左键在文本框中按下时触发该消息。
这样, 我们把鼠标左键被按下的消息绑定在文本框self.txtCnblogsUrl中:
#-----事件绑定----- #--绑定鼠标在文本框按下事件, 响应方法self.OnClearTipValue self.txtCnblogsUrl.Bind( wx.EVT_LEFT_DOWN, self.OnClearTipValue ) |
通过绑定的事件可以看出, 当鼠标左键被按下的消息触发后, 使用OnClearTipValue方法进行处理, 处理方法:
#清除文本框中的提示文字 def OnClearTipValue( self, evt ): if self.tipValue: #当提示存在时将提示置空 self.txtCnblogsUrl.SetValue(u'') self.tipValue = False |
根据self.tipValue判断提示文字是否存在, 当存在时将其置空, 当提示被清空后将值再设为False, 避免用户再次点击鼠标左键时重复将文本框内容置空。这样, 当鼠标点击文本框中时, 提示文字被清空的效果就出来了。
3. 分类采集对话框
分类采集对话框的第一步对话框主要是通过一个for循环生成一个复选框, 在返回值方面还未做相应的处理, 仅仅是在点击"下一步"时将自身销毁, 对于返回值这块代码将实现UI的方法中介绍。这里仅仅贴出相关的代码。
View Code - UseClassificationDlg class UseClassificationDlg(wx.Dialog): def __init__( self, parent = None ): wx.Dialog.__init__( self, parent = parent, title = u'分类采集', size = (400, 300) ) #-----分类复选框----- self.groupSelectBox = wx.StaticBox( self, label = u'选择分类', pos = ( 20, 20 ), size = ( 350, 120 ) ) #--所有分类 allType= [ u'首页随笔', u'精华随笔', u'候选随笔', u'推荐博客', u'专家博客', u'全部选择' ] x, y = self.groupSelectBox.Rect[0] + 40, self.groupSelectBox.Rect[1] + 30 self.SelectType = [] for i in range( len(allType) ): self.SelectType.append( wx.CheckBox( self, label = allType[i], pos = ( x, y ) ) ) x += 100 if x > 300: x = self.groupSelectBox.Rect[0] + 40 y += 50 #--下一步按钮 #-----"下一步"按钮----- self.btnNextStep = wx.Button( self, label = u'下一步', size = (80, 50), ) self.btnNextStep.Position = ( (self.ClientRect[2] - self.btnNextStep.Rect[2]) / 2 , self.ClientRect[3] - 100 ) #-----事件绑定----- self.Bind( wx.EVT_BUTTON, self.OnNextStep, self.btnNextStep ) def OnNextStep( self, evt ): self.Destroy() |
二、设置对话框
由于软件较小, 软件需要设置的参数也不多, 有句话叫"麻雀虽小,五脏俱全", 为了体现一个项目的完整性这里还是要实现下"设置"功能的, 对于需要设置的内容暂时定为采集默认保存目录和采集完成后的提示设置。
看下运行截图:
浏览目录按钮是弹出文件夹选择的对话框, 不过这里实现的仅仅都是些UI部分, 设置对话框的UI代码如下:
#!/usr/bin/python #coding:utf-8 #------------------------------------------------------------------------------- # Name: CnblogsFan_SettingDlg.py # Purpose: # # Author: Mr.Wid # # Created: 17-10-2012 # Copyright: (c) Mr.Wid 2012 # Licence: GNU GPL #------------------------------------------------------------------------------- import wx class SettingDlg(wx.Dialog): def __init__( self, parent = None ): wx.Dialog.__init__( self, parent = parent, title = u'设置', size = ( 500, 300 ) ) #-----设置保存目录----- rect = self.GetClientRect() self.groupSaveBox = wx.StaticBox( self, label = u'采集保存目录设置', pos = ( rect[0] + 20 , rect[1] + 20 ), size = ( rect[2] - 40, rect[3] - 200 ), ) #--提示标签 rect = self.groupSaveBox.Rect lblSelectTip = wx.StaticText( self, label = u'请选择默认保存目录:' ) lblSelectTip.SetPosition( ( rect[0]+ 20 , rect[1] + (rect[3] - lblSelectTip.Rect[3] ) / 2 ) ) #--路径文本框 rect = lblSelectTip.Rect self.txtPath = wx.TextCtrl( self, size = ( 200, -1 ), pos = ( rect[0] + rect[2] + 10, rect[1] - 3 ) ) #--选择目录按钮 rect = self.txtPath.Rect self.btnSelectPath = wx.Button( self, label = u'浏览目录', size = ( 80, rect[3] + 5 ), pos = ( rect[0] + rect[2] + 10, rect[1] - 3 ) ) #-----完成提示----- rect = self.groupSaveBox.Rect self.groupTipBox = wx.StaticBox( self, label = u'任务完成提示设置', pos = ( rect[0] , rect[1] + rect[3] + 10 ), size = ( rect[2], rect[3] ) ) self.chkSoundTip = wx.CheckBox( self, label = u'声音提示' ) rect = self.groupTipBox.Rect self.chkSoundTip.SetPosition( ( rect[0] + 30, rect[1] + (rect[3] - self.chkSoundTip.Rect[3]) / 2 + 5 ) ) self.chkWindowTip = wx.CheckBox( self, label = u'窗口提示' ) rect = self.chkSoundTip.Rect self.chkWindowTip.SetPosition( ( rect[0] + rect[2] + 30, rect[1] ) ) #-----保存取消按钮----- rect = self.GetClientRect() self.btnSaveSetting = wx.Button( self, label = u'保存设置', size = ( 80, 30 ) ) self.btnCancelSetting = wx.Button( self, label = u'取消', size = ( 80, 30 ) ) self.btnSaveSetting.SetPosition( ( ( rect[2] - self.btnCancelSetting.Rect[2] ) / 2 - 80 , rect[3] - 50 ) ) self.btnCancelSetting.SetPosition( ( ( rect[2] + self.btnCancelSetting.Rect[2] ) / 2, rect[3] - 50 ) ) def test(): app = wx.PySimpleApp() dlg = SettingDlg() dlg.ShowModal() if __name__ == '__main__': test() |
代码较短, 也不复杂, 主要就是计算相关控件坐标部分可能有些稍微难以理解, 坐标的计算原则依然是根据上次所说的那样, 根据上一个控件的RECT结构计算下一个控件的位置, 实际上你也可以直接忽略计算过程, 仅将其看做一个具体的数值, 这样不会影响对代码的阅读了。
三、关于软件对话框
几乎所有的应用软件都有"关于软件"对话框, 这里我们也不能太寒酸不是, 所以我们也设计了一个关于软件的对话框用来对软件做下简单的介绍, 先来预览下我们设计"关于", 如图
在这个关于对话框中, 我们主要进行了将位图文件显示在对话框中、建立了一个软件信息说明的静态框和一个选项卡以及一个确定按钮, 实现的过程如下:
#!/usr/bin/python #coding:utf-8 #------------------------------------------------------------------------------- # Name: CnblogsFan_AboutDlg.py # Purpose: # # Author: Mr.Wid # # Created: 17-10-2012 # Copyright: (c) Mr.Wid 2012 # Licence: GNU GPL #------------------------------------------------------------------------------- import wx #软件的相关介绍 CnblogsFan_Introduction = u'''CnblogsFan是一款完全开源的绿色软件, 用于采集Cnblogs(博客园)上的随笔. *蜘蛛模式: 采集Cnblogs上的所有随笔。 *指定采集: 采集指定用户的随笔。 *分类采集: 采集首页的各大分类中的随笔。 *过滤功能: 轻松找到令您感兴趣的随笔。 作者: Mr.Wid 博客: http://www.cnblogs.com/mr-wid E-mail: mr_wid@163.com ''' #协议声明文字 CnblogsFan_License = u'''采用GNU General Public License version 3开源协议. 协议在线阅读: http://www.gnu.org/licenses/gpl-3.0.html CnblogsFan项目下载: https://github.com/mrwid/CnblogsFan ''' #其他说明, 暂时还没有写 CnblogsFan_Others = ''' ''' class AboutDlg(wx.Dialog): def __init__( self, parent = None ): wx.Dialog.__init__( self, parent = parent, title = u'关于', size = (400, 500) ) self.lblImage() #显示图片 self.boxInf() #显示关于软件信息 #-----创建控件----- #--CnblogsFan文字图片 def lblImage(self): #显示图片方法 img = wx.Image('src/CnblogsFan_TextCnblogsFan.png', wx.BITMAP_TYPE_ANY) width = img.GetWidth() CnblogsFanImage = wx.StaticBitmap( self, -1, wx.BitmapFromImage(img), pos = ( (400 - width) / 2 - 5, 20 ) ) #--软件信息 def boxInf(self): #软件信息方法 self.groupBox = wx.StaticBox( self, label = u'信息', pos = ( 15, 110 ), size = ( 365, 140 ) ) rect = self.groupBox.Rect self.lblVersion = wx.StaticText( #软件版本 self, label = u'版本: 1.0.0', pos = ( rect[0] + 20, rect[1] + 30 ) ) rect = self.lblVersion.Rect self.lblAuthor = wx.StaticText( #作者 self, label = u'作者: Mr.Wid', pos = ( rect[0], rect[1] + 25 ) ) rect = self.lblAuthor.Rect self.lblWidEmail = wx.StaticText( #E-mail self, label = u'E-mail:', pos = ( rect[0], rect[1] + 25 ) ) rect = self.lblWidEmail.Rect self._lblLinkWid = wx.HyperlinkCtrl( #定义一个超链接 self, id = -1, label = u'mr_wid@163.com', url = u'mailto:mr_wid@163.com', pos = ( rect[0] + rect[2] + 10, rect[1] ) ) rect = self.lblWidEmail.Rect self.lblWidBlog = wx.StaticText( #wid的博客地址 self, label = u'博客: ', pos = ( rect[0], rect[1] + 25 ) ) rect = self.lblWidBlog.Rect self.lblLinkWidBlog = wx.HyperlinkCtrl( #建立一个指向博客地址的超链接 self, id = -1, label = u'http://www.cnblogs.com/mr-wid', url = u'http://www.cnblogs.com/mr-wid', pos = ( rect[0] + rect[2], rect[1] ) ) #--建立一个选项卡 rect = self.groupBox.Rect self.noteBook = wx.Notebook( self, -1, pos = ( rect[0], rect[1] + rect[3] + 10 ), size=( rect[2], 170 ), style = wx.NB_FIXEDWIDTH ) #建立三个文本框用于输出文字 txtIntroduction = wx.TextCtrl( #介绍 self.noteBook, -1, style = wx.MULTIPLE|wx.TE_READONLY ) txtLicense = wx.TextCtrl( #协议 self.noteBook, -1, style = wx.MULTIPLE|wx.TE_READONLY ) txtOthers = wx.TextCtrl( #其他 self.noteBook, -1, style = wx.MULTIPLE|wx.TE_READONLY ) #将文本框添加到选项卡 self.noteBook.AddPage( txtIntroduction, u"介绍" ) self.noteBook.AddPage( txtLicense, u"协议" ) self.noteBook.AddPage( txtOthers, u"其他" ) #设置介绍、协议、其他文本框中的内容 txtIntroduction.SetValue(CnblogsFan_Introduction) txtLicense.SetValue(CnblogsFan_License) txtOthers.SetValue(CnblogsFan_Others) #------确定按钮------ rect = self.GetClientRect() self._btnOK = wx.Button( self, id = wx.ID_OK, label = u"确定", pos = ( (rect[2] - 60) /2 , rect[3] - 40 ), size = ( 60, 30 ) ) def test(): app = wx.PySimpleApp() aboutDlg = AboutDlg() aboutDlg.ShowModal() if __name__ == '__main__': test() |
与其他几个对话框不同, 这里的对话框初始化方法使用了调用两个类成员函数来完成, 避免了__init__方法中堆积大量代码, 也利于以后的对话框中的控件位置调整。
所有项目文件均在GitHub上, 项目地址: https://github.com/mrwid/CnblogsFan
最新的项目进展欢迎关注wid的博客, 或者从GitHub上获得最新的项目代码, 如果您对CnblogsFan项目有任何的意见或建议, 恳请提出, wid一定会根据您的意见或建议调整、改进相关的不足之处, 同时, 也希望能够与各位朋友共同交流、进步。
实现过滤设置对话框
报告下CnblogsFan项目最新的进度, 下午wid完成了过滤设置对话框的布局设计, 过滤设置暂时的设计如下:
1>. 根据感兴趣的关键词进行采集;
2>. 限定采集的随笔的发表时间;
3>. 限定采集随笔内容的最短长度;
对话框的设计如下:
对话框由静态框、文本标签、文本框、单选按钮、下拉组合框、滑块以及一个"开始按钮"组成, 相关的代码如下
f#!/usr/bin/python #coding:utf-8 #------------------------------------------------------------------------------- # Name: CnblogsFan_FilterDlg.py # Purpose: # # Author: Mr.Wid # # Created: 18-10-2012 # Copyright: (c) Mr.Wid 2012 # Licence: GNU GPL #------------------------------------------------------------------------------- import wx import time class FilterDlg(wx.Dialog): def __init__( self, parent = None ): wx.Dialog.__init__( self, parent = parent, title = u'过滤设置', size = (500, 400 ) ) #-----感兴趣的关键词----- rect = self.GetClientRect() #--静态库框 self.groupKeyWordBox = wx.StaticBox( self, label = u'关键词检索(可选)', pos = ( rect[0] + 20 , rect[1] + 20 ), size = ( rect[2] - 40, rect[0] + 100 ) ) #--标签提示 rect = self.groupKeyWordBox.Rect self.lblKeyWord = wx.StaticText( self, label = u'感兴趣的关键词:', pos = ( rect[0] + 30, rect[1] + 30 ) ) #--关键词输入文本框 rect = self.lblKeyWord.Rect self.txtKeyWord = wx.TextCtrl( self, size = ( 300, -1 ), pos = ( rect[0] + rect[2] + 10, rect[1] - 3 ), value = u'关键词之间用空格隔开' ) self.tipKeyWordValue = True self.txtKeyWord.Bind( wx.EVT_LEFT_DOWN, self.OnClearTipText ) #--检索方式选择单选组 rect = self.groupKeyWordBox.Rect self.rdoboxKeyMode = wx.RadioBox( self, choices = [ u'仅检索标题', u'全文检索' ], style = wx.RA_HORIZONTAL ) self.rdoboxKeyMode.Position = ( rect[0] + (rect[2] - self.rdoboxKeyMode.Rect[2]) / 2 , rect[1] + (rect[3] - self.rdoboxKeyMode.Rect[3]) / 2 + 20 ) #-----时间过滤----- rect = self.groupKeyWordBox.Rect self.groupTimeBox = wx.StaticBox( self, label = u'允许采集的时间范围(可选)', pos = ( rect[0] , rect[1] + rect[3] + 20 ), size = ( rect[2], rect[0] + 50 ) ) #-----起始日期下拉选单 #--起始年份 year = [] for i in range( int( time.localtime()[0]), 2002 , -1 ): year.append( str(i) ) rect = self.groupTimeBox.Rect self.cboStartYear = wx.ComboBox( self, value = u'年', pos = ( rect[0] + 30, rect[1] + 30 ), choices = year ) #--起始月份 month = [] for i in range( 1, 13 ): month.append( str(i) ) rect = self.cboStartYear.Rect self.cboStartMonth = wx.ComboBox( self, value = u'月', pos = ( rect[0] + rect[2] + 10, rect[1] ), choices = month ) #--起始天数 rect = self.cboStartMonth.Rect self.cboStartDay = wx.ComboBox( self, value = u'日', pos = ( rect[0] + rect[2] + 10, rect[1] ), size = ( rect[2], rect[3] ) ) self.cboStartMonth.Bind( wx.EVT_COMBOBOX, self.OnShowStartDay ) self.cboStartYear.Bind( wx.EVT_COMBOBOX, self.OnShowStartDay ) #--标签 rect = self.cboStartDay.Rect wx.StaticText( self, label = u'至', pos = ( rect[0] + rect[2] + 15, rect[1] + 3 ) ) #-----截止日期下拉选单 #--结束年份 year = [] for i in range(int( time.localtime()[0]), 2002 , -1 ): year.append( str(i) ) rect = self.groupTimeBox.Rect self.cboEndYear = wx.ComboBox( self, value = u'年', pos = ( rect[0] + 240, rect[1] + 30 ), choices = year ) #--结束月份 month = [] for i in range( 1, 13 ): month.append( str(i) ) rect = self.cboEndYear.Rect self.cboEndMonth = wx.ComboBox( self, value = u'月', pos = ( rect[0] + rect[2] + 10, rect[1] ), choices = month ) #--结束天数 rect = self.cboEndMonth.Rect self.cboEndDay = wx.ComboBox( self, value = u'日', pos = ( rect[0] + rect[2] + 10, rect[1] ), size = ( rect[2], rect[3] ) ) self.cboEndMonth.Bind( wx.EVT_COMBOBOX, self.OnShowEndDay ) self.cboEndYear.Bind( wx.EVT_COMBOBOX, self.OnShowEndDay ) #-----允许采集的最短内容长度 rect = self.groupTimeBox.Rect self.groupLeastBox = wx.StaticBox( self, label = u'允许采集的随笔最短字数(可选)', pos = ( rect[0] , rect[1] + rect[3] + 20 ), size = ( rect[2], rect[3] ) ) #--建议一个滑块 rect = self.groupLeastBox.Rect self.sliderLeastWord = wx.Slider( self, value = 0, minValue = 0, maxValue = 5000, pos = ( rect[0] + 20, rect[1] + 20 ), size = ( 410, -1 ), style = wx.SL_HORIZONTAL | wx.SL_LABELS ) #-----开始采集按钮----- rect = self.GetClientRect() self.btnStart = wx.Button( self, label = u'开始采集', size = ( 80, 40 ), pos = ( (rect[2] - 80 ) / 2 , rect[3] - 50 ) ) #-----事件响应方法------ #--清空文本框提示文字 def OnClearTipText( self, evt ): self.txtKeyWord.SetFocus() if self.tipKeyWordValue: self.txtKeyWord.SetValue(u'') self.tipKeyWordValue = False #--计算结束下拉选单开始日期"日"的天数 def OnShowStartDay( self, evt ): try: year = int( self.cboStartYear.GetLabel() ) month = self.cboStartMonth.GetLabel() except: return day = 31 while day: try: time.strptime( '%s-%s-%d'%(year, month, day ), '%Y-%m-%d') self.lstDay = [ str(i) for i in range(1, day + 1) ] self.cboStartDay.SetItems(self.lstDay) self.cboStartDay.SetLabel( u'日' ) break except: day -= 1 #--计算结束下拉选单结束日期"日"的天数 def OnShowEndDay( self, evt ): try: year = int( self.cboEndYear.GetLabel() ) month = self.cboEndMonth.GetLabel() except: return day = 31 while day: try: time.strptime('%s-%s-%d'%(year, month, day ), '%Y-%m-%d') self.lstDay = [ str(i) for i in range(1, day + 1) ] self.cboEndDay.SetItems(self.lstDay) self.cboEndDay.SetLabel( u'日' ) break except: day -= 1 def test(): app = wx.PySimpleApp() dlg = FilterDlg() dlg.ShowModal() if __name__ == '__main__': test() |
转发 :http://www.cnblogs.com/mr-wid/archive/2012/10/15/2724929.html