Alex Spataru
  • Home
  • Blog
  • Projects
    • Serial Studio
  • Home
  • Blog
  • Projects
    • Serial Studio
Search

Blog posts, ideas & rambles...

Using Qt Widgets in your QML/QtQuick applications

3/20/2021

0 Comments

 
There is no denying that QtQuick has a lot of handy features and allows programmers to build nice GUI applications with ease. However, sometimes we would like to integrate the functionality of "legacy" Qt Widgets with our QML applications.

For example, QtQuick does not have a direct replacement of the QPlainTextEdit widget (which is very useful if you need to log large amounts of data or if you want to build a custom code editor). Other examples of such widgets are the QTableWidget and third-party widgets (such as Qwt or QScintilla).

I bumped into this issue while developing an appropriate way to create a QML-friendly serial output console for Serial Studio. Using the standard QML TextArea resulted too slow for displaying high-frequency data from a serial port device.

To fix this, I decided to use the QPlainTextEdit widget for displaying incoming serial data and use QQuickPaintedItem to render the widget in the QML interface. 

We'll begin our unorthodox programming ritual with the following header code:
#ifndef UI_QML_PLAINTEXTEDIT_H
#define UI_QML_PLAINTEXTEDIT_H

#include <QPainter>
#include <QPlainTextEdit>
#include <QQuickPaintedItem>

class QmlPlainTextEdit : public QQuickPaintedItem
{
public:
    QmlPlainTextEdit(QQuickItem *parent = 0);
    ~QmlPlainTextEdit();

    virtual bool event(QEvent *event) override;
    virtual void paint(QPainter *painter) override;
    virtual bool eventFilter(QObject *watched, QEvent *event) override;

    QPlainTextEdit *textEdit() const;

private slots:
    void updateWidgetSize();

protected:
    void processMouseEvents(QMouseEvent *event);
    void processWheelEvents(QWheelEvent *event);

private:
    QPlainTextEdit *m_textEdit;
};

#endif

​And here is the implementation code for this class:
#include "QmlPlainTextEdit.h"

/**
 * Constructor function
 */
QmlPlainTextEdit::QmlPlainTextEdit(QQuickItem *parent)
    : QQuickPaintedItem(parent)
    , m_textEdit(new QPlainTextEdit)
{
    // Set item flags
    setFlag(ItemHasContents, true);
    setFlag(ItemAcceptsInputMethod, true);
    setFlag(ItemIsFocusScope, true);
    setAcceptedMouseButtons(Qt::AllButtons);

    // Install event filter for widget
    textEdit()->installEventFilter(this);

    // Set the QML item's implicit size
    auto hint = textEdit()->sizeHint();
    setImplicitSize(hint.width(), hint.height());

    // Setup default options
    textEdit()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    textEdit()->setSizeAdjustPolicy(QPlainTextEdit::AdjustToContents);

    // Resize QPlainTextEdit to fit QML item
    connect(this, &QQuickPaintedItem::widthChanged, this,
            &QmlPlainTextEdit::updateWidgetSize);
    connect(this, &QQuickPaintedItem::heightChanged, this,
            &QmlPlainTextEdit::updateWidgetSize);
}

/**
 * Destructor function
 */
QmlPlainTextEdit::~QmlPlainTextEdit()
{
    m_textEdit->deleteLater();
}

/**
 * Handle application events manually
 */
bool QmlPlainTextEdit::event(QEvent *event)
{
    switch (event->type())
    {
        case QEvent::FocusIn:
            forceActiveFocus();
            return QQuickPaintedItem::event(event);
            break;
        case QEvent::Wheel:
            processWheelEvents(static_cast<QWheelEvent *>(event));
            return true;
            break;
        case QEvent::MouseButtonPress:
        case QEvent::MouseButtonRelease:
        case QEvent::MouseButtonDblClick:
        case QEvent::MouseMove:
            processMouseEvents(static_cast<QMouseEvent *>(event));
            return true;
            break;
        default:
            break;
    }

    return QApplication::sendEvent(textEdit(), event);
}

