Motivation
Let’s say we’re working on a QML project that involves a TextEdit
.
There’s some text in it:
We want to select part of this text and hit ctrl+B to make it bold:
In Qt Widgets, this is trivial, but not so much in QML – we can get font.bold
of the entire TextEdit
, but not of just the text in the selection. We have to implement formattable selections manually.
To do this, there are two approaches we’ll look at:
- The first is to hack it together by getting the formatted text from the selection and editing this. Rather than setting properties of selected text, this solution actually inserts or removes formatting symbols from the underlying rich text source.
- The other way to do this is to create a QML object that is implemented in C++ and exposed to
TextEdit
as a property. This way we can make use of QTextDocument
and QTextCursor
to actually set text properties within the selection area. This more closely follows the patterns expected in Qt.
In Qt 6.7, the TextEdit
QML element does have a cursorSelection
property that works in this way, and by dissecting its implementation, we can write a pseudo-backport for other Qt versions.
Before we do this, let’s take a look at the hacky QML/JS solution.
Hacky Approach
We start by focusing on just making ctrl+B bold shortcuts work:
Notice that we actually remove and replace the selected text, and reselect the insertion manually.
We can set up similar shortcuts for italics and underline trivially, but what if we want to set font properties of only the text in the selected area?
To keep things simple, let’s see what happens if we want to set just the font family and size:
If we start messing with other font style properties like italic
, bold
, spacing
, etc., we will end up with almost unreadably nasty string manipulation here.
This solution is overall hacky, as we replace HTML-formatted text from a snipped out section. It would be more Qt-idiomatic to retrieve QFont
info from a selection and set the properties without editing raw rich text. Furthermore, it’s better to do as much logic as possible in C++ rather than with JavaScript in QML.
Implementation of cursorSelection
in Qt 6.7 QML
Let’s take a look at the cursorSelection
property of QtQuick TextEdit
in Qt 6.7.
By looking at its property declaration in qquicktextedit_p.h
, the type of cursorSelection
is QQuickTextSelection
.
This type is very basic. It has four read/write properties.
Here is the header qquicktextselection_p.h
:
Notice we’ve got these private data members:
The m_doc
and m_control
are retrieved from the TextEdit
which parents the selection object. The object is always constructed by a QQuickTextEdit
, so in the constructor, the parent is cast to one using qmlobject_cast
. Then we set these two fields.
Now what are m_charFormat
and m_blockFormat
?
Text documents are composed of a list of text blocks, which can be paragraphs, lists, tables, images, etc. Thus, a block format represents an individual block’s alignment formatting. Char format contains formatting information at the character level, like font family, weight, style, size, color, and so forth.
To initialize these, we need to get the cursor from the text control.
The cursor will give us a char format and a block format, which we use to get the font / color / alignment at the cursor’s location.
currentCharFormatChanged
is emitted by QQuickTextControl
when the cursor moves or the document’s contents change. If this format is indeed different from the fields of the selection object, we must update them and emit the selection’s signals, just as we would in setters. Since we keep track of block alignment too, we have to do the same when the cursor moves and block format is different.
Here are the setters for the properties, which use the cursor to access and mutate the character or block properties at its position.
Now, we want to do something like this in our code. The issue is that this implementation resides in the Qt source code itself, and cursorSelection
is a property of QQuickTextEdit
. If we want to do something like this without changing Qt source code, we have to use attached properties.
Implementing an Attached Property
Using CursorSelection
as an attached property for a TextEdit
in QML might look something like this:
To create our own attached property, we have to create two classes: CursorSelectionAttached
and CursorSelection
.
CursorSelectionAttached
will contain the implementation of the selection, while CursorSelection
serves as the attaching type, using the qmlAttachedProperties()
method to expose the signals and properties of an instance of CursorSelectionAttached
to the parent to which it is attached.
CursorSelection
also needs the QML_ATTACHED()
macro in its header declaration, and we must specify that it has an attached property with the macro QML_DECLARE_TYPEINFO()
outside the class scope.
Thus, CursorSelection
will just look like this:
Where the entire implementation is just this function definition:
Notice that we perform the qobject_cast
here and forward the result as the parent of the attached object. This way we only construct an attached object if we can cast the parent object to a TextEdit
.
Now, let’s see how CursorSelectionAttached
should be implemented. We begin with the constructor:
Note that we connect to these three slots:
moveAnchorIfDeselected
updatePosition
applyFormatToNewTextIfNeeded
Let’s investigate the purpose of these.
moveAnchorIfDeselected
is invoked when the TextEdit’s selected text changes. A QTextCursor
has an anchor, which controls selection area. If text is being selected, the anchor is fixed in place where the selection is started, and the cursor position moves independently of the anchor. The selection area is located between the two positions. When a cursor moves without selecting anything, the anchor is located at and moves along with the cursor position.
Thus, when a cursor’s position is moved, we need to know if the anchor should be moved with it.
Since we invoke moveAnchorIfDeselected
when the selected text changes, we know that if the selection is now empty, this means there was a selection that has been deselected. Thus, the cursor and anchor should be equal to one another.
updatePosition
is invoked when the TextEdit’s cursor position changes. Depending on the TextEdit’s selection start and end positions, there are a few ways the cursor could be updated.
If there is no selected area in the TextEdit, the cursor and anchor should move together. If a selection’s start and end position both change, we must move the cursor twice: once to the start position, with the anchor moving, and once to the end position, with the anchor fixed in place. If the selection area is being resized, for example by dragging or using Shift+ArrowKeys, the cursor should move with the anchor fixed in place.
applyFormatToNewTextIfNeeded
is invoked when the contents of the text document change. This is because font properties might be set without an active selection. In this case, the expected behavior is for the characters added afterwards will have these properties.
For example, if the font family is changed with no selection, and we start typing, we expect our text to be in this new font. To do this, we need an optional
in which we can save a format to apply to new text if needed, or otherwise contains nullopt
. We will call it mOptFormat
. It can be set in property setters, which you will see later. For now, we just make sure to use it when the text document content changes and there exists a value in the optional.
Now, let’s take a look at the properties to expose to QML, and how they can be retrieved and set using the cursor. Like the QQuickTextSelection
implementation, we will have properties text
and font
. We can implement the others as well, but for the sake of brevity, we will just focus on these two.
We’ll need to declare and define these getters and setters, and declare the signals:
Getters:
The getter and setter implementations will look very similar to the previous implementations shown for QQuickTextSelection
, with some minor differences.
Getter implementations:
The only thing that needs to be done now is override the destructor, which can just be set to default:
Now we have all the implementation we need to use the attached property. If we put the two classes in one header file, it will look like this:
With this header, an implementation file containing the definitions, and a call to qmlRegisterUncreatableType<CursorSelection>
in your main.cpp
, the attached property can be used in QML.
Final Remarks
Though this is not a perfect backport, this code allows us to set font properties for selected text in QML in a nearly identical way to its implementation in Qt 6.7. This is especially useful to implement any kind of richtext editing in a QML application, where this functionality is severely lacking in any Qt version prior to 6.7. Hopefully this is a helpful guide to backporting features, implementing attached properties, and doing more sane text editing in QML apps. :)
Trusted software excellence across embedded and desktop platforms
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.