PyQt5桌面应用系列

鼠标不要钱,手腕还不要钱吗?

每当打工人开始摸鱼的时候,打工人就会摸鱼。反正老板买的鼠标不要钱,我们来摸一个鼠标点击游戏的大鱼。我们要做个按钮精灵在屏幕上跑来跑去,鼠标点一下,另外一个数字精灵就显示点击次数。这样我们就可以开心的测试自己准确操作鼠标的能力,可以采取计时点击也可以采取点击FF次的时间。

游戏界面大概就是这样,由于录屏软件的功能和保密的原因,除了两个跑来跑去的烦人精之外的地方都被录成黑色,实际运行中,这两玩意就在当前显示屏的最上层跑来跑去。

PyQt5桌面应用开发(11):摸鱼也要讲基本法之桌面精灵-LMLPHP

当时鼠标不要钱 and 手腕也不要钱的时候,就可以玩出来这样的场景。程序键盘控制参见:

PyQt5桌面应用开发(11):摸鱼也要讲基本法之桌面精灵-LMLPHP

这个程序有几个要点:

  1. 报表:点击次数显示在QLCDNumber中,当然我们都认识16进制,那就显示16进制,显得更酷!
  2. 数据:点击次数的数据就记录在QLCDNumber的值中。
  3. 交互要求:没有窗口,点击按钮在窗口范围内随机跑,计数控件追着点击按钮跑。

PyQt5源程序

这里首先就把代码发出来。

python文件

import importlib
import subprocess
import sys
from random import randint

from PyQt5 import uic
from PyQt5.QtCore import QSize, Qt, QFile, QIODevice, QRect
from PyQt5.QtGui import QIcon, QKeyEvent, QKeySequence
from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, QLCDNumber, QStyleFactory


# pyinstaller counting.py -w -y --add-data "countingmainwindow.ui;." --add-data "resources_rc.py;."
# produce distributed version in windows, run it first to get resouces_rc.py

# workaround with qrc
def make_rc(fn: str):
    if fn.endswith(".qrc"):
        name, _ = fn.split(".")
        with subprocess.Popen(["pyrcc5", fn, "-o", f"{name}_rc.py"], stdout=subprocess.PIPE) as proc:
            msg = proc.stdout.read()
            if len(msg) > 0:
                raise IOError(f"error compile qrc file: {msg}")


# load it or compile and load it
try:
    importlib.import_module(f"resources_rc")
except:
    make_rc("resources.qrc")
    importlib.import_module(f"resources_rc")


