pyqt5 pyqtProperty的便捷封装,用于打包读写函数和值改变信号

[toc]

cpp版戳这里

背景

  • 项目用到了pyqt,需要使用qt动态属性来暴露控件的一些样式属性,以便于在qss中一次性解决主题问题

    传统的pyqtProperty定义方式

    • 如下,非常的繁琐,大量都是相同逻辑的重复代码,属性一旦多了就非常不美观,如果是C++ 用Qt Creator还能通过编辑器自动生成一下,pyqt暂时没有找到类似的便捷方法只能手敲,而且python也没有宏这样的代码替换工具,于是以此为动机需要找一个方法来封装冗余的代码块
 class Meal(QObject):

    def __int__(self):
        super(Meal, self).__int__()
        self.temperature = 0

    def setTemperature(self, value: int):
        if value== self.temperature:
            return
        self.temperature = value
        self.temperatureChanged.emit(self.temperature)

    def temperature(self)
        return self.temperature 

    temperatureChanged = pyqtSignal(int)
    temperature = pyqtProperty(type=int, fget=temperature, fset=setTemperature)

网上关于pyqtProperty的描述很少,看来看去就几篇帖子,还是从官方的简陋文档翻译的,看得我头疼,索性就自己研究一下。

实现思路

  1. 从pyqtProperty派生,将setter和getter以及信号都封装到该类中,然后像这样去使用;

    class Meal(QObject):
    
    def __int__(self):
        super(Meal, self).__int__()
    
    temperature = QtNotifyProperty(int)

    世界都清净了,比C++的定义都清晰。

    适用场景

  • 凡是需要定义setter getter signal这三要素的属性的地方,一行代码替代
  • 在同类实例较少的情况下使用,如UI类,单例类等;这是因为实现使用的是Python的Dict,并且是定义在类的静态空间中;一个该类实例对应一个属性,如果同类实例过多的话会影响效率。

    源码

from PyQt5.QtCore import *

class QtNotifyProperty(pyqtProperty):
    class PropertyObject(QObject):
        changed = pyqtSignal(QVariant)

        def __call__(self, *args, **kwargs):
            return self._value

        def __init__(self, PropertyType):
            super(QtNotifyProperty.PropertyObject, self).__init__()
            self._value = PropertyType()

        def set(self, value):
            if value == self._value:
                return
            self._value = value
            self.changed.emit(self._value)

        def bind(self, obj: QObject, propertyName: str, onChange: 'def (src: srcType)->destType' = None):
            if onChange is None:
                self.changed.connect(lambda value: obj.setProperty(propertyName, value))
            else:
                self.changed.connect(lambda value: obj.setProperty(propertyName, onChange(value)))

        def get(self):
            return self._value

    def __set__(self, instance, value):
        self.setPropertyValue(instance, value)

    def __get__(self, instance, owner) -> PropertyObject:
        return self.getPropertyObj(instance)

    def __init__(self, propertyType):
        self._properties: dict[QObject, QtNotifyProperty.PropertyObject] = {}

        def _getPropertyObj(owner: QObject) -> QtNotifyProperty.PropertyObject:
            try:
                return self._properties[owner]
            except KeyError:
                self._properties[owner] = \
                    self.PropertyObject(propertyType)

                def removeProperty():
                    del self._properties[owner]
                    print("remove")

                owner.destroyed.connect(removeProperty)
                return self._properties[owner]

        def _getPropertyValue(owner: QObject):
            return _getPropertyObj(owner)()

        def _setPropertyValue(owner: QObject, value) -> None:
            _getPropertyObj(owner).set(value)

        self.getPropertyObj = _getPropertyObj
        self.setPropertyValue = _setPropertyValue
        super(QtNotifyProperty, self).__init__(type=propertyType, fget=_getPropertyValue,
                                               fset=_setPropertyValue)

class Meal(QObject):

    def __int__(self):
        super(Meal, self).__int__()

    temperature = QtNotifyProperty(int)
    calorie = QtNotifyProperty(int)

def main():
    a = QCoreApplication([])
    meal = Meal()
    meal2 = Meal()
    # 直接 属性.信号名 的方式获取信号
    meal.temperature.changed.connect(lambda value: print("meal on temperature changed:", value))
    meal.calorie.changed.connect(lambda value: print("meal on calorie changed:", value))
    meal2.temperature.changed.connect(lambda value: print("meal2 on temperature changed:", value))
    meal2.calorie.changed.connect(lambda value: print("meal2 on calorie changed:", value))
    # 属性绑定,将meal2和meal对象进行默认的属性绑定
    meal.temperature.bind(meal2, "temperature")
    meal.calorie.bind(meal2, "calorie")
    # setProperty的方式触发值改变信号
    meal.setProperty("temperature", 50)
    meal.setProperty("calorie", 1000)
    # property的方式访问属性值
    print("temperature: ", meal.property("temperature"))
    print("calorie: ", meal.property("calorie"))
    # 赋值的方式触发值改变信号
    meal.temperature = 60
    meal.calorie = 2000
    # 获取属性值的另外两种方式 .get() 和 ()
    print("temperature", meal.temperature())
    print("calorie", meal.calorie.get())

