制作程序运行提示标志的历程 | Mayx的博客
Author: mayx
Description: 有图形界面的程序可真是难做啊……
Date: 2022-09-05T00:00:00+08:00

5 September 2022 - 字数统计：4273 - 阅读大约需要14分钟 - Hits: Loading...

# 制作程序运行提示标志的历程

by mayx

这是一篇创建于 天前的文章，其中的信息可能已经有所发展或是发生改变。

AI摘要

这篇文章讲述了作者从使用Python的PyQt5库制作程序运行提示图标，遇到体积过大问题，到尝试Pystray库，结果依然不理想，再到最终选择Tkinter和PyQt5实现跨平台悬浮图标过程中的经历。作者在寻求轻量级解决方案时，虽然Tkinter在Windows上表现尚可但在Linux上遇到透明度问题，而Qt5虽能提供跨平台一致性，但学习和使用起来较为复杂。作者对跨平台开发的复杂性以及选择不同工具的权衡有了深入的认识。

- 起因
- 制作的历程
  - 使用托盘区图标
    - 使用PyQt5库实现
    - 使用pystray库实现
  - 制作悬浮图标
    - 通过Tkinter实现
    - 使用PyQt5库实现
- 感想

有图形界面的程序可真是难做啊……

#
起因

最近我做了一个程序，类似于守护进程那样的一个用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的各种功能了。

tags: Python - 程序 - 标志 查看原始文件

#### 推荐文章

Loading...

上一篇：如何避免Cloudflare背后的源站被恶意访问
下一篇：使用Python制作可热载的定时调度器
想问这篇文章
召唤伊斯特瓦尔