Search code examples
pythonpyqtqmlqquickview

How to change source of QQuickView


I am working on game, which is written in qml and pyqt, but should be divided into two windows (Launcher + the Game). What is the proper way to switch between these two qml files? I don´t want to use QmlLoader, because it doesnt resize the window and It needs to many signals! I was trying also this variant:

view.engine().clearComponentCache()
view.setSource(source())

but it didn´t work (qml window stopped working... -classic windows error, however no error was written in pycharm console)

My code looks like this:

from PyQt5.QtCore import pyqtProperty, QRectF, QUrl, QObject, pyqtSignal, pyqtSlot, QVariant
from PyQt5.QtGui import QColor, QGuiApplication, QPainter, QPen
from PyQt5.QtQml import qmlRegisterType
from PyQt5.QtQuick import QQuickItem, QQuickPaintedItem, QQuickView
from PyQt5 import QtNetwork as QN
from PyQt5 import QtCore as QC

from multiprocessing import Process
import server as S
import client as C
from time import time, sleep


class Launcher(QQuickItem):
    PORTS = (9998, 9999)
    PORT = 9999
    SIZEOF_UINT32 = 4

    changeUI = pyqtSignal()
    changeName= pyqtSignal(int, str)
    connection= pyqtSignal(int, bool)
    connected= pyqtSignal(QVariant)


    @pyqtSlot(name="startGame")
    def start_game(self):
        #app.exit()
        startGame()
        print ("startGame")

    @pyqtProperty(str)
    def name(self):
        print ("return name")
        return self._name

    @name.setter
    def name(self, n):
        print ("set name")
        self._name= n


    @pyqtSlot(name= "terminate")
    def terminate_server(self):
        if not self.server: return
        self.server.terminate()     #Bye
        self.server.join()
        #self.bstopServer.setEnabled(False)
        #self.bserver.setEnabled(True)
        self.server = None

    @pyqtSlot()
    def _quit(self):
        if self.server:
            self.server.terminate()     #Bye
            self.server.join()
        app.exit()

    @pyqtSlot()
    def back(self):
        self.client.disconnect()


    def __init__(self, parent=None):
        super(Launcher, self).__init__(parent)

        self.socket= QN.QTcpSocket()        #Yeah, the game will be over internet
        self.server = None
        self.client = None                  #client is a QObject

        self._turnedOn = False
        self._players = 1
        self._name = "YourName"


class Game(QQuickItem):
    def __init__(self, parent= None):
        super(Game, self).__init__(parent)

        self.client = True      #I should get the client from the launcher, but I don´t know how


def startGame():
    view.engine().clearComponentCache()
    view.setResizeMode(QQuickView.SizeViewToRootObject)
    view.showFullScreen()
    view.setSource(
            QUrl.fromLocalFile(
                    os.path.join(os.path.dirname(__file__),'Game.qml')))
    view.show()
    #app.exec_()

def startLauncher():
    view.engine().clearComponentCache()
    view.setResizeMode(QQuickView.SizeViewToRootObject)
    view.setSource(
            QUrl.fromLocalFile(
                    os.path.join(os.path.dirname(__file__),'Launcher.qml')))


    view.show()

    app.exec_()

if __name__ == '__main__':
    import os
    import sys

    app = QGuiApplication(sys.argv)

    qmlRegisterType(Launcher, "ParanoiaLauncher", 1, 0, "App")
    qmlRegisterType(Game, "ParanoiaGame", 1, 0, "App")

    view = QQuickView()

    startLauncher()

As you might see, my structure is kind of chaotic, because I do this switching behaviour for the first time, so I dont really know, how it should be done right... Every advice welcomed! :)


Solution

  • I had to face same problem same time ago. Instead load the same component using the same QQuickView over and over again, I created a component in QML as content container and I load the required component as child of it. Every time a new component is set, we destroy the current one and set the new one as child again.

    // ContentFrame.qml
    Item{
        width: 800
        height: 600
        Item{
            id: contentContainer
            anchors.fill: parent
            anchors.margins: 4
        }
    }
    

    Before anything, forgive me but functional code is written in C++, but I think that the concept can be undertood. I made a shorten version of the process and I hope it can be ported to python.

    To load the ContentFrame component, I used a class derived from QQuickView (ViewManager) that has a method called setView. This method loads a component (Launcher or Game in your case), set it as child of contentContainer and set its anchor.fill to fill the whole parent.

    ViewManager::ViewManager() :  QQuickView("ContentFrame.qml")
    {
        // More ctor code
    }
    
     // Other stuff
    
    void ViewManager::setView(const QString &filename)
    {
        QQuickItem *mostTopItem = rootObject(); //QQuickView::rootObject() method
        QQuickItem *contentItem->findChild<QQuickItem*>("contentContainer");
        if(m_current != NULL)
        {
            m_current->setProperty("visible", false);
        }
    
        // Call a method to load the component and create an instance of it
        QQuickItem *newItem = createOurComponentFromFile(filename);
    
        newItem->setProperty("parent", QVariant::fromValue<QObject*>(contentItem));
    
        // When "delete item" in C++, the object will be destroyed in QQmlEngine
        // in pyqt???
        QQmlEngine::setObjectOwnership(newItem, QQmlEngine::CppOwnership);
    
        newItem->setProperty("anchors.fill", QVariant::fromValue<QObject*>(contentItem));
    
        // Cleanup current object
        if(m_current != NULL)
        {
            delete m_current;
        }
        m_current = newItem;
    }
    

    There are more code, but the heart of the ViewManager is this method. I don't call QQmlEngine::clearComponentCache() here because I load the same components more than once, but in your case it could be a good idea.