Custom QQuickItem is not painted - python

I am trying to write my own QQuickItems that use the SceneGraph to draw shapes as if it the parent Item was a Canvas. I am using PySide6 and Python. During my attempts I found a bug in PySide6, which was earlier reported. I downloaded the patch that fixed it and it seems to be ok now (https://bugreports.qt.io/browse/PYSIDE-1345).
My program now compiles and runs, but the node is not painted. If I understand the documentation correctly, I need to do three things to have a custom QQuickItem painted:
Inherit from QQuickItem
Override updatePaintNode
Set the ItemHasContents flag
I did all that, registered my type, added it in QML and made sure it has non-zero dimensions. Unfortunately, it still does not get paited. I do not know what to do next.
Here's the minimal working example:
main.py
# This Python file uses the following encoding: utf-8
import os
from pathlib import Path
import sys
from PySide6.QtGui import QGuiApplication, QColor
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
from PySide6.QtQuick import QQuickItem, QSGGeometryNode, QSGGeometry, QSGFlatColorMaterial
class JustItem(QQuickItem):
def __init__(self, parent=None):
super().__init__(parent)
self.setFlag(QQuickItem.ItemHasContents)
def updatePaintNode(self, node, update_data):
if node is None:
node = QSGGeometryNode()
geometry = QSGGeometry(QSGGeometry.defaultAttributes_Point2D(), 4)
geometry.setDrawingMode(QSGGeometry.DrawTriangles)
vertex_data = geometry.vertexDataAsPoint2D()
vertex_data[0].set(10, 10)
vertex_data[1].set(100, 10)
vertex_data[2].set(100, 100)
vertex_data[3].set(10, 100)
material = QSGFlatColorMaterial()
material.setColor(QColor(255, 0, 0, 127))
node.setGeometry(geometry)
node.setMaterial(material)
return node
if __name__ == "__main__":
qmlRegisterType(JustItem, "PythonTypes", 1, 0, "JustItem")
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load(os.fspath(Path(__file__).resolve().parent / "main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
And QML:
import QtQuick
import QtQuick.Window
import PythonTypes 1.0
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
JustItem {
}
}
The result is an empty white window. When I resize it, it segfaults.

This is a bug in PySide 2. Currently it is not possible to draw your custom QQuickItems in PySide 2.
The bug is discussed in more detail here: https://forum.qt.io/topic/116585/qsggeometry-does-not-work-on-pyside2/16
There's also a bug report, which has a proposed fix. The fix causes segmentation fault: https://bugreports.qt.io/browse/PYSIDE-1345
Unfortunately, if you want to draw custom QQuickItems you either need to write them in C++ or use another GUI framework entirely.

Related

Module "material" is not installed

I am running code on a Windows 7 VM with Qt Creator and I am getting a module "material" is not installed error when running my code. It is initialized with Python 3.8 using Pyside6. My Qt Creator is version 4.15.0
This is the QML code:
import QtQuick 2.0
import QtQuick.Controls 2.12
Item {
width: 640
height: 480
visible: true
Rectangle {
id: rectangle
x: 232
y: 220
width: 200
height: 200
color: "#c96565"
}
}
This is the Python script: It is a modified version of one of the example default scripts.
import os
import sys
import urllib.request
import json
from pathlib import Path
import PySide6.QtQml
from PySide6.QtQuick import QQuickView
from PySide6.QtCore import QStringListModel, Qt, QUrl
from PySide6.QtGui import QGuiApplication
if __name__ == '__main__':
#Set up the application window
app = QGuiApplication(sys.argv)
view = QQuickView()
view.setResizeMode(QQuickView.SizeRootObjectToView)
#Load the QML file
qml_file = Path(__file__).parent / "main.qml"
view.setSource(QUrl.fromLocalFile(os.fspath(qml_file.resolve())))
#Show the window
if view.status() == QQuickView.Error:
sys.exit(-1)
view.show()
#execute and cleanup
app.exec()
del view
I didn't use Qt Quick Control elements in the QMl yet though. Even if I do have a Quick Control elements there the same thing happens. However, if I remove/comment out the import statement for Qt Quick Controls below, the application runs fine. I've tried changing the version number next to quick controls too without success.
UPDATE: It seems that not being able to find the module is a path issue, so I brute forced the path into the qml file by adding this line to the top: import "./Controls.2" as QtQuickControls
And I copied pasted the Controls.2 folder that is nested in the PySide6 folder into the root directory of this project. The error I get now is
The plugin <filepath to qtquickcontrols2plugin.dll> uses incompatible Qt library. <5.15.0> [release] import "./Controls.2" as QtQuickControls
I figured it out: I don't need to brute force the path in; in the .pydevproject file of your python project, make sure you add in the full path to PySide6 because for some reason the system cannot find it without the path.
Something like this:
<pydev_pathproperty name="org.python.pydev.PROJECT_EXTERNAL_SOURCE_PATH">
<path>\path\to\your\python\venv\or\folder\Lib\site-packages\PySide6</path>
</pydev_pathproperty>
If that doesn't work, try calling the Material module directly:
import QtQuick.Controls.Material 2.12

QML findChild from a different component

The objective:
I'm writing a Gui front-end for a Matplotlib-based library for nested samples (pip install anesthetic if you want to have a look).
How I would go about it in C++: My previous experience with QML was a C++ program, where instead of going into QML to find a canvas to render to, I created a C++ object, registered it in QML's type system, and had it behave as a QtQuick controls widget. As far as I know this is the recommended way of doing things: have all the rendering be done in QML, and have all the business-end-logic in C++.
THe best approach and why I can't do it: This approach doesn't work here. AFAIK you can only implement custom QML using C++, and I need for the program to be pure-ish Python (for others to be able to maintain it) some JS is accessible and QML is pretty easy to understand and edit, so I had no objections (C++ was a hard no).
what I got working: I have a working implementation of what I want. It was all in one file. So, naturally I wanted to split the canvas to which I'm drawing to into a separate file: figure.qml. Trouble is, I can't seem to find the object by that name whenever it's loaded from a separate file (the next step is to use a Loader, because the Figure is quite clunky).
I have a two-file project with view.qml being the root, and a component in Figure.qml.
The trouble is, it only works if I load the thing with objectName: "component" in view.qml and not in Component.qml.
So how does one findChild in Pyside for an objectName that's in a different .qml file?
MWE:
main.py
import sys
from pathlib import Path
from matplotlib_backend_qtquick.backend_qtquickagg import FigureCanvasQtQuickAgg
from matplotlib_backend_qtquick.qt_compat import QtGui, QtQml, QtCore
def main():
app = QtGui.QGuiApplication(sys.argv)
engine = QtQml.QQmlApplicationEngine()
displayBridge = DisplayBridge()
context = engine.rootContext()
qmlFile = Path(Path.cwd(), Path(__file__).parent, "view.qml")
engine.load(QtCore.QUrl.fromLocalFile(str(qmlFile)))
win = engine.rootObjects()[0]
if win.findChild(QtCore.QObject, "figure"):
print('success') # This fails
app.exec_()
view.qml
import QtQuick.Controls 2.12
import QtQuick.Windows 2.12
ApplicationWindow{
Figure {
}
}
Figure.qml
import QtQuick.Controls 2.12
import QtQuick 2.12
Component{
Rectangle{
objectName: "figure"
}
}
Component is used to define a QML element, it does not instantiate it, therefore you cannot access the object. Creating a Figure.qml is equivalent to creating a Component, and you are creating a Component inside another Component.
The solution is not to use Component:
Figure.qml
import QtQuick.Controls 2.12
import QtQuick 2.12
Rectangle{
objectName: "figure"
}
But it is not recommended to use objectName since, for example, if you create multiple components, how will you identify which component it is? o If you create the object after a time T, or use Loader or Repeater you will not be able to apply that logic. Instead of them it is better to create a QObject that allows obtaining those objects:
from PySide2 import QtCore
import shiboken2
class ObjectManager(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._qobjects = []
#property
def qobjects(self):
return self._qobjects
#QtCore.Slot(QtCore.QObject)
def add_qobject(self, obj):
if obj is not None:
obj.destroyed.connect(self._handle_destroyed)
self.qobjects.append(obj)
print(self.qobjects)
def _handle_destroyed(self):
self._qobjects = [o for o in self.qobjects if shiboken2.isValid(o)]
# ...
object_manager = ObjectManager()
context = engine.rootContext()
context.setContextProperty("object_manager", object_manager)
qmlFile = Path(Path.cwd(), Path(__file__).parent, "view.qml")
engine.load(QtCore.QUrl.fromLocalFile(str(qmlFile)))
# ...
import QtQuick.Controls 2.12
import QtQuick 2.12
Rectangle{
Component.onCompleted: object_manager.add_qobject(this)
}

QVideoFilterRunnable causes Segmentation Fault with PySide2

I am attempting to show a video feed while processing the image with OpenCV. I already have a script that does the image processing using OpenCV and Python, but attempting to add any filter to the VideoOutput results in a segmentation fault. The documentation only has examples for C++, but I have tried to replicate it exactly in Python.
Here is my Python file (example.py):
from PySide2.QtWidgets import QApplication
from PySide2.QtQml import qmlRegisterType
from PySide2.QtQuick import QQuickView
from PySide2.QtMultimedia import QAbstractVideoFilter, QVideoFilterRunnable
from PySide2.QtCore import QUrl
class ExampleFilterRunnable(QVideoFilterRunnable):
def run(self, frame, surfaceFormat, flags):
return frame
class ExampleFilter(QAbstractVideoFilter):
def createFilterRunnable(self):
return ExampleFilterRunnable()
app = QApplication([])
qmlRegisterType(ExampleFilter, "ExampleFilter", 1, 0, "ExampleFilter")
view = QQuickView()
url = QUrl("example.qml")
view.setSource(url)
view.setResizeMode(view.SizeRootObjectToView)
view.show()
app.exec_()
And the QML (example.qml):
import QtQuick 2.0
import QtQuick.Controls 2.3
import QtMultimedia 5.8
import ExampleFilter 1.0
Rectangle {
id: rectangle
width: 800
height: 600
color: "black"
ExampleFilter {
id: filter
}
MediaPlayer {
id: player
source: "https://archive.org/download/Mario1_500/Mario1_500_LQ.avi"
autoPlay: true
}
VideoOutput {
id: videoOutput
source: player
filters: [filter]
anchors.fill: parent
}
}
I added debug output to try to understand what was happening, as no errors are printed beside "Segmentation fault (core dumped)". The segmentation fault occurs sometime after the ExampleFilterRunnable is instantiated in createFilterRunnable and before ExampleFilterRunnable::run is called.
I have run it on different computers with different video cards, and with different types of videos and camera feeds, and the result is the same. If I comment out the filters: [filter] line in VideoOutput, the video plays.
It appears that this might actually be a bug in PySide2: https://bugreports.qt.io/browse/PYSIDE-785

PyQT5 OpenGL 4.1 Core profile - invalid frame buffer operation - Mac OS

This question is maybe related to another SO question.
I'm running MacOS 10.11.2 El Capitan. Wishing rich GUI features
around my OpenGL applications I decided to give PyQT5 a shot to
create the OpenGL context so I can integrate my OpenGL as a QtWidget int a GUI application.
QT5 provides several API methods for QGlWidget which I summarize shortly here:
initializeGL: gets invoked once before paintGL
paintGL: place to draw stuff to active frame buffer
I'm able to create the widget and initialize shaders etc. But when it comes to framebuffer related operations like glClear an error appears:
File "errorchecker.pyx", line 53, in OpenGL_accelerate.errorchecker._ErrorChecker.glCheckError (src/errorchecker.c:1218)
OpenGL.error.GLError: GLError(
err = 1286,
description = 'invalid framebuffer operation',
baseOperation = glClear,
cArguments = (GL_COLOR_BUFFER_BIT,)
)
I found a website reporting about related issue. It seems that there is no framebuffer configured when the API methods get invoked. As I feel it should be the task of QT I did not try to configure window framebuffer myself. But I found that after some calls of the API methods the framebuffer was created magically. Thus I build a little hack which will wait until paintGL was invoked NSKIP_PAINTGL=3 times. Then I configure my object so the normal paintGL process starts to work. This seems to work. But sometimes it needs more than NSKIP_PAINTGL times, so I included a little sleep within the workaround. QT seems to create the frame buffer a little after it should. Maybe QT does it in a separate thread? The QOpenGLWidget confirm that the framebuffer may not be created at some time:
Returns The frame buffer object handle or 0 if not yet initialized.
I don't like work arounds like this, since I am afraid of raised conditions here. Also I do not have a lot of control here (I need to rely on the fact that QT invokes paintGL often enough in first place so the hack can work). I'm not familiar with QT framework at the moment so here is my question:
How can I create some kinda loop which, when QGLControllerWidget was created, runs updateGL methods covered by a try/catch and retries as long as the GlError appears? Alternatively the loop may listen to QOpenGLWidget::defaultFramebufferObject() and waits for an object handle.
Of course I want to integrate this hack as elegant as possible into QT application flow - doing it the cute way.
Or did I miss something here? Is it possible to setup PyQT in some way so it won't invoke the OpenGL API methods before a valid framebuffer exists?
Here is an isolated code with the hack which runs on my Mac:
from PyQt5 import QtGui, QtCore, QtOpenGL, QtWidgets
from PyQt5.QtOpenGL import QGLWidget
from OpenGL.GL import *
from time import sleep
NSKIP_PAINTGL = 3
class QGLControllerWidget(QGLWidget):
"""
basic test widget: a black screen which clears
framebuffer on paintGL event so it should stay
black on resize and so on.
"""
def __init__(self, format = None):
super(QGLControllerWidget, self).__init__(format, None)
self._weird_pyqt5_framebuffer_hack = 0
# replace paintGL by workaround
self._weird_pyqt5_framebuffer_hack_original_paintGL = self.paintGL
self.paintGL = self._weird_pyqt5_framebuffer_hack_paintGL
def initializeGL(self):
pass
def _weird_pyqt5_framebuffer_hack_paintGL(self):
self._weird_pyqt5_framebuffer_hack += 1
if self._weird_pyqt5_framebuffer_hack < NSKIP_PAINTGL:
return
sleep(0.1)
# restore original paintGL
self.paintGL = self._weird_pyqt5_framebuffer_hack_original_paintGL
self.updateGL()
def paintGL(self):
glClear(GL_COLOR_BUFFER_BIT)
if __name__ == '__main__':
import sys
class QTWithGLTest(QtWidgets.QMainWindow):
def __init__(self, parent = None):
super(QTWithGLTest, self).__init__(parent)
# MacOS core profile 4.1
qgl_format = QtOpenGL.QGLFormat()
qgl_format.setVersion(4, 1)
qgl_format.setProfile(QtOpenGL.QGLFormat.CoreProfile)
qgl_format.setSampleBuffers(True)
self.widget = QGLControllerWidget(qgl_format)
self.setCentralWidget(self.widget)
self.show()
app = QtWidgets.QApplication(sys.argv)
window = QTWithGLTest()
window.show()
app.exec_()
Note that also C++ snippets are welcome as I will try convert then into python.
The QGL* stuff is deprecated in Qt5. That might be the reason why it's calling paintGL() ahead of time. You should try to use QOpenGLWidget or QOpenGLWindow instead.
Alternatively, you could try to use the isValid() method inside paintGL() and bail out early if it returns a falsy value:
def paintGL(self):
if not self.isValid():
return
If you want to try QOpenGLWidget, you can use this as starting point:
#!/usr/bin/env python
from PyQt5.QtGui import (
QOpenGLBuffer,
QOpenGLShader,
QOpenGLShaderProgram,
QOpenGLVersionProfile,
QOpenGLVertexArrayObject,
QSurfaceFormat,
)
from PyQt5.QtWidgets import QApplication, QMainWindow, QOpenGLWidget
class QTWithGLTest(QMainWindow):
"""Main window."""
def __init__(self, versionprofile=None, *args, **kwargs):
"""Initialize with an OpenGL Widget."""
super(QTWithGLTest, self).__init__(*args, **kwargs)
self.widget = QOpenGLControllerWidget(versionprofile=versionprofile)
self.setCentralWidget(self.widget)
self.show()
class QOpenGLControllerWidget(QOpenGLWidget):
"""Widget that sets up specific OpenGL version profile."""
def __init__(self, versionprofile=None, *args, **kwargs):
"""Initialize OpenGL version profile."""
super(QOpenGLControllerWidget, self).__init__(*args, **kwargs)
self.versionprofile = versionprofile
def initializeGL(self):
"""Apply OpenGL version profile and initialize OpenGL functions."""
self.gl = self.context().versionFunctions(self.versionprofile)
if not self.gl:
raise RuntimeError("unable to apply OpenGL version profile")
self.gl.initializeOpenGLFunctions()
self.createShaders()
self.createVBO()
self.gl.glClearColor(0.0, 0.0, 0.0, 0.0)
def paintGL(self):
"""Painting callback that uses the initialized OpenGL functions."""
if not self.gl:
return
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT | self.gl.GL_DEPTH_BUFFER_BIT)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, 3)
def resizeGL(self, w, h):
"""Resize viewport to match widget dimensions."""
self.gl.glViewport(0, 0, w, h)
def createShaders(self):
...
def createVBO(self):
...
if __name__ == '__main__':
import sys
fmt = QSurfaceFormat()
fmt.setVersion(4, 1)
fmt.setProfile(QSurfaceFormat.CoreProfile)
fmt.setSamples(4)
QSurfaceFormat.setDefaultFormat(fmt)
vp = QOpenGLVersionProfile()
vp.setVersion(4, 1)
vp.setProfile(QSurfaceFormat.CoreProfile)
app = QApplication(sys.argv)
window = QTWithGLTest(versionprofile=vp)
window.show()
sys.exit(app.exec_())
Also, you don't need to use the OpenGL functions provided by Qt like I did. You can use the plain functions from OpenGL.GL as well or you can even mix and match (Qt provides some nice classes that make working with GL easier). If you just want to use OpenGL.GL, you can remove everything referring to QOpenGLVersionProfile, versionprofile, versionFunctions, initializeOpenGLFunctions and self.gl.

How to Dynamically Instantiate QML items in Pyside

Building off this Pyside tutorial:
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_1
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_2
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_3
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_4
I am attempting to do everything in Python and not have any java script.
The only difficulty I've run into is when calling the createObject() method of a QDeclarativeComponent which is described nicely as a "Dynamic Object Management" here:
http://qt-project.org/doc/qt-4.8/qdeclarativedynamicobjects.html
So here is a bare bones example that causes the error:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import *
class MainWindow(QDeclarativeView):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle("Main Window")
# Renders game screen
self.setSource(QUrl.fromLocalFile('game2.qml'))
# QML resizes to main window
self.setResizeMode(QDeclarativeView.SizeRootObjectToView)
# a qml object I'd like to add dynamically
self.component = QDeclarativeComponent(QDeclarativeEngine(), QUrl.fromLocalFile("Block2.qml"))
# check if were ready to construct the object
if self.component.isReady():
# create the qml object dynamically
dynamicObject = self.component.createObject(self.rootObject())
if __name__ == '__main__':
# Create the Qt Application
app = QApplication(sys.argv)
# Create and show the main window
window = MainWindow()
window.show()
# Run the main Qt loop
sys.exit(app.exec_())
With main window QML file contents ("game2.qml"):
import QtQuick 1.0
Rectangle {
id: screen
width: 490; height: 720
SystemPalette { id: activePalette }
}
And QML object I'd like to dynamically construct ("Block2.qml"):
import QtQuick 1.0
Rectangle {
id: block
}
When I run this code, it crashes at:
dynamicObject = self.component.createObject(self.rootObject())
with:
TypeError: Unknown type used to call meta function (that may be a signal): QScriptValue
I understand the parent must be a QObject but otherwise I'm not entirely sure from the docs what more it should constitute:
http://srinikom.github.io/pyside-docs/PySide/QtDeclarative/QDeclarativeComponent.html
This isn't an issue in C++ according to:
https://qt-project.org/forums/viewthread/7717
It is clearly only an issue in Pyside currently.
Any idea what might be causing this issue? Potential bug?
A work around is to rely on javascript for object creation while everything else is python. In this implementation you pass the qml file of the component and its parent to the javascript implementation that creates the component. It does a basic construction of the object. Would be ideal having pure python solution though for brevity.
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import *
class MainWindow(QDeclarativeView):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle("Main Window")
# Renders game screen
self.setSource(QUrl.fromLocalFile('game2.qml'))
# QML resizes to main window
self.setResizeMode(QDeclarativeView.SizeRootObjectToView)
# a qml object I'd like to add dynamically
parent = self.rootObject()
view = QDeclarativeView()
view.setSource(QUrl.fromLocalFile("comp_create.qml"))
block = view.rootObject().create("Block2.qml", parent)
print block
block.x = 100
block.y = 200
# prove that we created the object
print block.x, block.y
if __name__ == '__main__':
# Create the Qt Application
app = QApplication(sys.argv)
# Create and show the main window
window = MainWindow()
window.show()
# Run the main Qt loop
sys.exit(app.exec_())
The only added QML is a component creator that uses javascript to create objects since Pyside currently wont work ("comp_create.qml"):
import QtQuick 1.0
Item {
id: creator
function create(qml_fname, parent) {
// for a given qml file and parent, create component
var comp = Qt.createComponent(qml_fname);
if (comp.status == Component.Ready) {
// create the object with given parent
var ob = comp.createObject(parent);
if (ob == null) {
// Error Handling
console.log("Error creating object");
}
return ob
} else if (component.status == Component.Error) {
// Error Handling
console.log("Error loading component:", component.errorString());
}
else {
component.statusChanged.connect(finishCreation);
}
return null
}
}
Note, borrowed this code mostly from:
http://qt-project.org/doc/qt-4.8/qdeclarativedynamicobjects.html

Categories

Resources