Reputation: 175
I am fairly new to Qt / QtQuick and I have to develop an application, which uses some sensor data that is received in a different thread over network periodically. This data should be used inside c++ for calculations and the latest data should also be displayed in QML. Everything is setup to be thread-safe inside c++ by using a mutex for protection and the data is visibly updated inside QML. However, I have some concerns about thread-safety on the QML side and I cannot find information or an example on the web about this topic. Specifically I am concerned about returning a pointer (which was the only way to return a C++ object to QML I guess) instead of a value and therefore a copy of the object. Here is a minimal example demonstrating the concern:
// File data.h
#include <QObject>
class Data : public QObject {
Q_OBJECT
Q_PROPERTY(QString someData READ someData WRITE setSomeData NOTIFY someDataChanged)
public:
explicit Data(QObject* parent = nullptr)
:QObject(parent)
{
}
QString someData() const {
return _someData;
}
void setSomeData(const QString& value) {
if (_someData != value) {
_someData = value;
emit someDataChanged();
}
}
signals:
void someDataChanged();
private:
QString _someData;
}; // Data
// File: controller.h
#include <QObject>
#include <thread>
class Controller : public QObject {
Q_OBJECT
Q_PROPERTY(Data data READ data NOTIFY dataChanged)
public:
explicit Controller(QObject* parent = nullptr)
:QObject(parent)
,_running(false)
,_data(nullptr)
{
_data = new Data();
}
virtual ~Controller() {
delete _data;
}
void start() {
_running = true;
_thread = std::thread([this]() { _threadFunc(); });
}
void stop() {
_running = false;
if (_thread.joinable()) {
_thread.join();
}
}
Data* data() {
return _data;
}
signals:
void dataChanged();
private:
void _threadFunc() {
while (_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
_data.setSomeData("foo");
emit dataChanged();
}
}
bool _running;
std::thread _thread;
Data* _data;
}; // Controller
// File main.qml
import QtQuick 2.0
Rectangle {
width: 100
height: 100
Text {
anchors.centerIn: parent
text: Controller.data.someData
}
}
Data is a simple container holding a QString as property. The controller contains the property data and starts a thread that periodically updates data and emits changes as signal. The output will be displayed correctly, but it feels pretty unsafe to return a raw pointer. So my questions here are:
Upvotes: 3
Views: 1232
Reputation: 175
Well, I think I found a solution to my problem: I am still sure that working on the same object will cause issues. I read a bit about QML ownership and found out that by using a Property, the ownership remains at the C++ side. By using a function returning a pointer, QML takes over ownership and will take care to delete the object later on. So what I did here was following if someone encounters the same issue some day:
// File data.h
#include <QObject>
class Data : public QObject {
Q_OBJECT
Q_PROPERTY(QString someData READ someData WRITE setSomeData NOTIFY someDataChanged)
public:
explicit Data(QObject* parent = nullptr)
:QObject(parent)
{
}
Data(const Data& data)
:QObject(data.parent)
,_someData(data.someData)
{
}
QString someData() const {
return _someData;
}
void setSomeData(const QString& value) {
if (_someData != value) {
_someData = value;
emit someDataChanged();
}
}
signals:
void someDataChanged();
private:
QString _someData;
}; // Data
// File: controller.h
#include <QObject>
#include <thread>
#include <mutex> // New
class Controller : public QObject {
Q_OBJECT
//Q_PROPERTY(Data data READ data NOTIFY dataChanged) // Removed
public:
explicit Controller(QObject* parent = nullptr)
:QObject(parent)
,_running(false)
,_data(nullptr)
{
_data = new Data();
}
virtual ~Controller() {
delete _data;
}
void start() {
_running = true;
_thread = std::thread([this]() { _threadFunc(); });
}
void stop() {
_running = false;
if (_thread.joinable()) {
_thread.join();
}
}
Q_INVOKABLE Data* data() { // Modified to be an invokable function instead of a property getter
std::lock_guard<std::mutex> lock(_mutex); // New
return new Data(*_data); // New
}
signals:
void dataChanged();
private:
void _threadFunc() {
while (_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lock(_mutex); // New
_data.setSomeData("foo");
emit dataChanged();
}
}
bool _running;
std::thread _thread;
std::mutex _mutex; // New
Data* _data;
}; // Controller
// File main.qml
// NOTE: Controller is registered as context property alias 'controller'
import QtQuick 2.0
// Import the datatype 'data' to be used in QML
//import ...
Rectangle {
id: myRect
width: 100
height: 100
property Data data
Connections {
target: controller
onDataChanged: {
myRect.data = controller.data()
}
}
Text {
anchors.centerIn: parent
text: data.someData
}
}
Basically, I make sure to lock the object and to take a copy. This copy can then safely be used by QML and the QML engine will take care to delete the memory after usage. Furthermore, I create an instance of the Data object in QML and register the signal to fetch and assign the latest copy.
Upvotes: 2