class CountingMainWindow(QMainWindow):
    def __init__(self):
        super(CountingMainWindow, self).__init__()

        uic.loadUi("countingmainwindow.ui", self)
        self.pushButton: QPushButton
        self.lcdNumber: QLCDNumber

        self.pushButton.setObjectName("btn")
        self.pushButton.setIconSize(QSize(48, 48))
        self.pushButton.setIcon(QIcon(":imgs/click.png"))
        self.pushButton.setText("")

        self.pushButton.setFixedSize(100, 100)
        self.lcdNumber.setFixedSize(100, 100)
        self.lcdNumber.setDigitCount(2)
        self.lcdNumber.setHexMode()

        self.pushButton.clicked.connect(self.move_widgets_like_crazy)
        self.pushButton.clicked.connect(lambda check: self.lcdNumber.display(self.lcdNumber.intValue() + 1))
        self.lcdNumber.overflow.connect(lambda: self.lcdNumber.display(0))

        self.setMenuBar(None)
        self.setStatusBar(None)

        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setWindowFlags(Qt.FramelessWindowHint)
        self.setWindowState(Qt.WindowFullScreen)

    def move_widgets_like_crazy(self, check: bool):
        self.pushButton: QPushButton
        self.lcdNumber: QLCDNumber

        # geometry return position related to its parent
        # relocated pushbutton
        rect_window: QRect = self.geometry()
        rect_button: QRect = self.pushButton.geometry()
        x = randint(10, rect_window.width() - rect_button.width() - 10)
        y = randint(10, rect_window.height() - rect_button.height() - 10)
        rect_button.setX(x)
        rect_button.setY(y)
        self.pushButton.setGeometry(rect_button)

        # lcdnumber moving toward pushbutton
        rect_lcd: QRect = self.lcdNumber.geometry()
        dx = rect_button.x() - rect_lcd.x()
        rect_lcd.setX(rect_lcd.x() + dx // 3)
        dy = rect_button.y() - rect_lcd.y()
        rect_lcd.setY(rect_lcd.y() + dy // 3)
        self.lcdNumber.setGeometry(rect_lcd)

    def keyPressEvent(self, event: QKeyEvent) -> None:
        if event.matches(QKeySequence.Cancel):
            self.close()
        if event.key() == Qt.Key_C:
            self.pushButton.clicked.emit(False)
        if event.key() == Qt.Key_R:
            self.lcdNumber.display(0)
        super(CountingMainWindow, self).keyPressEvent(event)


def read_qss_file(filename: str) -> str:
    file = QFile(filename)
    if not file.open(QIODevice.ReadOnly | QIODevice.Text):
        raise IOError(f"Cannot open file: {filename}")
    qss = str(file.readAll(), encoding="utf-8")
    return qss


if __name__ == '__main__':
    app = QApplication([])
    app.setStyle(QStyleFactory.create("Fusion"))
    app.setStyleSheet(read_qss_file(":stylesheets/style.qss"))
    mw = CountingMainWindow()
    mw.show()
    sys.exit(app.exec_())

资源定义

程序还包括了一个资源文件(附带一个png的图标,一个qss文件)和一个界面的ui文件。


<RCC>
    <qresource>
        <file>imgs/click.png</file>
        <file>stylesheets/style.qss</file>
    </qresource>
</RCC>

图标放于imgs/目录,qss文件放于stylesheets/目录。

PyQt5桌面应用开发(11):摸鱼也要讲基本法之桌面精灵-LMLPHP

QPushButton#btn {
    background-color: "cyan";
    border-radius: 15px;
    border: 1px solid black;
    padding: 5px;
}

QPushButton#btn:hover {
    background-color: "cyan";
    border-radius: 15px;
    border: 1px solid red;
    padding: 5px;
    padding-bottom: 1px;
}

QPushButton#btn:pressed {
    background-color: "#8E0000";
    border-radius: 15px;
    border: 1px solid red;
    padding: 5px;
    padding-bottom: 3px;
}

QLCDNumber {
    background-color: "lightgray";
    color: #FE5F01;
    border-radius: 15px;
    border: 1px solid black;
    padding: 5px;
}

程序运行时,如果pyrcc5在目录,那么很简单,只要上面的三个文件按照qrc中的目录放置,就能自动产生resource_rc.py,并且自动导入。

pyrcc5 resouces.qrc -o resources_rc.py

界面定义文件

designer生成的ui文件最简单,就随便拉一个QMainWindow,上面丢一个按钮QPushButton,一个LCDNumber,保存就行。

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
    <class>my_ui::CountingMainWindow</class>
    <widget class="QMainWindow" name="my_ui::CountingMainWindow">
        <property name="geometry">
            <rect>
                <x>0</x>
                <y>0</y>
                <width>803</width>
                <height>402</height>
            </rect>
        </property>
        <property name="windowTitle">
            <string>CountingMainWindow</string>
        </property>
        <widget class="QWidget" name="centralwidget">
            <widget class="QPushButton" name="pushButton">
                <property name="geometry">
                    <rect>
                        <x>10</x>
                        <y>10</y>
                        <width>136</width>
                        <height>41</height>
                    </rect>
                </property>
                <property name="sizePolicy">
                    <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
                        <horstretch>0</horstretch>
                        <verstretch>0</verstretch>
                    </sizepolicy>
                </property>
                <property name="font">
                    <font>
                        <pointsize>24</pointsize>
                    </font>
                </property>
                <property name="text">
                    <string>Clicked:</string>
                </property>
            </widget>
            <widget class="QLCDNumber" name="lcdNumber">
                <property name="geometry">
                    <rect>
                        <x>410</x>
                        <y>180</y>
                        <width>64</width>
                        <height>23</height>
                    </rect>
                </property>
                <property name="sizePolicy">
                    <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
                        <horstretch>0</horstretch>
                        <verstretch>0</verstretch>
                    </sizepolicy>
                </property>
                <property name="font">
                    <font>
                        <pointsize>24</pointsize>
                    </font>
                </property>
                <property name="smallDecimalPoint">
                    <bool>false</bool>
                </property>
                <property name="mode">
                    <enum>QLCDNumber::Hex</enum>
                </property>
            </widget>
        </widget>
        <widget class="QStatusBar" name="statusbar"/>
    </widget>
    <resources/>
    <connections/>
