runcat-pyqt5-win:在windows任务栏养猫

V1

runcat-pyqt5-win

RunCat是一款 mac应用,用奔跑的猫来显示当前系统资源(CPU)占用情况。

但是只有mac版,于是用python撸了一个,可以在windows任务栏(通知区域)养猫。

首先用psutil
获得CPU或内存的使用情况,然后用pyqt5创建QSystemTrayIcon显示在任务栏的托盘区域。

GPU的使用情况可以用 nvidia-ml-py, https://pypi.org/project/nvidia-ml-py/
的pynvml模块(仅限nvidia gpu)。

运行效果

Requirements

  • psutil
  • pyqt5
  • nvidia-ml-py

Code

  • github仓库: https://github.com/shenbo/runcat-pyqt5-win

  • CPU 版

    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
    import sys
    import time
    import threading

    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import QApplication, QSystemTrayIcon

    import psutil

    # Get cpu usage
    def func():
    while True:
    global cpu
    cpu = psutil.cpu_percent(interval=1) / 100
    time.sleep(1)

    # Create Qt App
    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(False)
    # Create trayicon
    tray = QSystemTrayIcon()
    tray.setIcon(QIcon('0.ico'))
    tray.setVisible(True)

    cpu = 0.1
    timer = threading.Timer(1, func, [])
    timer.start()

    while True:
    t = (cpu * cpu - 10 * cpu + 10) / 40
    for i in range(5):
    # Update trayicon
    tray.setIcon(QIcon('{}.ico'.format(i)))
    tray.setToolTip('CPU: {:.2%}'.format(cpu))
    time.sleep(t)

    app.exec_()
  • 内存版

    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
    import sys
    import time
    import threading

    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import QApplication, QSystemTrayIcon

    import psutil

    # Get memory usage
    def func():
    while True:
    global mem
    mem = psutil.virtual_memory().percent / 100
    time.sleep(1)

    # Create Qt App
    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(False)
    # Create trayicon
    tray = QSystemTrayIcon()
    tray.setIcon(QIcon('0.ico'))
    tray.setVisible(True)

    mem = 0.1
    timer = threading.Timer(1, func, [])
    timer.start()

    while True:
    t = (mem * mem - 10 * mem + 10) / 40
    for i in range(5):
    # Update trayicon
    tray.setIcon(QIcon('{}.ico'.format(i)))
    tray.setToolTip('Memory: {:.2%}'.format(mem))
    time.sleep(t)

    app.exec_()
  • GPU 版

    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
    import sys
    import time
    import threading

    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import QApplication, QSystemTrayIcon

    import pynvml

    pynvml.nvmlInit()
    handle = pynvml.nvmlDeviceGetHandleByIndex(0) # GPU id: 0

    # Get gpu usage
    def func():
    while True:
    global gpu
    meminfo = pynvml.nvmlDeviceGetMemoryInfo(handle)
    gpu = meminfo.used / meminfo.total
    time.sleep(1)

    # Create Qt App
    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(False)
    # Create trayicon
    tray = QSystemTrayIcon()
    tray.setIcon(QIcon('0.ico'))
    tray.setVisible(True)

    gpu = 0.1
    timer = threading.Timer(1, func, [])
    timer.start()

    while True:
    t = (gpu * gpu - 10 * gpu + 10) / 40
    for i in range(5):
    # Update trayicon
    tray.setIcon(QIcon('{}.ico'.format(i)))
    tray.setToolTip('GPU: {:.2%}'.format(gpu))
    time.sleep(t)

    app.exec_()

Usage

  • 直接clone或下载, 改 *.pyw 运行

  • 或者下载打包后的 *.exe, 但是有30多M。。。下载exe

ref:


V2

之前的功能比较简单,这次增加了右键菜单:

  • 可切换图标类型: [cat, mario]
  • 可切换监控类型: [cpu, memory, gpu(nVidia)]
  • 增加了退出按钮

运行效果

Requirements

  • psutil
  • pyqt5
  • nvidia-ml-py

完整代码

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import os
import sys
import threading
import time

import psutil
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QAction

import pynvml

pynvml.nvmlInit()
handle = pynvml.nvmlDeviceGetHandleByIndex(0) # GPU id: 0


class TrayIcon(QSystemTrayIcon):
def __init__(self, parent=None):
super(TrayIcon, self).__init__(parent)
self.monitor = 'cpu'
self.cpu_usage = 0.2 # 初始化
self.mem_usage = 0.2 # 初始化
self.gpu_usage = 0.2 # 初始化

self.icon_type = 'runcat' # 设定默认图标,并加载
self.icon_list = self.loadIcon()
self.setIcon(self.icon_list[0])

self.setVisible(True)
self.setMenu() # 加载菜单
self.updateIcon() # 更新图标

# 加载图标
def loadIcon(self):
if self.icon_type == 'mario':
return [QIcon(f'icons/{self.icon_type}/{i}.png') for i in range(3)]
return [QIcon(f'icons/{self.icon_type}/{i}.png') for i in range(5)]