if __name__ == '__main__':
    main()

执行结果

利用宏简化Q_PROPERTY动态属性的定义

[toc]

实现历程

传统定义方式

using type = int;
class Any: public QObject
{
    Q_OBJECT
    Q_PROPERTY(type name READ name WRITE setname NOTIFY nameChanged)
public:
    void setname(const type& name)
    {
        if (m_name == name) return;
        m_name = name;
        emit nameChanged();
    }

    type name() const
    {
        return m_name;
    }
signals:
    void nameChanged();
private: type m_name; 
}

关于Q_PROPERTY这个宏我就不赘述了,传统方式可以写好Q_PROPERTY后用qtcreator右键菜单重构选项生成实现代码,但是换一个编辑器或者要修改的时候就有点恼火了。
简化地思路其实很简单,相信很多人应该跟我有同样的想法,将传统定义的setter和getter以及信号一起打包到一个宏里边去就行了。

预想的方式

#define NOTIFY_PROPERTY(type, name) \
 //读写函数以及信号的定义...

class Any: public QObject
{
    Q_OBJECT
    NOTIFY_PROPERTY(type, name)
}

测试通过的例程

mainwindow.h

#ifndef DEF_NOTIFY_CONNECT
#include <functional>

template<typename T>
struct ptr_traits;

template<typename className>
//!
//! \brief The ptr_traits struct
//! 可以像这样使用 T* p = new ptr_traits<T*>::class_name; 用于提取指针的类型
//!
struct ptr_traits<className*>
{
    using class_name = className;
};

#define DEF_GETTER(type, name, ...)\
    public: type name() const\
{\
    __VA_ARGS__\
    return m_##name;\
    }

