Integrating a PyQt3D window into a QMainWindow

2019-07-29 18:26发布

问题:

We can use QWidget.createWindowContainer to add a 3D view into a QMainWindow (a window with menus, status bar, etc).

However, I found that this approach does not work, the windows opens up but fails to render the 3D contents.

It also displays the error

QOpenGLContext::swapBuffers() called with non-exposed window, behavior is undefined

Here is a sample code that compares this approach with a native PyQt3D (that is not capable of using QWidgets to build a complete UI)

from PyQt5.QtWidgets import QMainWindow, QAction, QApplication, QWidget, QPushButton, qApp, QLabel, QHBoxLayout, QVBoxLayout, QSplitter
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QImage, QMatrix4x4, QQuaternion, QVector3D, QColor, QGuiApplication
from PyQt5.QtCore import QSize, Qt
import sys
from PyQt5.Qt3DCore import QEntity, QTransform, QAspectEngine
from PyQt5.Qt3DRender import QCamera, QCameraLens, QRenderAspect
from PyQt5.Qt3DInput import QInputAspect
from PyQt5.Qt3DExtras import QForwardRenderer, QPhongMaterial, QCylinderMesh, QSphereMesh, QTorusMesh, Qt3DWindow, QOrbitCameraController

class View3D(QWidget):
    def __init__(self):
        super(View3D, self).__init__()
        self.view = Qt3DWindow()
        self.container = self.createWindowContainer(self.view)

        vboxlayout = QHBoxLayout()
        vboxlayout.addWidget(self.container)
        self.setLayout(vboxlayout)

        scene = createScene()

        # Camera.
        initialiseCamera(self.view, scene)

        self.view.setRootEntity(scene)

def initialiseCamera(view, scene):
    # Camera.
    camera = view.camera()
    camera.lens().setPerspectiveProjection(45.0, 16.0 / 9.0, 0.1, 1000.0)
    camera.setPosition(QVector3D(0.0, 0.0, 40.0))
    camera.setViewCenter(QVector3D(0.0, 0.0, 0.0))

    # For camera controls.
    camController = QOrbitCameraController(scene)
    camController.setLinearSpeed(50.0)
    camController.setLookSpeed(180.0)
    camController.setCamera(camera)

def createScene():
    # Root entity.
    rootEntity = QEntity()

    # Material.
    material = QPhongMaterial(rootEntity)

    # Torus.
    torusEntity = QEntity(rootEntity)
    torusMesh = QTorusMesh()
    torusMesh.setRadius(5)
    torusMesh.setMinorRadius(1)
    torusMesh.setRings(100)
    torusMesh.setSlices(20)

    torusTransform = QTransform()
    torusTransform.setScale3D(QVector3D(1.5, 1.0, 0.5))
    torusTransform.setRotation(
            QQuaternion.fromAxisAndAngle(QVector3D(1.0, 0.0, 0.0), 45.0))

    torusEntity.addComponent(torusMesh)
    torusEntity.addComponent(torusTransform)
    torusEntity.addComponent(material)

    # Sphere.
    sphereEntity = QEntity(rootEntity)
    sphereMesh = QSphereMesh()
    sphereMesh.setRadius(3)

    sphereEntity.addComponent(sphereMesh)
    sphereEntity.addComponent(material)

    return rootEntity

class Application(QMainWindow):
    def __init__(self):
        super().__init__()
        #
        view3d = View3D()
        self.setCentralWidget(view3d)
        self.show()

# Approach 1 - Integrate Qt3DWindow into a QMainWindow
if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Application()
    sys.exit(app.exec_())

'''
# Approach 2 - A native Qt3DWindow
if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    view = Qt3DWindow()

    scene = createScene()
    initialiseCamera(view, scene)

    view.setRootEntity(scene)
    view.show()

    sys.exit(app.exec_())
'''

Any ideas on how to correctly use QWidget.createWindowContainer for adding a 3D view to a classic QMainWindow ?

回答1:

In Approach 1, your scene variable in View3D goes out of scope as soon as the __init__() method ends. It is then garbage collected by Python, leaving nothing to be displayed.

In Approach 2, scene remains in scope throughout the execution of the program, so there's no problem.

I changed the three occurrences of scene in __init__() to self.scene, thereby keeping a reference to it. Approach 1 and Approach 2 now both work as expected.

Approach 1 still gives the error

QOpenGLContext::swapBuffers() called with non-exposed window, behavior is undefined

but it doesn't seem to cause problems.