</ui>

PyQt5桌面应用开发(11):摸鱼也要讲基本法之桌面精灵-LMLPHP

技术要素

这个游戏中展示了大量好玩的东西。

资源文件

资源文件的定义和访问是一个很好的方式,pyrcc5命令把图形文件、数据文件编译成二进制的形式,在程序中就能够以:imgs/click.png:stylesheets/style.qss
这样的形式访问。在打包文件或者提供程序的时候,只需要提供pyrcc5生成的py文件就行。

这里值得注意的时读入qss文件的那个函数。

    with open(":stylesheets/style.qss") as fid:
    print(fid.read())

经过测试是不行的,QFile才行。但是QFile读出来的是QByteArray,要用str来转换成字符串。

def read_qss_file(filename: str) -> str:
    file = QFile(filename)
    if not file.open(QIODevice.ReadOnly | QIODevice.Text):
        raise IOError(f"Cannot open file: {filename}")
    qss = str(file.readAll(), encoding="utf-8")
    return qss

图形文件用QIcon(":imgs/click.png")可以访问。

StyleSheets

这个程序里面采用了setStyleSheet的方式来设置控件的显示方式,也是一个很有意思的话题。因为这个话题很大,就不再这里详细讲述。

QMainWindow设置

为了设置成背景透明,全屏疯跑,对QMainWindow进行了设置。

self.setAttribute(Qt.WA_TranslucentBackground)
self.setWindowFlags(Qt.FramelessWindowHint)
self.setWindowState(Qt.WindowFullScreen)

第一个函数是属性,WindowAttribute,这组枚举类型用WA开头,背景透明就是其中之一。这个Attribute列表那么长,我就不列了:Attribute

setWindowFlags是设置窗口的特性。枚举列表在窗体特性

setWindowState是设置窗口的状态。

  • Qt::WindowNoState 常规状态
  • Qt::WindowMinimized 最小化状态
  • Qt::WindowMaximized 最大化状态
  • Qt::WindowFullScreen 全屏状态
  • Qt::WindowActive 活动状态(例如键盘输入焦点)

这三个状态一设,就有了我们的按钮精灵可以到处跑。

窗体几何

这段让按钮到处跑的代码中使用geometrysetGeometry这一对函数来实现。这一对函数获得的是一个QRect,包括x,y,w,h,位置坐标(左上角)和长宽。这些尺寸都是相对于夫控件来定义的。

def move_widgets_like_crazy(self, check: bool):
    self.pushButton: QPushButton
    self.lcdNumber: QLCDNumber

    # geometry return position related to its parent
    # relocated pushbutton
    rect_window: QRect = self.geometry()
    rect_button: QRect = self.pushButton.geometry()
    x = randint(10, rect_window.width() - rect_button.width() - 10)
    y = randint(10, rect_window.height() - rect_button.height() - 10)
    rect_button.setX(x)
    rect_button.setY(y)
    self.pushButton.setGeometry(rect_button)

    # lcdnumber moving toward pushbutton
    rect_lcd: QRect = self.lcdNumber.geometry()
    dx = rect_button.x() - rect_lcd.x()
    rect_lcd.setX(rect_lcd.x() + dx // 3)
    dy = rect_button.y() - rect_lcd.y()
    rect_lcd.setY(rect_lcd.y() + dy // 3)
    self.lcdNumber.setGeometry(rect_lcd)

结论

  1. 实现桌面精灵在PyQt5里面很简单,设置窗口的属性就可以;
  2. PyQt5可以把资源文件整合到程序中,作为一个py文件,在Qt5中就直接编译到exe中;
  3. 控件的直接定位用geometrysetGoemetry完成,x,y的数值是相对于父节点的左上角定义的。

05-09 12:36