Question

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.

Was it helpful?

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
    }
}

OTHER TIPS

Just for fun, and at the risk of downvotes for a completely tangential answer, here's a GameOfLife implemented entirely in QML, just put it in a .qml file and run it with qmlscene. Works on Qt 5.3.0, and runs surprisingly (to me) fast on an old Core 2 Duo lappy. I'm sure it'll never be as fast/efficient as a C++ QQuickImageProvider based solution though, but it does make the point it's possible to do quite a lot in QML without resorting to C++.

import QtQuick 2.2

Rectangle {
  id: main
  width: 640
  height: 640
  color: '#000088'

  Timer {
    interval: 1000/60
    running: true
    repeat: true
    onTriggered: {advance();display();}
  }

  Component {
    id: cellComponent
    Rectangle {
      objectName: 'cell'
      property int row: 0
      property int col: 0      
      x: main.width/2+width*col
      y: main.height/2+height*row      
      width: 5
      height: 5
      radius: 2
      smooth: true
      color: '#ffcc00'
    }
  }

  property var cells: null

  Component.onCompleted: {
    cells=[[-1, 0],[-1, 1],[ 0,-1],[ 0, 0],[ 1, 0]];
    display();
  }

  function display() {
    // Just completely regenerate display field each frame
    // TODO: might be nicer to do differential updates, would allow birth/death animations   

    // Nuke all previously displayed cells
    for (var i=0;i<children.length;i++) {
      if (children[i].objectName=='cell') {
        children[i].destroy();
      }
    }

    // Show current set of cells
    for (var i=0;i<cells.length;i++) {
      var c=cellComponent.createObject(
        main,
        {'row':cells[i][0],'col':cells[i][1]}
      );
    }
  }

  function advance() {

    // Build a hash of the currently alive cells and a neighbour count (includes self)
    var a=new Object;
    var n=new Object;
    for (var i=0;i<cells.length;i++) {
      var p=cells[i]
      var r=p[0];
      var c=p[1];
      if (!(r in a)) a[r]=new Object;
      a[r][c]=1;
      for (var dr=r-1;dr<=r+1;dr++) {
        for (var dc=c-1;dc<=c+1;dc++) {
          if (!(dr in n)) n[dr]=new Object;
          if (!(dc in n[dr])) n[dr][dc]=0;
          n[dr][dc]+=1;
        }
      }
    }

    // For all live cells, assess viability
    var kill=[];
    var stay=[];
    for (var r in a) {
      for (var c in a[r]) {
        if (n[r][c]-1<2 || n[r][c]-1>3)
          kill.push([Number(r),Number(c)]);
        else
          stay.push([Number(r),Number(c)]);
      }
    }

    // For neighbours of live cells, assess potential for births
    var born=[];
    for (var r in n) {
      for (var c in n[r]) {
        if (!((r in a) && (c in a[r]))) {
          if (n[r][c]==3)
            born.push([Number(r),Number(c)]);
        }
      }
    }   

    cells=stay.concat(born)
  }
}

And for a pure QML version using GLSL (via a recursive QML ShaderEffect) to compute the Game of Life rules on GPU see here.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top