Search code examples
c++qmlqt5qt-quick

QtQuick, Dynamic Images and C++


I'm new to Qt, and from what I've read on qt-project.org and other places; QtQuick seems like an attractive option because of its ability to work on both pointer and touch based devices. My problem is getting it to work well with c++.

I decided to write a variant of Conway's Game of Life as a next step after "Hello World". I am thoroughly mystified as to how to get the "board" -- a [height][width][bytes-per-pixel] array of char -- integrated into the scene graph.

Basically, the process is that the "LifeBoard" iterates through its rules and updates the char*/image. I've got this simple QML:

:::QML
ApplicationWindow {
    id:         life_app_window
    visible:    true
    title: qsTr("Life")

    menuBar: MenuBar {
        Menu {
            title: qsTr("File")
            MenuItem {
                text: qsTr("Quit")
                onTriggered: Qt.quit();
            }
        }
    }

    toolBar: ToolBar {
        id: lifeToolBar;
        ToolButton {
            id: toolButtonQuit
            text: qsTr("Quit")
            onClicked: Qt.quit()
        }
        ToolButton {
            id: toolButtonStop
            text: qsTr("Stop")
            enabled: false
            //onClicked:
        }
        ToolButton {
            id: toolButtonStart
            text: qsTr("Start")
            enabled: true
            //onClicked: //Start life.
        }
        ToolButton {
            id: toolButtonReset
            text: qsTr("Stop")
           // onClicked: //Reset life.
        }
    }

    Flow {
        id: flow1
        anchors.fill: parent
        //*****
        // WHAT GOES HERE
        //*****
    }

    statusBar: StatusBar {
        enabled: false
        Text {
            // Get me from number of iterations
            text: qsTr("Iterations.")
        }
    }
}

I want to image to come from a class with a api kinda like this:

class Life {
    public:
        QImage getImage() {}
        // Or
        char* getPixels(int h, int w, QImage::Format_ARGB8888) {}
}

I have no clue, and hours wading through tutorials did not help. How does one link a char* image in c++ to a ??? in QML so that the QML can start/stop the "Life" loop and so that the "Life" loop and update the char array and notify QML to redraw it?


Note: I've looked at subclassing QQuickImageProvider based on the info here. The problem with this approach is that I cannot see how to let c++ "drive" the on screen image. I wish to pass control from QML to c++ and let c++ tell QML when to update the display with the changed image. Is there a solution with this approach? Or another approach entirely.


Solution

  • First way to do that would be creating a Rectangle for each game pixel in QML, which might be fancy for a 8x8 board, but not for a 100x100 board, since you need to write the QML code manually for each pixel.

    Thus I'd go for images created in C++ and exposed to QML. You call them via an image provider to allow asynchronous loading. Let Life do the logic only.

    The image is called from QML like this:

    Image {
        id: board
        source: "image://gameoflife/board"
        height: 400
        width: 400
    }
    

    Now gameoflife is the name of the image provider and board the so-called id you can use later.

    Register gameoflife in you main.cpp

    LifeImageProvider *lifeIP = new LifeImageProvider(life);
    engine.addImageProvider("gameoflife", lifeIP);
    

    where engine is your main QQmlApplicationEngine and life an instance of your Life game engine.

    LifeImageProvider is your class to create pixeldata. Starts somehow like

    class LifeImageProvider : public QQuickImageProvider
    {
    public:
        LifeImageProvider(Life *myLifeEngine);
        QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize);
    
    private:
        Life *myLifeEngine_;
    };
    

    The important method is requestPixmap, which is called from QML. You need to implement it.

    To refresh the game board when Life sends a stateChanged() signal, expose life as a global object to QML:

    context->setContextProperty("life", &life);
    

    You can bind the signal to QML

    Image {
        id: board
        source: "image://gameoflife/board"
        height: 400
        width: 400
    }
    
    Connections {
        target: life
        onStateChanged: {
            board.source = "image://gameoflife/board?" + Math.random()
            // change URL to refresh image. Add random URL part to avoid caching
        }
    }