QSyntaxHighlighter rehighlight() ignores changes triggered from another thread - qt

I have a spellchecking thread which fires spellcheck() signals from time to time which are connected to my highlighter's rehighlight() method. The latter sets the whole block to have red foreground.
This used to work in Qt 5.6.2 and ceased to work in newer versions. I hopelessly waited for it to get fixed in Qt 5.9.5, but it still does not work (neither in Windows 10, nor in OS X).
Small example which reproduces the problem could be obtained from here https://bitbucket.org/ribtoks/qt-highlighting-issue (in order to repro, type something in the input. rehighlight() will be triggered each 7 seconds from background thread)
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickTextDocument>
#include <QSyntaxHighlighter>
#include <QThread>
#include <QString>
class SpellCheckWorker : public QObject
{
Q_OBJECT
public:
explicit SpellCheckWorker(QObject *parent = 0) : QObject(parent), m_Counter(0), m_IsOK(false)
{ }
public:
bool isOK() { return m_IsOK; }
signals:
void spellcheck();
public slots:
void process() {
qInfo() << "Worker Thread is" << QThread::currentThreadId();
while (1) {
m_Counter++;
m_IsOK = m_Counter % 7 == 0;
QThread::sleep(1);
emit spellcheck();
}
}
private:
int m_Counter;
volatile bool m_IsOK;
};
class SpellCheckErrorsHighlighter : public QSyntaxHighlighter
{
Q_OBJECT
public:
SpellCheckErrorsHighlighter(SpellCheckWorker *worker, QTextDocument *document):
QSyntaxHighlighter(document),
m_Worker(worker)
{ }
virtual ~SpellCheckErrorsHighlighter() {}
protected:
virtual void highlightBlock(const QString &text) override {
if (!m_Worker->isOK()) {
qDebug() << "Worker is not OK" << text;
return;
}
qInfo() << "Reapplied formatting for" << text;
qInfo() << "Highlight thread is" << QThread::currentThreadId();
this->setFormat(0, text.length(), QColor(0xff, 0, 0));
}
private:
SpellCheckWorker *m_Worker;
};
class MainModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
public:
explicit MainModel(QObject *parent = 0) : QObject(parent), m_Worker(nullptr)
{ }
public:
QString name() const { return m_name; }
void startChecking() {
qInfo() << "Main thread is" << QThread::currentThreadId();
m_Worker = new SpellCheckWorker();
QThread *thread = new QThread();
m_Worker->moveToThread(thread);
QObject::connect(thread, &QThread::started, m_Worker, &SpellCheckWorker::process);
thread->start();
}
Q_INVOKABLE void initNameHighlighting(QQuickTextDocument *document) {
SpellCheckErrorsHighlighter *highlighter = new SpellCheckErrorsHighlighter(m_Worker, document->textDocument());
QObject::connect(m_Worker, &SpellCheckWorker::spellcheck,
highlighter, &SpellCheckErrorsHighlighter::rehighlight);
}
signals:
void nameChanged();
public slots:
void setName(QString name)
{
if (m_name == name)
return;
m_name = name;
emit nameChanged();
}
private:
SpellCheckWorker *m_Worker;
QString m_name;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
MainModel mainModel;
mainModel.startChecking();
QQmlApplicationEngine engine;
QQmlContext *rootContext = engine.rootContext();
rootContext->setContextProperty("mainModel", &mainModel);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
and main.qml
import QtQuick 2.6
import QtQuick.Controls 1.0
import QtQuick.Layouts 1.3
import QtQuick.Window 2.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
StackView {
id: mainStackView
anchors.fill: parent
focus: true
initialItem: Rectangle {
anchors.fill: parent
Rectangle {
id: titleRect
height: 30
width: 300
anchors.centerIn: parent
color: "#ffffff"
border.color: "#000000"
border.width: titleTextInput.activeFocus ? 1 : 0
clip: true
focus: false
Flickable {
id: titleFlick
contentWidth: titleTextInput.paintedWidth
contentHeight: titleTextInput.paintedHeight
height: parent.height
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 5
anchors.rightMargin: 5
clip: true
flickableDirection: Flickable.HorizontalFlick
interactive: false
focus: false
function ensureVisible(r) {
if (contentX >= r.x)
contentX = r.x;
else if (contentX+width <= r.x+r.width)
contentX = r.x+r.width-width;
}
TextEdit {
id: titleTextInput
width: paintedWidth > titleFlick.width ? paintedWidth : titleFlick.width
height: titleFlick.height
text: mainModel.name
focus: true
onTextChanged: mainModel.name = text
Component.onCompleted: mainModel.initNameHighlighting(titleTextInput.textDocument)
onCursorRectangleChanged: titleFlick.ensureVisible(cursorRectangle)
}
}
}
}
}
}
Is there any way to get it working with workarounds? I need to keep spellchecking logic in the background thread so it's not possible to move it to main thread.