/**
 * Render the text edit on the given @a painter
 */
void QmlPlainTextEdit::paint(QPainter *painter)
{
    if (painter)
        textEdit()->render(painter);
}

/**
 * Custom event filter to manage redraw requests
 */
bool QmlPlainTextEdit::eventFilter(QObject *watched, QEvent *event)
{
    Q_ASSERT(m_textEdit);

    if (watched == textEdit())
    {
        switch (event->type())
        {
            case QEvent::Paint:
            case QEvent::UpdateRequest:
                update();
                break;
            default:
                break;
        }
    }

    return QQuickPaintedItem::eventFilter(watched, event);
}

/**
 * Resizes the text editor widget to fit inside the QML item.
 */
void QmlPlainTextEdit::updateWidgetSize()
{
    textEdit()->setFixedSize(width(), height());
    update();
}

/**
 * Hack: call the appropriate protected mouse event handler function 
 *       of the QPlainTextEdit item depending on event type.
 */
void QmlPlainTextEdit::processMouseEvents(QMouseEvent *event)
{
    // Subclass QPlainTextEdit so that we can call protected functions
    class Hack : public QPlainTextEdit
    {
    public:
        using QPlainTextEdit::mouseDoubleClickEvent;
        using QPlainTextEdit::mouseMoveEvent;
        using QPlainTextEdit::mousePressEvent;
        using QPlainTextEdit::mouseReleaseEvent;
    };

    // Call appropiate function
    auto hack = static_cast<Hack *>(textEdit());
    switch (event->type())
    {
        case QEvent::MouseButtonPress:
            hack->mousePressEvent(event);
            break;
        case QEvent::MouseMove:
            hack->mouseMoveEvent(event);
            break;
        case QEvent::MouseButtonRelease:
            hack->mouseReleaseEvent(event);
            break;
        case QEvent::MouseButtonDblClick:
            hack->mouseDoubleClickEvent(event);
            break;
        default:
            break;
    }
}

/**
 * Hack: call the protected wheel event handler function of the
 *       QPlainTextEdit item
 */
void QmlPlainTextEdit::processWheelEvents(QWheelEvent *event)
{
    // Subclass QPlainTextEdit so that we can call protected functions
    class Hack : public QPlainTextEdit
    {
    public:
        using QPlainTextEdit::wheelEvent;
    };

    // Call wheel event handler
    static_cast<Hack *>(textEdit())->wheelEvent(event);
}

Note: in the processMouseEvents() and processWheelEvents() functions, we are writing potentially illegal C++ code because we are indirectly accessing protected functions of the QPlainTextEdit class. Unfortunately, this is the only way that I managed to get the widget to accept mouse/wheel events. If you have a better solution, you are welcome to share it :)

To use this class from the QML interface, we need to add the following code in the main() function (or before initializing the QML engine):
qmlRegisterType<QmlPlainTextEdit>("QtHacks", 1, 0, "QmlPlainTextEdit");


Finally, here is an example of using QmlPlainTextEdit from QML:
import QtQuick 2.12
import QtQuick.Window 2.12

import QtHacks 1.0

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("QML PlainTextEdit Example")

    QmlPlainTextEdit {
        anchors.fill: parent
        anchors.margins: 8
    }
}

You can also add functions to read/write widget properties from QML using the Q_PROPERTY macro. You can check the complete QmlPlainTextEdit code in the following links:
  • Console.qml
  • QmlPlainTextEdit.h
  • QmlPlainTextEdit.cpp

Please take into account that these files are specific to Serial Studio, so you will need to modify them to use them in a "portable" manner in your projects.

Finally, you can follow this approach with any QWidget based object that you need to use from your QML interface. I have used this approach with a QTableWidget for a proprietary project without any major issues.
0 Comments



Leave a Reply.

    Alex Spataru

    Website author

    Archives

    March 2021
    December 2020

    Categories

    All
    FOSS
    Hacks
    Microcontrollers
    QML
    Qt

    RSS Feed

Powered by Create your own unique website with customizable templates.
  • Home
  • Blog
  • Projects
    • Serial Studio