PyQt 实战:简易便签软件的制作
为什么写便签软件
- 一直都有做一个笔记软件的想法,而我给笔记软件设计的一个特色功能就是它的便签功能。不过由于各种原因,笔记软件无法完成,但是他的便签功能也可以脱离笔记单独存在。不过功能也随着有着相应的变化
- 我们可能每天都需要一个计划表来帮助我们更加高效的工作,在windows上我们可能会使用它自带的便签软件,也有一些其他的改进版,但是我认为他们不够友好。于是我非常期待一个功能出色的便签。(我自己写的这个也只能说是个雏形,需要以后进行加工)
它具有什么特点
- 我和几个同学交流过,从用户角度上讲,一个便签首先要简易,其中操作需要简单,界面不需要花哨,要实用。
便签的开发
功能
添加、删除、修改和编辑“事件”,托盘图标,windows全局快捷键(已实现)
闹钟提醒功能 (未实现)
对于“事件”的保存 (关机重启之后仍然可以显示之前的未完成“事件”)(未实现)
桌面浮动提醒,界面的动画交互 … 等 (未实现)
对于这些功能,也不是要单单的实现这些功能,我们可以通过一些手段让这些普通的功能更加受用户的喜爱,比如说:闹钟提示:你可以添加一个贴心的小功能进去,当是、用户使用电脑时间过久,便签自动进行一些人性化的提醒之类。(这只是功能发散的一个方向)
便签的界面截图
便签软件的结构
从文件角度
widget.py
: 程序运行入口,主界面的实现- 'trayicon': 负责系统托盘功能的实现
- 'myLabel, myButton, myMeny': 重载一些Qt类,实现自定义的相应组件
github
https://github.com/zjuysw/memo.git
从功能角度讲解PyQt在其中的使用
>如果没有使用过软件,可能对下面的代码注释会有点不理解
主界面布局
- 主界面采用HBoxLayout,其中包含两个VBoxLayout。(实现起来没什么困难)
界面前端关键技术
主界面设置背景图片: 采用QPalette。注:使用stylesheet会让子widget继承。(widget.py)
backImg = QPixmap('./img/1.png').scaled(self.size()) palette = QPalette() palette.setBrush(self.backgroundRole(), QBrush(backImg)) self.setPalette(palette)
图标的背景图片和样式:采用stylesheet
特效(透明):采用QGraphicsOpacityEffect (mylable.py)
self.opacity = QGraphicsOpacityEffect() self.opacity.setOpacity(0.7) self.setGraphicsEffect(self.opacity)
便签的拖拽技术
主要是重写widget的鼠标事件 (mylable.py)
def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.dragPos = event.globalPos() - self.pos() event.accept() def mouseMoveEvent(self, QMouseEvent): pw = self.parentWidget() # 获取父widget,也就是本程序中的主widget widget1 = pw.getTrashRect() # 获取主widget 的 垃圾箱widget(函数名没有改过来) flag = self.isCollide(widget1, self) # 检测两个widget的碰撞 if flag: self.emit(SIGNAL('collideTrash'), True) # 碰撞就发射collideTrash信号 else: self.emit(SIGNAL('collideTrash'), False) # 以下代码用于进行widget的拖拽 if QMouseEvent.buttons() == Qt.LeftButton: self.move(QMouseEvent.globalPos() - self.dragPos) QMouseEvent.accept() if QMouseEvent.buttons() == Qt.RightButton: QMouseEvent.ignore() def mouseReleaseEvent(self, QMouseEvent): # 拖拽动作完成之后检测是否碰撞以确定该widget是否被删除 pw = self.parentWidget() widget1 = pw.getTrashRect() flag = self.isCollide(widget1, self) if flag: print "yes" self.emit(SIGNAL('collideTrash'), False) self.hide() self.destroy() else: self.emit(SIGNAL('collideTrash'), False) self.hide() self.show()
自定义信号发送和接受技术
下面的代码大概表示了这个技术的核心内容(实际运用请看项目完整代码中的运用)
parentWidget = QWidget() subWidget = QWidget(parentWidget) subWidget.emit(SIGNAL("sub")) parentWidget.connect(subWidget, SIGNAL("sub"), parentWidget.doSomething)
显示和编辑的替换技术
思路:一个layout中包含两个layout,其中layout各自包含2个widget,分别是:内容lable,时间lable,编辑框textedit和确定按钮button。要显示的时候,我们让编辑框和按钮隐藏,编辑的时候,我们让内容和时间隐藏。(mylable.py)
def mouseDoubleClickEvent(self, event): if event.button() == Qt.LeftButton: self.label.hide() self.timeLabel.hide() self.textEdit.show() self.textEdit.setFocus() self.textEdit.setText(self.label.text()) self.okBtn.show()
widget的碰撞检测技术
思路:假设垃圾桶为widget1,我们的显示lable是widget2,由于刚开始的时候垃圾桶在显示lable的左下角,所以他们如果碰撞(重叠)就必然会有:
widget2的右上角在widget1的左下角的右上方,widget2的左下角必定在widget1的右上角的左下方
def isCollide(self, widget1, widget2): dict1 = {} dict1['size'] = widget1.size() dict1['pos'] = widget1.pos() dict2 = {} dict2['size'] = widget2.size() dict2['pos'] = widget2.pos() r1TopRightX = dict1['pos'].x() + dict1['size'].width() r1TopRightY = dict1['pos'].y() r1BottomLeftX = dict1['pos'].x() r1BottomLeftY = dict1['pos'].y() + dict1['size'].height() r2TopRightX = dict2['pos'].x() + dict2['size'].width() r2TopRightY = dict2['pos'].y() r2BottomLeftX = dict2['pos'].x() r2BottomLeftY = dict2['pos'].y() + dict2['size'].height() if r1TopRightX > r2BottomLeftX and r1TopRightY < r2BottomLeftY \ and r2TopRightX > r1BottomLeftX and r2TopRightY < r1BottomLeftY: return True else: return False
编辑焦点检测
直接运用QTextEdit的QFocusEvent (mylable.py)
def focusInEvent(self, event): print "edit" self.emit(SIGNAL("Editing")) def focusOutEvent(self, event): if event.reason() == 4: # popup focus event.ignore() else: self.emit(SIGNAL("EditFinish"))
windows的全局快捷键技术
使用python的ctypes模块(Qt本身没有相应全局快捷键处理类)(hotkey.py)
#!/usr/bin/env python # -*- coding: utf8-*- import sys import time from ctypes import * from ctypes.wintypes import * from PyQt4.QtGui import QApplication import widget delta = 0.3 lastTime = 0 WM_HOTKEY = 0x0312 MOD_ALT = 0x0001 MOD_CONTROL = 0x0002 MOD_SHIFT = 0x0004 WM_KEYUP = 0x0101 class MSG(Structure): _fields_ = [('hwnd', c_int), ('message', c_uint), ('wParam', c_int), ('lParam', c_int), ('time', c_int), ('pt', POINT)] key = 192 # ~ key hotkeyId = 1 if not windll.user32.RegisterHotKey(None, hotkeyId, None, key): sys.exit("Cant Register Hotkey") msg = MSG() app = QApplication(sys.argv) w = widget.mainUi() while True: if (windll.user32.GetMessageA(byref(msg), None, 0, 0) != 0): if msg.message == WM_HOTKEY and msg.wParam == hotkeyId: if (time.time() - lastTime) < delta: w.show() else: pass lastTime = time.time() if msg.message == WM_KEYUP: print "up" w.myHide() windll.user32.TranslateMessage(byref(msg)) windll.user32.DispatchMessageA(byref(msg))
系统托盘技术
基本上看看PyQt的文档差不多了(trayicon.py)
# -*- coding:utf8 -*- import sys from PyQt4 import QtCore, QtGui from PyQt4.QtCore import * from PyQt4.QtGui import * class TrayIcon(QSystemTrayIcon): def __init__(self, parent=None): super(TrayIcon, self).__init__(parent) self.initObjects() self.setObjects() self.activated.connect(self.iconClicked) def initObjects(self): self.menu = QMenu() self.quitAction = QAction(u"退出", self, triggered=self.exitApp) self.icon = QIcon('./img/icon.png') def setObjects(self): self.menu.addAction(self.quitAction) self.setIcon(self.icon) self.setContextMenu(self.menu) def iconClicked(self, reason): print reason if reason==2 or reason==3: pw = self.parent() if pw.isVisible(): pw.hide() else: pw.show() def exitApp(self): self.setVisible(False) qApp.quit() sys.exit() if __name__ == "__main__": import sys app = QApplication(sys.argv) ti = TrayIcon() ti.show() sys.exit(app.exec_())
小结
好像自己编写的过程中遇到的比较难的技术问题就这些,不过关键还是要把PyQt的一些基础知识学牢固,自己组织软件的时候把需求想清楚,把软件的结构理清楚。欢迎_交流_与_指正_或者_提出更好的方法和建议_。思维的碰撞总会有意想不到的惊喜
注
- 读程序代码可能有点头疼,因为注释很少有。
- 尤其是信号的发送和接受,这些都是在不同widget之间的传递,自然代码就会写在不同的文件中,这也是这次实践遇到的一个问题,
怎样在代码行数增多时,仍然保持它的可读性和可维护性
。- 其中我个人认为代码行数上四位数就最好给代码配上相应的文档,各个函数的注释(功能方面,依赖性,)也是要写清楚。
- 各个模块的耦合度要低,不然当你需要对代码进行修改,发现改了这一个地方,其他一大堆需要更改。
- 另外,不得不提的是,这代码的确写的比较烂,比如说可读性不高,维护不容易(现在可能是靠自己对项目的记忆,从而进行修改)。×不晓得设计模式那些数都是写什么的×
- 这样不大的项目可能不怎么要考虑架构(这个词的具体含义其实我也不懂),但是之前写笔记软件就发现数据不符合你软件的结构,那么最后注定失败。 </ul>
来自:http://my.oschina.net/zjuysw/blog/318352