You seem to be referring to two different root causes with the following two statements:
There's NO problem in delivering slot call to Highlighter.
and
m_Worker->isOK() check is there for a reason to execute highlighting only once per 7 seconds. The bug in Qt can only be demonstrated with this.
If I just focus on the second statement, it looks like the problem is that you are not able to hit the statement if (!m_Worker->isOK()) right?
That may well be a problem due to your code or a Qt issue on a specific platform.
Can you change the code to avoid the condition i.e. emit the spellcheck signal only when 7 seconds have passed to avoid making this check from another thread?

Ok, so after messing with your code, The issue is that you are not calling this line inside of the hilightBlock directive in main.cpp near line 55 in order to keep the format from before.
Add this in to fix it (I think) The question was still fairly unclear...
if (!m_Worker->isOK()) {
qDebug() << "Worker is not OK" << text;
this->setFormat(0, text.length(), this->format(0));
return;
}
The other part of the issue is that you don't have any rules defined for the syntax highlighter... so it will always be red once it turns red.

Related

Qt QML How to manually select word in TextArea by double click

double click selects words, but i'd like to customise it to, for example, select the $1000 term.
Example:
import QtQuick 2.7
import QtQuick.Controls 1.4
ApplicationWindow
{
visible: true
width: 640
height: 480
TextArea
{
anchors.fill: parent
text: "hello, try to select the $1000 prize!"
font.pointSize: 14
}
}
For example, by somehow overriding selectWord or by overriding onDoubleClicked or by somehow adding my own MouseArea that doesn't break the existing TextArea functionality.
Not sure how to do this.
Thanks for any help.
update
I tried adding a MouseArea but it didnt work. Example;
ApplicationWindow
{
visible: true
width: 640
height: 480
TextArea
{
anchors.fill: parent
text: "hello, try to select the $1000 prize!"
font.pointSize: 14
MouseArea
{
anchors.fill: parent
propagateComposedEvents: true
onClicked:
{
// attempt to get an event on click without
// affecting the TextArea. But it breaks selection.
console.log("clicked some text")
mouse.accepted = false
}
}
}
}
Update 2
I think this problem is a version of the long-running Qt problem that you can't have some kind of "event observer" whose job is to check events but not stop them from continuing their normal operation.
If i had an event observer, i could make it "observe" the TextArea and do something on click or double click.
So, here goes trying to make one....
toucharea.h
#pragma once
#include <QGuiApplication>
#include <QQuickItem>
#include <QTime>
class TouchArea : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged)
public:
QTime _lastMousePress;
int _clickThresholdMS = 300;
bool eventFilter(QObject*, QEvent *event) override
{
// if false this will allow the event to continue as normal
// if true it will stop the event propagating
bool handled = false;
// https://doc.qt.io/qt-5/qevent.html#Type-enum
QEvent::Type t = event->type();
switch (t)
{
case QEvent::TouchUpdate:
break;
case QEvent::KeyPress:
case QEvent::KeyRelease:
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
qDebug("key press %d", keyEvent->key());
}
break;
case QEvent::MouseButtonPress:
{
qDebug() << "mouse press";
_lastMousePress.start();
break;
}
case QEvent::MouseButtonRelease:
{
qDebug() << "mouse release";
int dt = _lastMousePress.elapsed();
if (dt < _clickThresholdMS)
{
qDebug() << "mouse click";
emit clicked();
}
break;
}
}
return handled;
}
QQuickItem *target() const { return _target; }
void setTarget(QQuickItem *target)
{
qDebug() << "set target";
if (_target == target) return;
if (_target)
_target->removeEventFilter(this);
_target = target;
if (_target)
_target->installEventFilter(this);
emit targetChanged();
}
signals:
void targetChanged();
void clicked();
private:
QQuickItem* _target = 0;
};
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <qqmlcontext.h>
#include "toucharea.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<TouchArea>("App", 1, 0, "TouchArea");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
main.qml
import QtQuick 2.12
import QtQuick.Controls 1.4
import App 1.0
ApplicationWindow
{
visible: true
width: 640
height: 480
TextArea
{
id: tarea
anchors.fill: parent
text: "hello, try to select the $1000 prize!"
font.pointSize: 14
MouseArea
{
enabled: false
anchors.fill: parent
TouchArea
{
target: parent
onClicked: console.log("captured a click")
}
}
}
}
Well it nearly worked. I can only capture my synthetic click if we handled the event in eventFilter. When not handled the filter does not see the MouseButtonRelease.
Why is this. I was expecting this to be the first handler encountered before the other QtQuick items get to see it.
Any help?
Figured out a way;
Step 1, define an Observer class for events
observer.h
#pragma once
#include <QGuiApplication>
#include <QQuickItem>
#include <QTime>
#include <QMouseEvent>
class Observer : public QQuickItem
{
Q_OBJECT
public:
QTime _lastMousePress;
int _clickThresholdMS = 300;
Observer()
{
setFiltersChildMouseEvents(true);
}
bool childMouseEventFilter(QQuickItem*, QEvent *event) override
{
// if false this will allow the event to continue as normal
// if true it will stop the event propagating
bool handled = false;
// https://doc.qt.io/qt-5/qevent.html#Type-enum
QEvent::Type t = event->type();
switch (t)
{
case QEvent::TouchUpdate:
break;
case QEvent::KeyPress:
case QEvent::KeyRelease:
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
qDebug("key press %d", keyEvent->key());
}
break;
case QEvent::MouseButtonPress:
{
//qDebug() << "mouse press";
_lastMousePress.start();
}
break;
case QEvent::MouseButtonRelease:
{
//qDebug() << "mouse release";
int dt = _lastMousePress.elapsed();
if (dt < _clickThresholdMS)
{
//qDebug() << "mouse click";
emit clicked();
}
}
break;
case QEvent::MouseButtonDblClick:
{
//QMouseEvent* mevent = static_cast<QMouseEvent*>(event);
//qDebug() << "mouse double click";
emit doubleClicked();
handled = true;
}
break;
}
return handled;
}
signals:
void clicked();
void doubleClicked();
};
Step 2, put this in main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <qqmlcontext.h>
#include "observer.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<Observer>("App", 1, 0, "Observer");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
Step 3, use Observer to detect event
Detect whichever event you want, then make it do what you need, for example, for double click to select a wider class of characters in TextArea;
import QtQuick 2.12
import QtQuick.Controls 1.4
import App 1.0
ApplicationWindow
{
visible: true
width: 640
height: 480
Observer
{
anchors.fill: parent
onDoubleClicked:
{
tarea.selectWord();
var s = tarea.selectionStart
var e = tarea.selectionEnd
function allowed(c)
{
if (c == "$" || c == "#") return true;
if (c >= "0" && c <= "9") return true;
if (c.toUpperCase() != c.toLowerCase()) return true;
return false;
}
while (allowed(tarea.getText(s-1, s))) tarea.select(--s, e);
while (allowed(tarea.getText(e, e+1))) tarea.select(s, ++e);
}
TextArea
{
id: tarea
anchors.fill: parent
text: "hello, try to select the #$$$1000###$foo prize!"
font.pointSize: 14
}
}
}
Looks like you can do this with TapHandler:
import QtQuick 2.15
import QtQuick.Controls 2.15
TextField {
width: parent.width
selectByMouse: true
TapHandler {
grabPermissions: PointerHandler.TakeOverForbidden
onDoubleTapped: (eventPoint) => {
print("taptap");
eventPoint.accepted = true;
}
}
}
Note that I experienced both that the double-click = select string on TextField did and did not work after the above. Click+drag to select always worked though.
I cannot think of any solution that would preserve the TextArea behavior, just on the QML side. I would instead try to wrap, inherit or reimplement the component on the C++ side, to add what you want.
Inheriting may be tricky, as few Qt classes are meant to be inherited directly, looking at QQuickTextArea and QQuickTextEdit source, it might be challenging
Reimplementing can be some work, but that would probably be the option I would go for, while keeping the logic and code there to a minimum. QQuickTextArea seems a good base/reference as smaller/simpler than QQuickTextArea (which in turn uses TextArea internally)
Two key elements to that:
Events management (mouse, selection, ...)
Rendering of the component, based on Qt OpenGL wrapper classes (QSG* Scene graph classes). It is a bit obscure, but there are some tutorials and presentations out there about it. And you may be able to just copy-paste the original code.
For both, I would recommend digging into the source code:
QQuickTextEdit: https://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quick/items/qquicktextedit.cpp
QQuickTextArea: https://code.qt.io/cgit/qt/qtquickcontrols2.git/tree/src/quicktemplates2/qquicktextarea.cpp

