PyQt 实战:简易便签软件的制作

jopen 10年前

为什么写便签软件

  • 一直都有做一个笔记软件的想法,而我给笔记软件设计的一个特色功能就是它的便签功能。不过由于各种原因,笔记软件无法完成,但是他的便签功能也可以脱离笔记单独存在。不过功能也随着有着相应的变化
  • 我们可能每天都需要一个计划表来帮助我们更加高效的工作,在windows上我们可能会使用它自带的便签软件,也有一些其他的改进版,但是我认为他们不够友好。于是我非常期待一个功能出色的便签。(我自己写的这个也只能说是个雏形,需要以后进行加工)

它具有什么特点

  • 我和几个同学交流过,从用户角度上讲,一个便签首先要简易,其中操作需要简单,界面不需要花哨,要实用。

便签的开发

功能

  1. 添加、删除、修改和编辑“事件”,托盘图标,windows全局快捷键(已实现)

  2. 闹钟提醒功能 (未实现)

  3. 对于“事件”的保存 (关机重启之后仍然可以显示之前的未完成“事件”)(未实现)

  4. 桌面浮动提醒,界面的动画交互 … 等 (未实现)

    对于这些功能,也不是要单单的实现这些功能,我们可以通过一些手段让这些普通的功能更加受用户的喜爱,比如说:闹钟提示:你可以添加一个贴心的小功能进去,当是、用户使用电脑时间过久,便签自动进行一些人性化的提醒之类。(这只是功能发散的一个方向)

便签的界面截图

PyQt 实战:简易便签软件的制作
PyQt 实战:简易便签软件的制作

便签软件的结构

从文件角度

  • 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之间的传递,自然代码就会写在不同的文件中,这也是这次实践遇到的一个问题,怎样在代码行数增多时,仍然保持它的可读性和可维护性
    • 其中我个人认为代码行数上四位数就最好给代码配上相应的文档,各个函数的注释(功能方面,依赖性,)也是要写清楚。
    • 各个模块的耦合度要低,不然当你需要对代码进行修改,发现改了这一个地方,其他一大堆需要更改。
    </li>
  • 另外,不得不提的是,这代码的确写的比较烂,比如说可读性不高,维护不容易(现在可能是靠自己对项目的记忆,从而进行修改)。×不晓得设计模式那些数都是写什么的×
  • 这样不大的项目可能不怎么要考虑架构(这个词的具体含义其实我也不懂),但是之前写笔记软件就发现数据不符合你软件的结构,那么最后注定失败。
  • </ul>

    来自:http://my.oschina.net/zjuysw/blog/318352