我目前正在研究使用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_()
在连接调试器的情况下启动它,我可以调用JavaScript控制台中的backend.debug()
插槽,这导致backend.foo
的值为#34;我修改了foo!&#34;之后,这意味着Python代码成功地改变了JavaScript变量。
但这有点乏味。对于我想分享的每一个价值,我都必须
self._foo
)QObject
正文有没有更简单的方法来实现这一目标?理想情况下某种单线声明?也许使用一个类或函数来打包这一切?我以后如何将其绑定到QObject
?我想的是像
# in __init__
self.foo = SyncedProperty(str)
这可能吗?谢谢你的想法!
答案 0 :(得分:3)
实现此目的的一种方法是使用元类:
SELECT t.res, IF(t.res=0, "zero", "more than zero")
FROM (
SELECT table.*, IF (RAND()<=0.2,1, IF (RAND()<=0.4,2, IF (RAND()<=0.6,3,0))) AS res
FROM table LIMIT 20) t
答案 1 :(得分:2)
基于 ekhumoro 和 Windel(你们都是救星)的出色回答,我制作了一个修改版本:
就像使用 Windel 的版本一样,要使用它,只需将属性指定为类属性,但使用它们的类型而不是值。 (对于从 QObject
继承的自定义用户定义类,请使用 QObject
。)可以在初始化方法中或您需要的任何其他地方分配值。
from PyQt5.QtCore import QObject
# Or for PySide2:
# from PySide2.QtCore import QObject
from properties import PropertyMeta, Property
class Demo(QObject, metaclass=PropertyMeta):
number = Property(float)
things = Property(list)
def __init__(self, parent=None):
super().__init__(parent)
self.number = 3.14
demo1 = Demo()
demo2 = Demo()
demo1.number = 2.7
demo1.things = ['spam', 'spam', 'baked beans', 'spam']
这是代码。我已经使用 Windel 的结构来容纳实例,简化了一些作为 ekhumoro 版本的人工制品保留下来的东西,并添加了一个新类来启用就地修改通知。
# properties.py
from functools import wraps
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
# Or for PySide2:
# from PySide2.QtCore import QObject, Property as pyqtProperty, Signal as pyqtSignal
class PropertyMeta(type(QObject)):
"""Lets a class succinctly define Qt properties."""
def __new__(cls, name, bases, attrs):
for key in list(attrs.keys()):
attr = attrs[key]
if not isinstance(attr, Property):
continue
types = {list: 'QVariantList', dict: 'QVariantMap'}
type_ = types.get(attr.type_, attr.type_)
notifier = pyqtSignal(type_)
attrs[f'_{key}_changed'] = notifier
attrs[key] = PropertyImpl(type_=type_, name=key, notify=notifier)
return super().__new__(cls, name, bases, attrs)
class Property:
"""Property definition.
Instances of this class will be replaced with their full
implementation by the PropertyMeta metaclass.
"""
def __init__(self, type_):
self.type_ = type_
class PropertyImpl(pyqtProperty):
"""Property implementation: gets, sets, and notifies of change."""
def __init__(self, type_, name, notify):
super().__init__(type_, self.getter, self.setter, notify=notify)
self.name = name
def getter(self, instance):
return getattr(instance, f'_{self.name}')
def setter(self, instance, value):
signal = getattr(instance, f'_{self.name}_changed')
if type(value) in {list, dict}:
value = make_notified(value, signal)
setattr(instance, f'_{self.name}', value)
signal.emit(value)
class MakeNotified:
"""Adds notifying signals to lists and dictionaries.
Creates the modified classes just once, on initialization.
"""
change_methods = {
list: ['__delitem__', '__iadd__', '__imul__', '__setitem__', 'append',
'extend', 'insert', 'pop', 'remove', 'reverse', 'sort'],
dict: ['__delitem__', '__ior__', '__setitem__', 'clear', 'pop',
'popitem', 'setdefault', 'update']
}
def __init__(self):
if not hasattr(dict, '__ior__'):
# Dictionaries don't have | operator in Python < 3.9.
self.change_methods[dict].remove('__ior__')
self.notified_class = {type_: self.make_notified_class(type_)
for type_ in [list, dict]}
def __call__(self, seq, signal):
"""Returns a notifying version of the supplied list or dict."""
notified_class = self.notified_class[type(seq)]
notified_seq = notified_class(seq)
notified_seq.signal = signal
return notified_seq
@classmethod
def make_notified_class(cls, parent):
notified_class = type(f'notified_{parent.__name__}', (parent,), {})
for method_name in cls.change_methods[parent]:
original = getattr(notified_class, method_name)
notified_method = cls.make_notified_method(original, parent)
setattr(notified_class, method_name, notified_method)
return notified_class
@staticmethod
def make_notified_method(method, parent):
@wraps(method)
def notified_method(self, *args, **kwargs):
result = getattr(parent, method.__name__)(self, *args, **kwargs)
self.signal.emit(self)
return result
return notified_method
make_notified = MakeNotified()
答案 2 :(得分:0)
感谢您的元类概念,我对其进行了少许修改,以使其能够与包含多个实例的类一起正常使用。我面临的问题是该值存储在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_'
演示用法:
class Demo(QtCore.QObject, metaclass=PropertyMeta):
my_prop = Property(3.14)
demo1 = Demo()
demo2 = Demo()
demo1.my_prop = 2.7