Property binding updates an event frame later

I have a C++ object exposed to QML as myObject. This C++ object has a Q_PROPERTY:
Q_PROPERTY (bool myProperty READ getMyPropert NOTIFY myPropertyChanged)
In QML I use this as a guard in a SignalTransition as follows:
property bool foo: myObject.myProperty
SignalTransition {
targetState: someState
signal: someSignal
onTriggered: console.info("foo: " + foo)
onTriggered: console.info("myObject.myProperty: " + myObject.myProperty)
}
The output is the following:
foo: false
myObject.myProperty: true
An event frame later, foo becomes true as well.
Is this behaviour normal?
EDIT:
Here is a working example:
MyClass.h:
#pragma once
#include <QObject>
class MyClass : public QObject {
Q_OBJECT
Q_PROPERTY(bool myProperty_1 READ getMyProperty_1 NOTIFY myClassChanged)
Q_PROPERTY(bool myProperty_2 READ getMyProperty_2 NOTIFY myClassChanged)
public:
static void initQML();
MyClass();
bool getMyProperty_1();
bool getMyProperty_2();
Q_INVOKABLE void setMyProperty(bool value);
signals:
void myClassChanged();
private:
bool m_MyProperty_1;
bool m_MyProperty_2;
};
MyClass.cpp:
#include "MyClass.h"
#include <QtQml>
#include <QDebug>
#include <QMetaType>
/*static*/ void MyClass::initQML() {
qmlRegisterType<MyClass>("Test", 1, 0, "MyClass");
}
MyClass::MyClass() : m_MyProperty_1(false), m_MyProperty_2(false) {
qDebug() << "Constructor called.";
}
bool MyClass::getMyProperty_1() {
return m_MyProperty_1;
}
bool MyClass::getMyProperty_2() {
return m_MyProperty_2;
}
void MyClass::setMyProperty(bool value) {
m_MyProperty_1 = value;
m_MyProperty_2 = value;
myClassChanged();
}
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "MyClass.h"
int main(int argc, char *argv[])
{
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
// Init
MyClass::initQML();
// Main loop
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
main.qml:
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQml.StateMachine 1.0 as QtQml_StateMachine
import Test 1.0
ApplicationWindow {
visible: true
width: 480
height: 272
Component.onCompleted: _stateMachine.start()
// UI
Button {
text: "Switch"
onClicked: {
console.info("Switch pressed with setter")
myClass.setMyProperty(true)
}
}
// StateMachine
property MyClass myClass: MyClass {}
QtQml_StateMachine.StateMachine {
id: _stateMachine
initialState: _defaultState
onStarted: console.info("_stateMachine started")
onExited: console.info("_stateMachine exited")
QtQml_StateMachine.State {
id: _defaultState
property bool myProperty_1: myClass.myProperty_1
property bool myProperty_2: myClass.myProperty_2
onMyProperty_1Changed: console.info("onMyProperty_1Changed: " + myProperty_1)
onMyProperty_2Changed: console.info("onMyProperty_2Changed: " + myProperty_2)
onEntered: console.info("_defaultState entered")
onExited: console.info("_defaultState exited")
// Doesn't work:
QtQml_StateMachine.SignalTransition {
targetState: _nextState
signal: _defaultState.myProperty_1Changed
guard: _defaultState.myProperty_2
onTriggered: console.info("myProperty_2")
}
// Works:
// QtQml_StateMachine.SignalTransition {
// targetState: _nextState
// signal: _defaultState.myProperty_1Changed
// guard: myClass.myProperty_2
// onTriggered: console.info("myClass.myProperty_2")
// }
}
QtQml_StateMachine.State {
id: _nextState
onEntered: console.info("_nextState entered")
onExited: console.info("_nextState exited")
}
}
}
If you use the commented SignalTransition it works. If you use the other one it won't work.
The issue is that there is a race condition, due to proxying the C++ object's properties to have individual Changed signals. This is due to the direct connections in QML.
The update works in the following order:
myClassChanged -> set: m_MyProperty_1 (C++) -> signal myProperty_1Changed (QML) -> transition and hit the guard, then -> set: m_MyProperty_2 (C++) -> signal m_MyProperty_2Changed (QML)
so myProperty_1 racing against myProperty_2.

QML Slot not displaying double value from serial input

I'm struggling to get my serial input "analogread2" converted to a double, to display in QML.
Ive added the Q_property, the root context in the main.cpp, and I can add the property in the qml, but I cant get it to display in text format.
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
SerialPort serialport;
//engine.rootContext()->setContextProperty("serialport", &serialport);
qmlRegisterType<SerialPort>("SerialPortlib", 1, 0, "SerialPort");
engine.load(QUrl(QStringLiteral("qrc:/main.qml")))
header.
Q_PROPERTY(double newValue MEMBER m_oil_pressure_volt WRITE set_oil_pressure_volt NOTIFY oil_pressure_volt_Changed )
qml
Window {
visible: true
width: 640
height: 480
id: gauge
SerialPort{
// what would I put in here to display the text value (double)
}
}
any help is much appreciated.
To do the test I am assuming that you are sending the data from the arduino in one of the following ways:
Serial.println(data);
Serial.print(data);
Assuming the above has been implemented the serialport class, this has a function called openDefault that scans the devices that are connected through serial and searches for the word "Arduino" within description or manufacturer and if one finds it connects to it.
The complete code can be found at: https://github.com/eyllanesc/stackoverflow/tree/master/Arduino-QML
serialport.h
#ifndef SERIALPORT_H
#define SERIALPORT_H
#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
class SerialPort : public QObject
{
Q_OBJECT
Q_PROPERTY(double oil_pressure_volt READ get_oil_pressure_volt WRITE set_oil_pressure_volt NOTIFY oil_pressure_volt_Changed )
public:
SerialPort(QObject *parent = 0);
~SerialPort();
double get_oil_pressure_volt() const;
void set_oil_pressure_volt(double newValue);
public slots:
void onReadData();
signals:
void oil_pressure_volt_Changed(double newValue);
private:
QSerialPort *arduino;
double mOil_pressure_volt;
QSerialPortInfo portInfo;
void openDefault();
};
serialport.cpp
#include "serialport.h"
#include <QDebug>
SerialPort::SerialPort(QObject *parent):QObject(parent)
{
arduino = new QSerialPort(this);
connect(arduino, &QSerialPort::readyRead, this, &SerialPort::onReadData);
openDefault();
}
SerialPort::~SerialPort()
{
delete arduino;
}
void SerialPort::set_oil_pressure_volt(double newValue)
{
if (mOil_pressure_volt == newValue)
return;
mOil_pressure_volt = newValue;
emit oil_pressure_volt_Changed(mOil_pressure_volt);
}
void SerialPort::onReadData()
{
if(arduino->bytesAvailable()>0){
QByteArray data = arduino->readAll();
QString value = QString(data).trimmed();
qDebug()<< value;
bool ok;
double val = value.toDouble(&ok);
if(ok)
set_oil_pressure_volt(val);
}
}
void SerialPort::openDefault()
{
for(auto info: QSerialPortInfo::availablePorts()){
qDebug()<<info.portName()<<info.description()<<info.manufacturer();
if(!info.isBusy() && (info.description().contains("Arduino") || info.manufacturer().contains("Arduino"))){
portInfo = info;
break;
}
}
if(portInfo.isNull()){
return;
}
arduino->setPortName(portInfo.portName());
arduino->setBaudRate(QSerialPort::Baud115200);
arduino->setDataBits(QSerialPort::Data8);
arduino->setParity(QSerialPort::NoParity);
arduino->setStopBits(QSerialPort::OneStop);
arduino->setFlowControl(QSerialPort::NoFlowControl);
if(arduino->open(QSerialPort::ReadWrite))
qDebug()<<"Connected to "<< portInfo.manufacturer()<< " on " << portInfo.portName();
else
qCritical()<<"Serial Port error: " << arduino->errorString();
}
double SerialPort::get_oil_pressure_volt() const
{
return mOil_pressure_volt;
}
Example:
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "serialport.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
qmlRegisterType<SerialPort>("SerialPortLib", 1, 0, "SerialPort");
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
main.qml
import QtQuick 2.6
import QtQuick.Window 2.2
import SerialPortLib 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Test")
SerialPort{
onOil_pressure_voltChanged: {
tx.text = "%1".arg(oil_pressure_volt);
}
}
Text {
id: tx
anchors.fill: parent
font.family: "Helvetica"
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}

Drawing QQuickItem's texture in a "draw-chain" through 2 QQuickFramebufferObjects fails

First off, sorry for the long source code but I couldn't reduce it any further while still having the bug happen. Most of it is uninteresting boilerplate though, so you can mostly get away with just my human-readable summary below.
Abbreviations used in this post:
QQFBO = QQuickFramebufferObject
Observed result: Two rotating squares, that's all.
What's missing from the observed result: A third, additional, non-rotating square, which is actually a static snapshot from the second one.
What my code does, in short:
class ClonerItem is a QQFBO just fetches a texture from a QQuickItem and draws it to the screen.
The ClonerItemQml is a Qml wrapper of ClonerItem that adds a Timer to it. Depending on the ClonerItem.live property, the timer either calls ClonerItem.update() constantly or not at all. (basically the same the live property of Qt's ShaderEffectSource)
In main.qml I create a "producer-consumer-chain" of 3 items in a RowLayout.
The first item is a rotating rectangle with layer.enabled: true.
The second item is a ClonerItemQml (with live: true) that has the previous one as a texture source.
The third item is a ClonerItemQml (called "snapshotter", with live: false) that has the the previous one as a texture source.
"snapshotter" has its update method connected to 2 signals:
A firstRedraw signal provided by item "2", which is called at the end of the first redraw of item "2"
For testing, the clicked signal for "snapshotter", so you can see that the "snapshotter" does work correctly when you force it to update later during the program's execution.
Any hints what I'm doing wrong?
Note: When I remove the Timer and instead of it, uncomment the if(m_live) update(); in render, it works fine.
My code:
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickFramebufferObject>
#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions>
#include <QOpenGLFramebufferObject>
#include <QQuickWindow>
// propertyhelper.h is from http://syncor.blogspot.bg/2014/11/qt-auto-property.html
#include "propertyhelper.h"
#include <QSGTextureProvider>
class ClonerItem : public QQuickFramebufferObject {
Q_OBJECT
AUTO_PROPERTY(QQuickItem*, sourceItem)
AUTO_PROPERTY(bool, live)
public:
Renderer* createRenderer() const;
signals:
void firstRedraw();
};
class ClonerItemRenderer : public QObject, public QQuickFramebufferObject::Renderer, protected QOpenGLFunctions {
Q_OBJECT
public:
ClonerItemRenderer() {
initializeOpenGLFunctions();
m_program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shader.vert.glsl");
m_program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shader.frag.glsl");
m_program.link();
createGeometry();
}
void render() {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
m_program.bind();
glActiveTexture(GL_TEXTURE0);
m_tex->bind();
m_program.setUniformValue("uTex", 0);
paintGeometry();
glFlush();
glFinish();
m_window->resetOpenGLState();
if(!m_haveRedrawnAtLeastOnce)
emit firstRedraw();
m_haveRedrawnAtLeastOnce = true;
//if(m_live)
// update();
}
void paintGeometry() {
m_program.enableAttributeArray("aPos");
m_program.setAttributeArray("aPos", m_vertices.constData());
glDrawArrays(GL_TRIANGLES, 0, m_vertices.size());
}
void createGeometry() {
m_vertices << QVector2D(-1, -1) << QVector2D(-1, +1) << QVector2D(+1, +1);
m_vertices << QVector2D(-1, -1) << QVector2D(+1, -1) << QVector2D(+1, +1);
}
QOpenGLFramebufferObject* createFramebufferObject(const QSize &size) {
QOpenGLFramebufferObjectFormat format;
format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
return new QOpenGLFramebufferObject(size, format);
}
private:
QVector<QVector2D> m_vertices;
QOpenGLShaderProgram m_program;
QQuickWindow* m_window;
QSGTexture* m_tex;
bool m_haveRedrawnAtLeastOnce = false;
bool m_live;
protected:
void synchronize(QQuickFramebufferObject* qqfbo) {
ClonerItem* parentItem = (ClonerItem*)qqfbo;
m_window = parentItem->window();
m_live = parentItem->live();
// fetch texture pointer
QQuickItem* sourceItem = parentItem->sourceItem();
QSGTextureProvider* sourceTexProvider = sourceItem->textureProvider();
m_tex = sourceTexProvider->texture();
}
signals:
void firstRedraw();
};
QQuickFramebufferObject::Renderer* ClonerItem::createRenderer() const {
auto renderer = new ClonerItemRenderer();
connect(renderer, &ClonerItemRenderer::firstRedraw, this, &ClonerItem::firstRedraw);
return renderer;
}
int main(int argc, char **argv) {
QGuiApplication app(argc, argv);
qmlRegisterType<ClonerItem>("ClonerItem", 1, 0, "ClonerItem");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
ClonerItem.qml:
import QtQuick 2.0
import ClonerItem 1.0
ClonerItem {
id: root
Timer {
id: renderTimer
running: root.live
interval: 1000/60
repeat: true
onTriggered: {
root.update();
}
}
}
main.qml:
import QtQuick 2.0
import ClonerItem 1.0
import QtQuick.Window 2.2
import QtQuick.Layouts 1.3
Window {
visible: true
width: 600
height: 200
RowLayout {
id: rowLayout
anchors.fill: parent
Item {
id: original
layer.enabled: true
Layout.fillWidth: true
Layout.fillHeight: true
RotatingSquare { }
}
ClonerItemQml {
id: liveClone
layer.enabled: true
sourceItem: original
live: true
Layout.fillWidth: true
Layout.fillHeight: true
onFirstRedraw: {
snapshotter.update();
}
}
ClonerItemQml {
id: snapshotter
sourceItem: liveClone
live: false
Layout.fillWidth: true
Layout.fillHeight: true
MouseArea {
anchors.fill: parent
onClicked: {
snapshotter.update();
}
}
}
}
}
RotatingSquare.qml:
import QtQuick 2.0
Rectangle {
color: "red"
border.color: "black"
width: parent.width / 2
height: parent.height / 2
anchors.centerIn: parent
NumberAnimation on rotation {
running: true
loops: Animation.Infinite
duration: 1000
from: 0
to: 360
}
}
shader.frag.glsl:
varying highp vec2 vTexCoord;
uniform sampler2D uTex;
void main() {
gl_FragColor = texture2D(uTex, vTexCoord);
}
shader.vert.glsl
attribute highp vec2 aPos;
varying highp vec2 vTexCoord;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
// aPos's components are in [-1, 1], transform them to the range [0, 1] for use as texcoords.
vTexCoord = aPos * .5 + vec2(.5);
}
If you're wondering why I'm doing such an elaborate/weird setup, I can elucidate: in my real app the second item in the "chain" is not a simple "cloner" but actually uses its source texture(s) for some more-complex rendering (which is also memory-hungry). And the third item in the "chain" is necessary to snapshot the result of the second one, so the second one can be destroyed when not animated, freeing up memory.
The reason I implement my own ClonerItem component, which does basically the same as Qt's ShaderEffectSource, is this Qt bug.

How to display QML view( with scroll) to load and display multiline Text around 10 million lines from file [duplicate]

I have a Qt application and I'd like to show some log. I use a TextArea. However, if the log is large or the events come too fast the GUI can't draw Textarea fast enough.
I have analyzed this problem with Qt Creator (QML Profiler) and if the log is large it takes 300 ms to draw the GUI. I use this software on a Raspberry Pi2.
Any ideas how to solve this? Should I use other QML controls? Thanks.
QML code:
TextArea {
text: appHandler.rawCommunication
readOnly: true
}
C++ code:
Q_PROPERTY(QString rawCommunication READ rawCommunication WRITE setrawCommunication NOTIFY rawCommunicationChanged)
void setrawCommunication(QString val)
{
val.append("\n");
val.append(m_rawCommunication);
m_rawCommunication = val;
emit rawCommunicationChanged(m_rawCommunication);
}
Use a view, like ListView. They instantiate their delegates as needed, based on which data the view says it needs to show depending on the position the user is at in the list. This means that they perform much better for visualising large amounts of data than items like TextArea, which in your case is going to keep a massive, ever-growing string in memory.
Your delegate could then be a TextArea, so you'd have one editable block of text per log line. However, if you don't need styling, I'd recommend going with something a bit lighter, like TextEdit. Taking it one step further: if you don't need editable text, use plain old Text. Switching to these might not make much of a difference, but if you're still seeing slowness (and have lots of delegates visible at a time), it's worth a try.
I tried the ListView suggestion but it has several drawbacks:
No easy way to keep the view positioned at the bottom when new output is added
No selection across lines/delegates
So I ended up using a cached TextArea, updating once every second:
TextArea {
id: outputArea_text
wrapMode: TextArea.Wrap
readOnly: true
font.family: "Ubuntu Mono, times"
function appendText(text){
logCache += text + "\n";
update_timer.start();
}
property string logCache: ""
Timer {
id: update_timer
// Update every second
interval: 1000
running: false
repeat: false
onTriggered: {
outputArea_text.append(outputArea_text.logCache);
outputArea_text.logCache = "";
}
}
Component.onCompleted: {
my_signal.connect(outputArea_text.appendText)
}
}
try this approach:
create a c++ logger class
append all logs to this class
and print them using some action, example a button click
this will solve your performance issue
Example of code:
Logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include <QQmlContext>
#include <QObject>
#include <QStringList>
#include <QQmlEngine>
#include <QString>
#include <QtCore>
#include <QDebug>
class Logger : public QObject
{
Q_OBJECT
public:
explicit Logger(QObject *parent = 0);
~Logger();
Q_INVOKABLE QStringList *getLogStream();
Q_INVOKABLE void printLogStream();
Q_INVOKABLE void appendLog(QString log);
Q_INVOKABLE void log(QString log="");
Q_INVOKABLE void log(QString fileName, QString log);
signals:
public slots:
private:
QStringList* stringStream_;
};
#endif // LOGGER_H
Logger.cpp
#include "logger.h"
Logger::Logger(QObject *parent) :
QObject(parent),
stringStream_(new QStringList)
{
}
~Logger(){
if(stringStream_ != NULL)
{
delete stringStream_;
stringStream_ = NULL;
}
}
QStringList* Logger::getLogStream(){
return stringStream_;
}
void Logger::printLogStream()
{
QStringListIterator itr(*stringStream_);
while (itr.hasNext())
qDebug()<< itr.next()<<"\n";
}
void Logger::appendLog(QString log){
stringStream_->push_back(log) ;
}
void Logger::log(QString fileName,QString log)
{
#ifdef ENABLElogs
fileName.push_front(" [");
if(!fileName.contains(".qml"))
{
fileName.append(".qml]:");
}
qDebug()<<fileName<<log;
#else
Q_UNUSED(log);
Q_UNUSED(fileName);
#endif
}
void Logger::log(QString log)
{
#ifdef ENABLElogs
qDebug()<<log;
#else
Q_UNUSED(log);
#endif
}
main.cpp
#include <QtGui/QGuiApplication>
#include "qtquick2applicationviewer.h"
#include "logger.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QtQuick2ApplicationViewer *viewer = new QtQuick2ApplicationViewer;
Logger* stream = new Logger;
viewer->rootContext()->setContextProperty("Stream",stream);
viewer->setMainQmlFile(QStringLiteral("qml/project/main.qml"));
viewer->showExpanded();
return app.exec();
}
main.qml
import QtQuick 2.0
import QtQuick.Controls 1.1
Rectangle {
width: 800
height: 480
Text {
text: qsTr("Hello World")
anchors.centerIn: parent
Component.onCompleted: Stream.appendLog("Text object is completed")
}
Column{
x:300
Button{
text:"append"
onClicked: {
Stream.appendLog("MouseArea object clicked")
}
Component.onCompleted: Stream.appendLog("Button object is completed")
}
Button{
text:"logger"
onClicked: {
Stream.printLogStream()
}
Component.onCompleted: Stream.appendLog("Button logger object is completed")
}
}
TextArea{
text:"blablabla"
Component.onCompleted: Stream.appendLog("TextArea object is completed")
}
Component.onCompleted: Stream.appendLog("the main object is completed")
}
project.pro
#add this line
# comment it, run qmake and recompile to disable logs
DEFINES += ENABLElogs
using this approch you can stop all logs with a single line change when you want to release your soft
However, I have included complete code, using "QAbstractListModel" for a Logging heavy data to QML
listmodel.h
#ifndef LISTMODEL_H
#define LISTMODEL_H
#include <QAbstractListModel>
class ListModel: public QAbstractListModel
{
Q_OBJECT
public:
ListModel();
// Q_PROPERTY(QStringList logs READ name WRITE nameChanged)
int rowCount(const QModelIndex & parent = QModelIndex()) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
Q_INVOKABLE QVariant activate(int i);
private:
QStringList m_list;
};
#endif // LISTMODEL_H
listmodel.cpp
#include "listmodel.h"
#include <QFile>
#include <QHash>
ListModel::ListModel()
{
QFile file("/home/ashif/LogFile");
if(!file.open(QIODevice::ReadOnly))
{
qDebug( "Log file open failed" );
}
bool isContinue = true;
do
{
if(file.atEnd())
{
isContinue = false;
}
m_list.append(file.readLine());
}
while( isContinue);
}
int ListModel::rowCount(const QModelIndex & parent ) const
{
return m_list.count();
}
QVariant ListModel::data(const QModelIndex & index, int role ) const
{
if(!index.isValid()) {
return QVariant("temp");
}
return m_list.value(index.row());
}
QVariant ListModel::activate(int i)
{
return m_list[i];
}
main.qml
import QtQuick 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
Window {
visible: true
ListView
{
width: 200; height: 250
anchors.centerIn: parent
model:mylistModel
delegate: Text
{
text:mylistModel.activate(index)
}
}
}
main.cpp
#include <QGuiApplication>
#include <QQmlContext>
#include <QQmlApplicationEngine>
#include "logger.h"
#include "listmodel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Logger myLogger;
ListModel listModel;
engine.rootContext()->setContextProperty("mylistModel", &listModel);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}

Resources