0%

【Python】使用PYQT5来构建持仓盈亏监控

目的

通过win10的通知来监控持仓盈亏,会在托盘上累积太多的图标,而且经常受到专注助手影响,效果不是太理想。所以尝试利用PyQt5来构建一个监控系统,实现类似雪球网页上同样的功能。

安装PyQt5

安装PyQt5

  1. pip安装 >pip install PyQt5

  2. 官网下载文件安装
    下载地址:PyQt下载官网 安装方法:打开命令行,通过命令行进入到下载的whl文件所在的文件夹,输入如下命令,安装即可。 >pip install PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win_amd64.whl

安装PyQt5-tools

PyQt5-tools里含有图形界面开发工具QtDesigner及国际化翻译工具Liguist。 1. pip安装 >pip install PyQt5-tools

  1. 官网下载文件安装
    下载地址:PyQt下载官网 安装方法:打开命令行,通过命令行进入到下载的whl文件所在的文件夹,输入如下命令,安装即可。 >pip install pyqt5_tools-5.15.2.3.0.2-py3-none-any.whl

在系统中加入以下环境变量 >d:-packages_tools

与VScode集成

  1. 安装pyqt integration插件 对pyqt integration插件进行配置,主要是pyuic5及qt designer的路径

界面设计

用小窗口来实现监控。

术语

  • 布局
    采用了布局之后能够让我们的程序在使用上更加美观,不会随着窗体的大小发生改变而改变,符合我们的使用习惯。在PyQt5中,有多种布局的方式供我们选择,比较常用的布局有以下几种:
    • 表单布局:QFormLayout
    • 网格布局:QGridLayout
    • 水平排列布局:QHBoxLayout
    • 垂直排列布局:QVBoxLayout

比较详细的说明可以参考PyQt5系列教程(6):布局

  • MVC模式 MVC 模式 指 Model-View-Controller(模型-视图-控制器) 模式。这种模式多应用于应用程序的分层开发。
    • Model是一个数据模型、一个虚拟的东西,显示不出来的,如果我们要把这些数据显示出来,就需要使用view。
    • View:用来显示Model这种数据
    • Controler:如果我们要编辑数据,就需要使用controler,不使用controler,我们只能对数据进行查看不能编辑

使用QtDesigner来设计界面

  1. 初始界面 初始界面设计如下:

相应的组件及布局如下:

计划使用QLabel来显示总的持仓盈亏情况,用Qtableview来显示具体的股票信息, Qlabel使用gridLayout, Qtableview使用verticalLayout

  1. 界面美化 在自定义的View类中写入代码进行美化。
  • 去除丑陋的边框
1
self.setWindowFlag(QtCore.Qt.FramelessWindowHint) 
  • 增加关闭及最小化按钮(以MacOS的样式) 用pyqtSlot装饰器来实现点击按钮关闭和最小化的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.CloseButton.setFixedSize(15,15) 
self.MinButton.setFixedSize(15,15)
self.CloseButton.setStyleSheet("QPushButton{background:#F76677;border-radius:7px;}QPushButton:hover{background:red;}")
self.MinButton.setStyleSheet("QPushButton{background:#6DDF6D;border-radius:7px;}QPushButton:hover{background:green;}")

@pyqtSlot()
def on_CloseButton_clicked(self):
"""
关闭窗口
"""
self.close()

@pyqtSlot()
def on_MinButton_clicked(self):
"""
最小化窗口
"""
self.showMinimized()
  • 针对盈亏对显示结果使用不同颜色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if float(self.label_4.text().split('(')[0]) < 0:
self.label_2.setStyleSheet("QLabel{color:#01D68A;font-size:15px;font-weight:bold;font-family:Hack;}")
self.label_4.setStyleSheet("QLabel{color:#01D68A;font-size:15px;font-weight:bold;font-family:Hack;}")
else:
self.label_2.setStyleSheet("QLabel{color:#FF0018;font-size:15px;font-weight:bold;font-family:Hack;}")
self.label_4.setStyleSheet("QLabel{color:#FF0018;font-size:15px;font-weight:bold;font-family:Hack;}")
if float(self.label_6.text().split('(')[0]) < 0:
self.label_6.setStyleSheet("QLabel{color:#01D68A;font-size:15px;font-weight:bold;font-family:Hack;}")
else:
self.label_6.setStyleSheet("QLabel{color:#FF0018;font-size:15px;font-weight:bold;font-family:Hack;}")
if float(self.label_8.text().split('(')[0]) < 0:
self.label_8.setStyleSheet("QLabel{color:#01D68A;font-size:15px;font-weight:bold;font-family:Hack;}")
else:
self.label_8.setStyleSheet("QLabel{color:#FF0018;font-size:15px;font-weight:bold;font-family:Hack;}")
  • Qtableview的表头使用与窗口同样的背景色以及文字左对齐
1
2
3
4
5
6
self.tableView.setStyleSheet("border-width: 0px; border-style: solid;")
self.tableView.horizontalHeader().setStyleSheet("QHeaderView::section{Background-color:#454545;border-radius: 0px;}")
self.tableView_2.setStyleSheet("border-width: 0px; border-style: solid;")
self.tableView_2.horizontalHeader().setStyleSheet("QHeaderView::section{Background-color:#454545;border-radius: 0px;}")
self.tableView.horizontalHeader().setDefaultAlignment(Qt.AlignLeft)
self.tableView_2.horizontalHeader().setDefaultAlignment(Qt.AlignLeft)
  • Qtableview显示的数据也同样根据盈亏使用不同的颜色
    这个在Model类中的data()函数中实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
