A «five-minute task» that turned out to involve half of Qt, mobile keyboards, and a fair amount of pain.
The Problem
Here’s the setup: a cross-platform mobile app, a multiline TextArea, and a requirement to paste rich-text from the clipboard as plain text.
Sounds simple, right? Just set textFormat: TextEdit.PlainText and call it a day.
Except — our TextArea has to support rich-text internally, because «that’s how it historically worked.» So we only want to strip formatting when pasting from the outside. The property approach is out.
Attempt #1: Intercept the Clipboard
The obvious next step: grab QClipboard, connect to the changed signal, and strip rich-text there before it ever reaches the input field. Fast, bold, and wrong.
Why wrong?
First, it breaks clipboard consistency across the OS. The user copies rich-text in some editor, switches to your app, your app silently mutates the clipboard, the user goes back and pastes — plain text. Surprise. QA might not catch it. Users will.
Second, it doesn’t actually work. The slot connected to changed never fires at the right moment — not when the app activates, not when the paste happens. This is tied to how Qt handles keyboards and input fields on mobile platforms (in short: it’s complicated and not great).
Finding the Right Event
Since everything here is a QObject, let’s use what QObject does well: event filtering. Slap QObject::eventFilter on the TextArea, log every event type to stdout, and see what shows up.
You’ll immediately notice a flood of QInputMethodEvent and QInputMethodQueryEvent. Nothing useful in them. Filter those out.
What’s left is interesting: when you tap Paste in the context menu (the one that appears after a long press on a text field), the TextArea receives a KeyPress and KeyRelease pair — specifically Ctrl+V. And at that exact moment, QClipboardalready holds the data being pasted.
So we have the data. Now how do we tell Qt to use plain-text instead of rich-text?
The Actual Solution (Spoiler: It’s Not Pretty)
You might think: just call insert on the TextArea. That won’t work when the user has a selection — insert creates a new QTextCursor at the given position, while we need to use the existing one that owns the current selection.
The existing cursor isn’t publicly accessible. But if you’re determined enough…
Add Qt6::QuickPrivate as a dependency and link against it in target_link_libraries. Then include these headers — order matters due to how QQuickTextControl is structured:
#include <QtQuick/private/qquicktextcontrol_p.h>#include <QtQuick/private/qquicktextcontrol_p_p.h>#include <QtQuick/private/qquicktextedit_p_p.h>
All QML text editors inherit from QQuickTextEdit. Cast the editor pointer from QML using qobject_cast, grab the private part, dig through to QQuickTextControl, and from its private part you can finally reach the cursor:
QQuickTextEdit* editor = qobject_cast<QQuickTextEdit*>(mEditor);if (editor != nullptr) { QQuickTextEditPrivate* editorPrivate = QQuickTextEditPrivate::get(editor); // QQuickTextControl has no standard static 'get' for its private part, // so here's a small trick with QObjectPrivate QObjectPrivate* objPrivate = QQuickTextControlPrivate::get(editorPrivate->control); if (objPrivate != nullptr) { QQuickTextControlPrivate* controlPrivate = static_cast<QQuickTextControlPrivate*>(objPrivate); }}
Once you have controlPrivate, insert plain-text through the cursor and mark the event as handled:
controlPrivate->cursor.insertFragment( QTextDocumentFragment::fromPlainText(someText));event->setAccepted(true);return true;
Full Event Filter
bool ClipboardHelper::eventFilter(QObject* obj, QEvent* event){ const auto type = event->type(); if (QEvent::KeyPress == type || QEvent::KeyRelease == type) { QKeyEvent* key = static_cast<QKeyEvent*>(event); if (QKeySequence::Paste == key) { if (QEvent::KeyPress == type) { return true; } auto* clip = qGuiApp->clipboard(); if (clip != nullptr && clip->mimeData() != nullptr) { QString cliptext = clip->mimeData()->text(); if (!cliptext.isEmpty()) { QQuickTextEdit* editor = qobject_cast<QQuickTextEdit*>(mEditor); if (editor != nullptr) { QQuickTextEditPrivate* editorPrivate = QQuickTextEditPrivate::get(editor); QObjectPrivate* objPrivate = QQuickTextControlPrivate::get(editorPrivate->control); if (objPrivate != nullptr) { QQuickTextControlPrivate* controlPrivate = static_cast<QQuickTextControlPrivate*>(objPrivate); controlPrivate->cursor.insertFragment( QTextDocumentFragment::fromPlainText(cliptext)); event->setAccepted(true); return true; } } } } } } return QObject::eventFilter(obj, event);}
Important Note
Using Qt’s private API comes with risks. Nobody guarantees stability between Qt versions. The solution above is valid for Qt 6.9.
More Qt/C++/QML cases — t.me/c0de1e55
GitHub — github.com/0xc0de1e55-cm
ссылка на оригинал статьи https://habr.com/ru/articles/1030916/