如何动态创建 PyQt 属性

Jer*_*imo 6 python properties pyqt

我目前正在寻找一种使用 PyQt5 的QWebEngineView使用 Python 和 HTML/CSS/JS 创建 GUI 桌面应用程序的方法

在我的小演示应用程序中,我使用QWebChannel发布 Python QObject到 JavaScript 端,以便数据可以共享和来回传递。到目前为止,共享和连接插槽和信号工作正常。

我在同步简单(属性)值时遇到了困难。从我读过的内容来看,要走的路是通过装饰的 getter 和 setter 函数在共享 QObject 中实现一个 pyqtProperty,并在 setter 中发出一个额外的信号,用于在值发生变化时通知 JavaScript。下面的代码显示了这一点,到目前为止它工作正常:

import sys
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal 
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage


class HelloWorldHtmlApp(QWebEngineView):
    html = '''
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8"/>        
        <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
        <script>
        var backend;
        new QWebChannel(qt.webChannelTransport, function (channel) {
            backend = channel.objects.backend;
        });
        </script>
    </head>
    <body> <h2>HTML loaded.</h2> </body>
    </html>
    '''

    def __init__(self):
        super().__init__()

        # setup a page with my html
        my_page = QWebEnginePage(self)
        my_page.setHtml(self.html)
        self.setPage(my_page)

        # setup channel
        self.channel = QWebChannel()
        self.backend = self.Backend(self)
        self.channel.registerObject('backend', self.backend)
        self.page().setWebChannel(self.channel)

    class Backend(QObject):
        """ Container for stuff visible to the JavaScript side. """
        foo_changed = pyqtSignal(str)

        def __init__(self, htmlapp):
            super().__init__()
            self.htmlapp = htmlapp
            self._foo = "Hello World"

        @pyqtSlot()
        def debug(self):
            self.foo = "I modified foo!"

        @pyqtProperty(str, notify=foo_changed)
        def foo(self):            
            return self._foo

        @foo.setter
        def foo(self, new_foo):            
            self._foo = new_foo
            self.foo_changed.emit(new_foo)


if __name__ == "__main__":
    app = QApplication.instance() or QApplication(sys.argv)
    view = HelloWorldHtmlApp()
    view.show()
    app.exec_()
Run Code Online (Sandbox Code Playgroud)

在连接调试器的情况下开始这个,我可以backend.debug()在 JavaScript 控制台中调用插槽,这会导致值backend.foo“我修改了 foo!” 之后,这意味着 Python 代码成功地更改了 JavaScript 变量。

不过这有点乏味。对于我想分享的每一个价值,我都必须

  • 创建一个内部变量(这里self._foo
  • 创建一个 getter 函数
  • 创建一个 setter 函数
  • QObject身体内产生信号
  • 在 setter 函数中显式地发出这个信号

有没有更简单的方法来实现这一目标?理想情况下某种单行声明?也许使用类或函数来打包所有内容?我将如何将其绑定到QObject稍后?我在想像

# in __init__
self.foo = SyncedProperty(str)
Run Code Online (Sandbox Code Playgroud)

这可能吗?谢谢你的想法!

ekh*_*oro 7

一种方法是使用元类:

class Property(pyqtProperty):
    def __init__(self, value, name='', type_=None, notify=None):
        if type_ and notify:
            super().__init__(type_, self.getter, self.setter, notify=notify)
        self.value = value
        self.name = name

    def getter(self, inst=None):
        return self.value

    def setter(self, inst=None, value=None):
        self.value = value
        getattr(inst, '_%s_prop_signal_' % self.name).emit(value)

class PropertyMeta(type(QObject)):
    def __new__(mcs, name, bases, attrs):
        for key in list(attrs.keys()):
            attr = attrs[key]
            if not isinstance(attr, Property):
                continue
            value = attr.value
            notifier = pyqtSignal(type(value))
            attrs[key] = Property(
                value, key, type(value), notify=notifier)
            attrs['_%s_prop_signal_' % key] = notifier
        return super().__new__(mcs, name, bases, attrs)

class HelloWorldHtmlApp(QWebEngineView):
    ...
    class Backend(QObject, metaclass=PropertyMeta):
        foo = Property('Hello World')

        @pyqtSlot()
        def debug(self):
            self.foo = 'I modified foo!'
Run Code Online (Sandbox Code Playgroud)


Win*_*del 5

感谢您的元类想法,我稍微修改了它,以便与包含多个实例的类正常工作。我面临的问题是该值存储在其Property自身中,而不是存储在类实例属性中。为了清楚起见,我还将Property全班分成了两个班级。


class PropertyMeta(type(QtCore.QObject)):
    def __new__(cls, name, bases, attrs):
        for key in list(attrs.keys()):
            attr = attrs[key]
            if not isinstance(attr, Property):
                continue
            initial_value = attr.initial_value
            type_ = type(initial_value)
            notifier = QtCore.pyqtSignal(type_)
            attrs[key] = PropertyImpl(
                initial_value, name=key, type_=type_, notify=notifier)
            attrs[signal_attribute_name(key)] = notifier
        return super().__new__(cls, name, bases, attrs)


class Property:
    """ Property definition.

    This property will be patched by the PropertyMeta metaclass into a PropertyImpl type.
    """
    def __init__(self, initial_value, name=''):
        self.initial_value = initial_value
        self.name = name


class PropertyImpl(QtCore.pyqtProperty):
    """ Actual property implementation using a signal to notify any change. """
    def __init__(self, initial_value, name='', type_=None, notify=None):
        super().__init__(type_, self.getter, self.setter, notify=notify)
        self.initial_value = initial_value
        self.name = name

    def getter(self, inst):
        return getattr(inst, value_attribute_name(self.name), self.initial_value)

    def setter(self, inst, value):
        setattr(inst, value_attribute_name(self.name), value)
        notifier_signal = getattr(inst, signal_attribute_name(self.name))
        notifier_signal.emit(value)

def signal_attribute_name(property_name):
    """ Return a magic key for the attribute storing the signal name. """
    return f'_{property_name}_prop_signal_'


def value_attribute_name(property_name):
    """ Return a magic key for the attribute storing the property value. """
    return f'_{property_name}_prop_value_'
Run Code Online (Sandbox Code Playgroud)

演示用法:


class Demo(QtCore.QObject, metaclass=PropertyMeta):
    my_prop = Property(3.14)

demo1 = Demo()
demo2 = Demo()
demo1.my_prop = 2.7

Run Code Online (Sandbox Code Playgroud)