value = self._data.iloc[index.row(), index.column()]
if role == Qt.ForegroundRole and isinstance(value, float) and index.column() == 3:
next_value = self._data.iloc[index.row(), index.column() + 1]
if next_value < 0:
return QtGui.QColor('#01D68A')
else:
return QtGui.QColor('#FF0018')

if role == Qt.ForegroundRole and isinstance(value, float) and index.column() != 2:
if value < 0:
return QtGui.QColor('#01D68A')
else:
return QtGui.QColor('#FF0018')
if role == Qt.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
  • 重写鼠标事件,实现窗口拖放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def initDrag(self):
# 设置鼠标跟踪判断扳机默认值
self._move_drag = False
self._corner_drag = False
self._bottom_drag = False
self._right_drag = False
self._padding = 5

def resizeEvent(self, QResizeEvent):
# 重新调整边界范围以备实现鼠标拖放缩放窗口大小,采用三个列表生成式生成三个列表
self._right_rect = [QPoint(x, y) for x in range(self.width() - self._padding, self.width() + 1) for y in range(1, self.height() - self._padding)]
self._bottom_rect = [QPoint(x, y) for x in range(1, self.width() - self._padding)
for y in range(self.height() - self._padding, self.height() + 1)]
self._corner_rect = [QPoint(x, y) for x in range(self.width() - self._padding, self.width() + 1) for y in range(self.height() - self._padding, self.height() + 1)]

def mousePressEvent(self, event):
# 重写鼠标点击的事件
if (event.button() == Qt.LeftButton) and (event.pos() in self._corner_rect):
# 鼠标左键点击右下角边界区域
self._corner_drag = True
event.accept()
elif (event.button() == Qt.LeftButton) and (event.pos() in self._right_rect):
# 鼠标左键点击右侧边界区域
self._right_drag = True
event.accept()
self.setCursor(QCursor(Qt.SizeHorCursor))
elif (event.button() == Qt.LeftButton) and (event.pos() in self._bottom_rect):
# 鼠标左键点击下侧边界区域
self._bottom_drag = True
event.accept()
self.setCursor(QCursor(Qt.SizeVerCursor))
elif (event.button() == Qt.LeftButton) and (event.y() < self.label_10.y()+self.label_10.height() or (event.y() > self.label_9.y() and event.y() < self.tableView_2.y())):
# 鼠标左键点击标题栏区域
self._move_drag = True
self.move_DragPosition = event.globalPos() - self.pos()
event.accept()
self.setCursor(QCursor(Qt.OpenHandCursor)) #更改鼠标图标


def mouseMoveEvent(self, QMouseEvent):
# 判断鼠标位置切换鼠标手势
# if QMouseEvent.pos() in self._corner_rect:
# self.setCursor(Qt.SizeFDiagCursor)
# print("corner:%s, in %s" %(QMouseEvent.pos(),self._corner_rect))
#print(QMouseEvent.globalPos(), QMouseEvent.pos(), self.graphWidget.x(), self.graphWidget.y(), self.graphWidget.pos(), self.graphWidget.width(), self.graphWidget.height())
if QMouseEvent.pos() in self._bottom_rect:
self.setCursor(QCursor(Qt.SizeVerCursor))
elif QMouseEvent.pos() in self._right_rect:
self.setCursor(QCursor(Qt.SizeHorCursor))
elif QMouseEvent.pos() in self._corner_rect:
self.setCursor(QCursor(Qt.SizeFDiagCursor))
else:
self.setCursor(QCursor(Qt.ArrowCursor))
# 当鼠标左键点击不放及满足点击区域的要求后,分别实现不同的窗口调整
# 没有定义左方和上方相关的5个方向,主要是因为实现起来不难,但是效果很差,拖放的时候窗口闪烁,再研究研究是否有更好的实现
if Qt.LeftButton and self._right_drag:
# 右侧调整窗口宽度
self.resize(QMouseEvent.pos().x(), self.height())
QMouseEvent.accept()
elif Qt.LeftButton and self._bottom_drag:
# 下侧调整窗口高度
self.resize(self.width(), QMouseEvent.pos().y())
QMouseEvent.accept()
elif Qt.LeftButton and self._corner_drag:
# 右下角同时调整高度和宽度
self.resize(QMouseEvent.pos().x(), QMouseEvent.pos().y())
QMouseEvent.accept()
elif Qt.LeftButton and self._move_drag:
# 标题栏拖放窗口位置
self.move(QMouseEvent.globalPos() - self.move_DragPosition)
QMouseEvent.accept()

def mouseReleaseEvent(self, QMouseEvent):
# 鼠标释放后,各扳机复位
self._move_drag = False
self._corner_drag = False
self._bottom_drag = False
self._right_drag = False
self.setCursor(QCursor(Qt.ArrowCursor))

效果

完成后的应用界面效果如下: # 参考 1. VSCode配置Python、PyQt5、QtDesigner环境并创建一个ui界面测试 2. python界面编程:VScode+pyqt+pyqt integration配置备忘 3. PyQt5(designer)入门教程 4. Embedding custom widgets from Qt Designer