#define DEF_SETTER(type, name, ...)\
    public: void set##name(type value)\
{\
    if (m_##name == value)\
    return;\
    m_##name = value;\
    __VA_ARGS__\
    }

#define DEF_MEMBER(type, name, ...)\
    private:\
    int m_##name;\
    __VA_ARGS__

#define DEF_PROPERTY(type, name, ...) \
    DEF_MEMBER(type, name)\
    DEF_GETTER(type, name)\
    DEF_SETTER(type, name)\
    __VA_ARGS__

#define DEF_NOTIFY_CONNECT(type, name)\
    public:\
    template<typename T>\
    void on_##name##_changed(T slotFunc)\
{\
    connect(this, &ptr_traits<decltype(this)>::class_name::name##Changed, slotFunc);\
    }\
    template<typename T>\
    void on_##name##_changed(QObject* recever,T slotFunc)\
{\
    connect(this, &ptr_traits<decltype(this)>::class_name::name##Changed, recever, slotFunc);\
    }

//signals关键字不会被元编译器识别但Q_SIGNAL可以
#define DEF_NOTIFY_PROPERTY(type,  name, ...) \
    Q_SIGNAL void name##Changed();\
    DEF_MEMBER(type, name)\
    DEF_GETTER(type, name)\
    DEF_SETTER(type, name, emit this->name##Changed();)\
    Q_PROPERTY(type name READ name WRITE set##name NOTIFY name##Changed)\
    DEF_NOTIFY_CONNECT(type,  name)\
    __VA_ARGS__\
    static_assert(true,"request a ';'")
#endif // DEF_NOTIFY_PROPERTY

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QDebug>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:

//最终用法
    DEF_NOTIFY_PROPERTY(int, name);
    DEF_NOTIFY_PROPERTY(int, name2);

    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
private:
    Ui::MainWindow *ui;

};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

main.cpp

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    w.on_name_changed(&w, [&w]()//简化版的connect指定接收者
    {
        qDebug() << QStringLiteral("测试1:") << w.name();
    });
    w.on_name2_changed([&]()//简化版的connect不指定接收者
    {
        qDebug() << QStringLiteral("测试2:") << w.name2();
        w.setProperty("name", 3);
    });
    w.setProperty("name", 1);
    w.setProperty("name2", 2);

    return a.exec();
}

执行结果

Qt6

qt6似乎有其他的解决方案感兴趣戳这里可能需要魔法

更新

经评论区老哥提醒发现把signals关键字替换为Q_SIGNAL后,可以定义到我们的宏里完成我预想的用法 。

记录目前使用vue3.2.0&&vite4.2.0时遇到的一些坑-持续更新

[toc]

vue3安装markdown-it

vue ts使用markdown-it需要使用以下命令安装才行

npm install --save markdown-it @vue/compiler-sfc

来源https://juejin.cn/s/vue3%20markdown-it

link中引用node_modules中css样式打包后丢失

在main.ts中导入该样式重新打包即可,猜测应该是因为路径引用并不会触发vite的打包逻辑

在link中引用css最好还是直接用生产域名直接方便有效,vite似乎可以配置环境变量来区分生产环境和测试环境,不过我的项目不复杂,直接用配置文件来区分就好了

arm平台npm run dev时报错 at Object.networkInterfaces

error when starting dev server:
SystemError [ERR_SYSTEM_ERROR]: A system error occurred: uv_interface_addresses returned Unknown system error 13 (Unknown system error 13)
    at Object.networkInterfaces (node:os:277:16)
    at resolveServerUrls (file:///root/work/product/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:12512:28)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.listen (file:///root/work/product/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:65106:39)
    at async CAC.<anonymous> (file:///root/work/product/node_modules/vite/dist/node/cli.js:776:9)

修改 /vite.config.ts 从文件首行添加以下代码

//解决arm平台报错问题
try {
  require('os').networkInterfaces()
} catch (e) {
  require('os').networkInterfaces = () => ({})
}

打包发布到nginx的子路由时无法访问,如https://xxx.com/product/

  1. 修改 /<项目路径>/router/index.ts文件如下,原理尚未可知

     const router = createRouter({
      history: createWebHashHistory(import.meta.env.VITE_BASE_PATH),
      routes,
    })
    
  2. 修改 /vite.config.ts 如下添加base属性为你的项目真实域名https://xxx.com/product/

    export default defineConfig(({ command, mode }) => {
      return {
        base: "https://xxx.com/product/"
        }
    }
  3. niginx配置,任意server 中添加一下配置

    location /product {
            alias <你的网站根目录>;  # 网站根目录 如 /www/xxx/product
            index index.html;   # 默认首页文件
    }

关于属性绑定界面不自动刷新问题、

使用以下两种方式创建的属性在修改后可以稳定触发控件的重新渲染

  1. 对于用于对象的绑定需使用reactive()包裹,如下

    let items = reactive(Array<any>())
    //创建后正常修改即可触发界面刷新
  2. 对于用于值的绑定须使用ref()进行创建,如下

    //创建方法
    const height = ref(window.innerHeight - heightOffset)
    //设置新的值方法
    height.value = window.innerHeight - heightOffset

ref()获取DOM元素对象的方法

<template>
<div ref="test">
</div>
</template>
<script lang='ts' setup>
const test = ref()

function dosomething(){
//确保dom渲染完成后调用
const element = test.value
}
</script>

$on的最轻量级替代方法使用document.dispatchEvent()

//创建自定义事件
const streamDoneEvent = new Event('eventName')||new CustomEvent('eventName',{detail:{/*any obj*/}})
//发射自定义事件
document.dispatchEvent(streamDoneEvent)
//监听自定义事件
document.addEventListener('streamDone', onSteamDone, false)

yuv422转rgb565

[toc]

大概流程

出队后,根据以获得的index读出对映的YUV数据即buffers[index]中的数据
//Yuv422所占内存为w*h*2Byte
//每两个像素点共享一个色度(U,V),因此每次循环处理两个像素,即每次处理4Byte
For(in = 0; in < w*h*2;in +=4)
{
//通过位运算分别得到无符号整型的Y0,Y1,U,V值
//转rgb565
}

//放大数据使其适应屏幕,一对多copy,w:160->480 h:120->272

YUV转RGB565

将YUV422转RGB565:
//Argments:
short Y,U,V
short R,G,B;
//转RGB888,未量化
R = Y +((360 * (V - 128)) >> 8);
G = Y - (((88*(U - 128) + 184*(V-128))) >> 8);
B = Y + ((455 * (U - 128)) >> 8);
//取0~255之间的值
……
//转RGB565
//忽略后3位左移11位,屏蔽后11位
R = ((R >> 3)) << 11) & 0xF800;
//忽略后2位左移5位,屏蔽前5位和后5位
G = ((G >> 2) << 5) & 0x07E0;
//忽略后3位,屏蔽前11位
B = (B >> 3) & 0x001F;
return (unsigned short)(R | G | B);