仓库源文站点原文


layout: post title: 制作程序运行提示标志的历程

tags: [Python, 程序, 标志]

有图形界面的程序可真是难做啊……<!--more-->

起因

最近我做了一个程序,类似于守护进程那样的一个用Python制作的脚本。脚本做出来很简单,可是我做出来之后需要向其他人证明我做的脚本正在运行,而且是给一个不懂电脑的人知道。在这个前提下我不可能让其他人去看任务管理器、或者执行ps -ef | grep xxx这种东西吧。所以还是得让它在运行的时候在桌面这样的图形界面显示一些东西才行。

制作的历程

使用托盘区图标

像一般的后台程序证明自己存在的方式就是任务栏的托盘区显示一个图标。于是首先我就按照这个想法先用PyQt5然后在网上找了一段代码然后自己改了改:

使用PyQt5库实现

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from threading import Thread

class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
    def __init__(self, icon, parent=None):
        QtWidgets.QSystemTrayIcon.__init__(self, icon, parent)

app = QtWidgets.QApplication(sys.argv)

w = QtWidgets.QWidget()
trayIcon = SystemTrayIcon(QtGui.QIcon("pic.ico"), w)
trayIcon.show()
Thread(target=some_random_function).start() # 我的脚本函数
sys.exit(app.exec_())

这样做完之后东西确实可以运行了,不过我的代码还要在其他人的电脑上运行,所以我先用Pyinstaller打包了一下,不过打包出来的程序很大,就几十行代码就算加上Python解析器最多也就不到10MiB,结果这加上PyQt库之后直接上升到40MiB左右,就算用upx压缩完也有30多MiB,实在是让人无法忍受,于是我就考虑看看能不能用其他方式来制作这个图标。
后来我在网上找到有一个叫做pystray的库似乎是专门干这个活的。那既然是专门干这个的那大小肯定比什么Qt库要小得多吧?于是我按照示例的代码随便写了一段:

使用pystray库实现

from threading import Thread
import pystray
from PIL import Image

image = Image.open("pic.png")
icon = pystray.Icon(name="SomeRandom", icon=image, title="SomeRandom", menu=None)

Thread(target=some_random_function).start() # 我的脚本函数
icon.run()

这个代码我没有测试过,不过在我写完之后首先用Pyinstaller打包了一下,结果大小比用PyQt5还要大😂,打包完大小要60多MiB,所以没办法我就只能继续用Qt的那个版本了……

制作悬浮图标

后来我的脚本由于应用面广泛需要在Ubuntu上使用,最新的Ubuntu使用的GNOME桌面不再支持托盘区了😓,所以没办法,只能想别的办法了。
现在的程序除了托盘区图标证明自己的存在可能还有就是悬浮球了吧?国产很多软件喜欢把自己程序整成悬浮球那样放在桌面上吸引用户的注意力,所以我的程序也要这样搞。我想了想用PyQt库实在是太重了,我想整个轻量的图形引擎,像Python自带的Tkinter就挺不错的,所以我首先用Tkinter做了一个版本出来:

通过Tkinter实现

import tkinter
from threading import Thread

root = tkinter.Tk()
height = 100
width = 100
root.overrideredirect(True)
root.attributes('-transparentcolor', "white")
root.attributes("-alpha", 0.9)  # 窗口透明度10 %
root.attributes("-topmost", 1)
root.geometry(f"{height}x{width}-40+60")
canvas = tkinter.Canvas(root, height=height, width=width, bg="white")
canvas.config(highlightthickness=0)
image_file = tkinter.PhotoImage(file=r'pic.png')
image = canvas.create_image(
    height//2, width//2, anchor=tkinter.CENTER, image=image_file)
canvas.pack()
x, y = 0, 0

show_menu = tkinter.Menu(root, tearoff=0)
show_menu.add_command(label="SomeRandom")

def move(event):
    global x, y
    new_x = (event.x-x)+root.winfo_x()-height//2
    new_y = (event.y-y)+root.winfo_y()-width//2
    s = f"{height}x{width}+" + str(new_x)+"+" + str(new_y)
    root.geometry(s)

canvas.bind("<B1-Motion>", move)
canvas.bind("<Enter>", lambda event: show_menu.post(event.x_root, event.y_root))
canvas.bind("<Leave>", lambda e: show_menu.unpost())

Thread(target=some_random_function).start() # 我的脚本函数

root.mainloop()

这个代码在Windows上工作还算可以,问题不是很多,但是在Linux上就出现了很糟糕的问题,根据tcl/tk documentation 好像也没写🤣 ,“-transparentcolor”属性只能在Windows等系统使用(貌似MacOS也能用?),因此在Linux中会报错。如果不能使用透明背景效果就会很差,我看Stack Overflow上有人说可以安装一个pqiv图片查看器,然后使用os.popen()或者subprocess.Popen()执行pqiv -c -c -i pic.png也能达到类似的效果,不过这种东西先不说还要安装,而且这个东西点两下就能看见它的窗口,还能关闭,那肯定是不符合我们的要求的。所以没办法……只能再考虑Qt的办法了。

使用PyQt5库实现

我在网上又找了些资料把PyQt的版本也做出来了,而且还加了支持Gif动态图片的效果:

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *

class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def mouseMoveEvent(self, e: QMouseEvent):  # 重写移动事件
        if self._tracking:
            self._endPos = e.pos() - self._startPos
            self.move(self.pos() + self._endPos)

    def mousePressEvent(self, e: QMouseEvent):
        if e.button() == Qt.LeftButton:
            self._startPos = QPoint(e.x(), e.y())
            self._tracking = True

    def mouseReleaseEvent(self, e: QMouseEvent):
        if e.button() == Qt.LeftButton:
            self._tracking = False
            self._startPos = None
            self._endPos = None

    def initUI(self):
        layout = QStackedLayout()
        self.lbl1 = QLabel(self)
        self.movie = QMovie("pic.gif")
        self.lbl1.setMovie(self.movie)
        self.movie.start()
        self.lbl1.setToolTip("SomeRandom")
        layout.addWidget(self.lbl1)

        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.Tool)

        #layout area for widgets
        layout.setCurrentIndex(1)
        self.setLayout(layout)
        self.setGeometry(QApplication.desktop().width()-130,50,100,100)
        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    Thread(target=some_random_function).start() # 我的脚本函数
    ex = Example()
    sys.exit(app.exec_())

最终做出来效果还不错,说不定加点功能放组简单的立绘动画就能做一个像我博客左下角的看板娘一样的东西呢🤣,虽然我也见过用Electron写的PPet,不过用Python写的话可能对更多人更友好吧 (Gif哪能和Live2D比😂)

感想

这次做这么一个Python程序运行的提示标志还真是复杂啊,尤其是为了跨平台,其实专门对应一个平台做起来可能也没有很复杂,不过想能在各个平台上都能使用还是挺难的。这次来看Qt的跨平台性确实很强,无论是在哪个平台上都能获得不错的体验,就是用起来感觉比较麻烦,其实说来如果能用C++之类的语言去开发Qt程序应该更好,Python这个基本上也就只能当作一个玩具算是熟悉一下Qt的各种功能了。