# 设置菜单
def setMenu(self):
self.menu = QMenu()
self.action_1 = QAction(QIcon(f'icons/cat.png'),
'Cat', self, triggered=lambda: self.changeIconType('runcat'))
self.action_2 = QAction(QIcon(f'icons/mario/0.png'),
'Mario', self, triggered=lambda: self.changeIconType('mario'))

self.action_c = QAction(QIcon(f'icons/cpu.png'),
'CPU', self, triggered=lambda: self.changeMonitor('cpu'))
self.action_m = QAction(QIcon(f'icons/mem.png'),
'Memory', self, triggered=lambda: self.changeMonitor('mem'))
self.action_g = QAction(QIcon(f'icons/gpu.png'),
'GPU', self, triggered=lambda: self.changeMonitor('gpu'))

self.action_q = QAction(QIcon(f'icons/quit.png'),
'Quit', self, triggered=self.quit)

self.menu.addAction(self.action_c)
self.menu.addAction(self.action_m)
self.menu.addAction(self.action_g)
self.menu.addSeparator()
self.menu.addAction(self.action_1)
self.menu.addAction(self.action_2)
self.menu.addSeparator()
self.menu.addAction(self.action_q)
self.setContextMenu(self.menu)

# 根据使用率更新图标,
# 创建两个 threading:一个获取使用率,一个更新图标
def updateIcon(self):
threading.Timer(0.1, self.thread_get_cpu_usage, []).start()
threading.Timer(0.1, self.thread_update_icon, []).start()

# get cpu usage
def thread_get_cpu_usage(self):
while True:
self.cpu_usage = psutil.cpu_percent(interval=1) / 100
self.mem_usage = psutil.virtual_memory().percent / 100
meminfo = pynvml.nvmlDeviceGetMemoryInfo(handle)
self.gpu_usage = meminfo.used / meminfo.total
# print(self.cpu_usage)
time.sleep(0.5)

# update icon
def thread_update_icon(self):
while True:
mon = self.cpu_usage
if self.monitor == 'mem':
mon = self.mem_usage
elif self.monitor == 'gpu':
mon = self.gpu_usage

t = 0.18 - mon * 0.15
# print(mon, t)
for i in self.icon_list:
self.setIcon(i)
tip = f'cpu: {self.cpu_usage:.2%} \nmem: {self.mem_usage:.2%} \ngpu: {self.gpu_usage:.2%}'
self.setToolTip(tip)
# print(i, self.cpu_usage)
time.sleep(t)

# Change icon type
def changeIconType(self, type):
print(type)
if type != self.icon_type:
self.icon_type = type
self.icon_list = self.loadIcon()
print(f'Load {self.icon_type}({len(self.icon_list)}) icons...')

# change monitor type
def changeMonitor(self, monitor_type):
print(monitor_type)
if monitor_type != self.monitor:
self.monitor = monitor_type

# 退出程序
def quit(self):
self.setVisible(False)
app.quit()
os._exit(-1) # 完全退出程序


if __name__ == "__main__":
app = QApplication(sys.argv)
tray = TrayIcon()

sys.exit(app.exec_())


V3

原来的基于 ptqt5 库比较大; 这次采用 pystray 轻量实现:

Requirements

  • psutil
  • pystray

完整代码

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
import threading
import time

from psutil import cpu_percent, virtual_memory
from pystray import Icon, Menu, MenuItem

# PIL dependency simplification
# ref: https://github.com/moses-palmer/pystray/issues/26
class ICOImage:
def __init__(self, path: str):
with open(path, 'rb') as file:
self._data = file.read()
def save(self, file, format):
file.write(self._data)

# get cpu usage
def thread_get_usage():
global cpu_usage, mem_usage
while True:
if thread_flag: break
cpu_usage = cpu_percent(interval=1) / 100
mem_usage = virtual_memory().percent / 100
time.sleep(0.5)

def changeMonitor(new_monitor):
global monitor
print(monitor, new_monitor)
if new_monitor != monitor:
monitor = new_monitor

def on_quit():
global thread_flag
thread_flag = 1
runcat.stop()

# 初始化
monitor = 'CPU'
cpu_usage = 0.2 # 初始化
mem_usage = 0.2 # 初始化
cats = [ICOImage(f'icons/runcat/{i}.ico') for i in range(5)]

menu = (MenuItem(text='CPU', action=lambda: changeMonitor('cpu')),
MenuItem(text='MEM', action=lambda: changeMonitor('mem')),
MenuItem(text='QUIT', action=on_quit))
runcat = Icon('run cat', icon=cats[0], title='run cat', menu=menu)

# 创建两个 threading:一个获取使用率,一个更新图标
thread_flag = 0
threading.Thread(target=runcat.run).start()
threading.Thread(target=thread_get_usage).start()

while True:
if thread_flag: break
for icon in cats:
runcat.icon = icon
mon = mem_usage if monitor == 'mem' else cpu_usage
t = 0.2 - mon * 0.15
print(f'{mon=:.2%}, {t=:.2f}s, {cpu_usage=:.2%}, {mem_usage=:.2%}')

tip = f'cpu: {cpu_usage:.2%} \nmem: {mem_usage:.2%}'
runcat.title = tip

time.sleep(t)

# pyinstaller -w -i favicon.ico runcat-v0.6-pystray.py --add-data "icons;icons"