The QTextCursor Interface

Documents can be edited via the interface provided by the QTextCursor class; cursors are either created using a constructor or obtained from an editor widget. The cursor is used to perform editing operations that correspond exactly to those the user is able to make themselves in an editor. As a result, information about the document structure is also available through the cursor, and this allows the structure to be modified. The use of a cursor-oriented interface for editing makes the process of writing a custom editor simpler for developers, since the editing operations can be easily visualized.

The QTextCursor class also maintains information about any text it has selected in the document, again following a model that is conceptually similar to the actions made by the user to select text in an editor.

Rich text documents can have multiple cursors associated with them, and each of these contains information about their position in the document and any selections that they may hold. This cursor-based paradigm makes common operations, such as cutting and pasting text, simple to implement programmatically, yet it also allows more complex editing operations to be performed on the document.

This chapter describes most of the common editing operations that you will need to perform using a cursor, from basic insertion of text and document elements to more complex manipulation of document structures.

Cursor-Based Editing

At the simplest level, text documents are made up of a string of characters, marked up in some way to represent the block structure of the text within the document. QTextCursor provides a cursor-based interface that allows the contents of a QTextDocument to be manipulated at the character level. Since the elements (blocks, frames, tables, etc.) are also encoded in the character stream, the document structure can itself be changed by the cursor.

The cursor keeps track of its location within its parent document, and can report information about the surrounding structure, such as the enclosing text block, frame, table, or list. The formats of the enclosing structures can also be directly obtained through the cursor.

Using a Cursor

The main use of a cursor is to insert or modify text within a block. We can use a text editor's cursor to do this:

     QTextEdit *editor = new QTextEdit();
     QTextCursor cursor(editor->textCursor());

Alternatively, we can obtain a cursor directly from a document:

     QTextDocument *document = new QTextDocument(editor);
     QTextCursor cursor(document);

The cursor is positioned at the start of the document so that we can write into the first (empty) block in the document.

Grouping Cursor Operations

A series of editing operations can be packaged together so that they can be replayed, or undone together in a single action. This is achieved by using the beginEditBlock() and endEditBlock() functions in the following way, as in the following example where we select the word that contains the cursor:

     cursor.beginEditBlock();
     cursor.movePosition(QTextCursor::StartOfWord);
     cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
     cursor.endEditBlock();

If editing operations are not grouped, the document automatically records the individual operations so that they can be undone later. Grouping operations into larger packages can make editing more efficient both for the user and for the application, but care has to be taken not to group too many operations together as the user may want find-grained control over the undo process.

Multiple Cursors

Multiple cursors can be used to simultaneously edit the same document, although only one will be visible to the user in a QTextEdit widget. The QTextDocument ensures that each cursor writes text correctly and does not interfere with any of the others.

Inserting Document Elements

QTextCursor provides several functions that can be used to change the structure of a rich text document. Generally, these functions allow document elements to be created with relevant formatting information, and they are inserted into the document at the cursor's position.

The first group of functions insert block-level elements, and update the cursor position, but they do not return the element that was inserted:

  • insertBlock() inserts a new text block (paragraph) into a document at the cursor's position, and moves the cursor to the start of the new block.
  • insertFragment() inserts an existing text fragment into a document at the cursor's position.
  • insertImage() inserts an image into a document at the cursor's position.
  • insertText() inserts text into the document at the cursor's position.

You can examine the contents of the element that was inserted through the cursor interface.

The second group of functions insert elements that provide structure to the document, and return the structure that was inserted:

  • insertFrame() inserts a frame into the document after the cursor's current block, and moves the cursor to the start of the empty block in the new frame.
  • insertList() inserts a list into the document at the cursor's position, and moves the cursor to the start of the first item in the list.
  • insertTable() inserts a table into the document after the cursor's current block, and moves the cursor to the start of the block following the table.

These elements either contain or group together other elements in the document.

Text and Text Fragments

Text can be inserted into the current block in the current character format, or in a custom format that is specified with the text:

     cursor.insertText(tr("Character formats"),
                       headingFormat);

     cursor.insertBlock();

     cursor.insertText(tr("Text can be displayed in a variety of "
                                   "different character formats. "), plainFormat);
     cursor.insertText(tr("We can emphasize text by "));
     cursor.insertText(tr("making it italic"), emphasisFormat);

Once the character format has been used with a cursor, that format becomes the default format for any text inserted with that cursor until another character format is specified.

If a cursor is used to insert text without specifying a character format, the text will be given the character format used at that position in the document.

Blocks

Text blocks are inserted into the document with the insertBlock() function.

     QTextBlockFormat backgroundFormat = blockFormat;
     backgroundFormat.setBackground(QColor("lightGray"));

     cursor.setBlockFormat(backgroundFormat);

The cursor is positioned at the start of the new block.

Frames

Frames are inserted into a document using the cursor, and will be placed within the cursor's current frame after the current block. The following code shows how a frame can be inserted between two text blocks in a document's root frame. We begin by finding the cursor's current frame:

     QTextFrame *mainFrame = cursor.currentFrame();
     cursor.insertText(...);

We insert some text in this frame then set up a frame format for the child frame:

     QTextFrameFormat frameFormat;
     frameFormat.setMargin(32);
     frameFormat.setPadding(8);
     frameFormat.setBorder(4);

The frame format will give the frame an external margin of 32 pixels, internal padding of 8 pixels, and a border that is 4 pixels wide. See the QTextFrameFormat documentation for more information about frame formats.

The frame is inserted into the document after the preceding text:

     cursor.insertFrame(frameFormat);
     cursor.insertText(...);

We add some text to the document immediately after we insert the frame. Since the text cursor is positioned inside the frame when it is inserted into the document, this text will also be inserted inside the frame.

Finally, we position the cursor outside the frame by taking the last available cursor position inside the frame we recorded earlier:

     cursor = mainFrame->lastCursorPosition();
     cursor.insertText(...);

The text that we add last is inserted after the child frame in the document. Since each frame is padded with text blocks, this ensures that more elements can always be inserted with a cursor.

Tables

Tables are inserted into the document using the cursor, and will be placed within the cursor's current frame after the current block:

     QTextCursor cursor(editor->textCursor());
     QTextTable *table = cursor.insertTable(rows, columns, tableFormat);

Tables can be created with a specific format that defines the overall properties of the table, such as its alignment, background color, and the cell spacing used. It can also determine the constraints on each column, allowing each of them to have a fixed width, or resize according to the available space.

     QTextTableFormat tableFormat;
     tableFormat.setBackground(QColor("#e0e0e0"));
     QList<QTextLength> constraints;
     constraints << QTextLength(QTextLength::PercentageLength, 16);
     constraints << QTextLength(QTextLength::PercentageLength, 28);
     constraints << QTextLength(QTextLength::PercentageLength, 28);
     constraints << QTextLength(QTextLength::PercentageLength, 28);
     tableFormat.setColumnWidthConstraints(constraints);
     QTextTable *table = cursor.insertTable(rows, columns, tableFormat);

The columns in the table created above will each take up a certain percentage of the available width. Note that the table format is optional; if you insert a table without a format, some sensible default values will be used for the table's properties.

Since cells can contain other document elements, they too can be formatted and styled as necessary.

Text can be added to the table by navigating to each cell with the cursor and inserting text.

     cell = table->cellAt(0, 0);
     cellCursor = cell.firstCursorPosition();
     cellCursor.insertText(tr("Week"), charFormat);

We can create a simple timetable by following this approach:

     for (column = 1; column < columns; ++column) {
         cell = table->cellAt(0, column);
         cellCursor = cell.firstCursorPosition();
         cellCursor.insertText(tr("Team %1").arg(column), charFormat);
     }

     for (row = 1; row < rows; ++row) {
         cell = table->cellAt(row, 0);
         cellCursor = cell.firstCursorPosition();
         cellCursor.insertText(tr("%1").arg(row), charFormat);

         for (column = 1; column < columns; ++column) {
             if ((row-1) % 3 == column-1) {
                 cell = table->cellAt(row, column);
                 QTextCursor cellCursor = cell.firstCursorPosition();
                 cellCursor.insertText(tr("On duty"), charFormat);
             }
         }
     }

Lists

Lists of block elements can be automatically created and inserted into the document at the current cursor position. Each list that is created in this way requires a list format to be specified:

     QTextListFormat listFormat;
     if (list) {
         listFormat = list->format();
         listFormat.setIndent(listFormat.indent() + 1);
     }

     listFormat.setStyle(QTextListFormat::ListDisc);
     cursor.insertList(listFormat);

The above code first checks whether the cursor is within an existing list and, if so, gives the list format for the new list a suitable level of indentation. This allows nested lists to be created with increasing levels of indentation. A more sophisticated implementation would also use different kinds of symbol for the bullet points in each level of the list.

Images

Inline images are added to documents through the cursor in the usual manner. Unlike many other elements, all of the image properties are specified by the image's format. This means that a QTextImageFormat object has to be created before an image can be inserted:

     QTextImageFormat imageFormat;
     imageFormat.setName(":/images/advert.png");
     cursor.insertImage(imageFormat);

The image name refers to an entry in the application's resource file. The method used to derive this name is described in The Qt Resource System.

Examples

Rich text is stored in text documents that can either be created by importing HTML from an external source, or generated using a QTextCursor.

Manipulating Rich Text

The easiest way to use a rich text document is through the QTextEdit class, providing an editable view onto a document. The code below imports HTML into a document, and displays the document using a text edit widget.

 QTextEdit *editor = new QTextEdit(parent);
 editor->setHtml(aStringContainingHTMLtext);
 editor->show();

You can retrieve the document from the text edit using the document() function. The document can then be edited programmatically using the QTextCursor class. This class is modeled after a screen cursor, and editing operations follow the same semantics. The following code changes the first line of the document to a bold font, leaving all other font properties untouched. The editor will be automatically updated to reflect the changes made to the underlying document data.

 QTextDocument *document = edit->document();
 QTextCursor cursor(document);

 cursor.movePosition(QTextCursor::Start);
 cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);

 QTextCharFormat format;
 format.setFontWeight(QFont::Bold);

 cursor.mergeCharFormat(format);

Note that the cursor was moved from the start of the first line to the end, but that it retained an anchor at the start of the line. This demonstrates the cursor-based selection facilities of the QTextCursor class.

Generating a Calendar

Rich text can be generated very quickly using the cursor-based approach. The following example shows a simple calendar in a QTextEdit widget with bold headers for the days of the week:

     editor = new QTextEdit(this);

     QTextCursor cursor(editor->textCursor());
     cursor.movePosition(QTextCursor::Start);

     QTextCharFormat format(cursor.charFormat());
     format.setFontFamily("Courier");

     QTextCharFormat boldFormat = format;
     boldFormat.setFontWeight(QFont::Bold);

     cursor.insertBlock();
     cursor.insertText(" ", boldFormat);

     QDate date = QDate::currentDate();
     int year = date.year(), month = date.month();

     for (int weekDay = 1; weekDay <= 7; ++weekDay) {
         cursor.insertText(QString("%1 ").arg(QLocale::system().dayName(weekDay), 3),
             boldFormat);
     }

     cursor.insertBlock();
     cursor.insertText(" ", format);

     for (int column = 1; column < QDate(year, month, 1).dayOfWeek(); ++column) {
         cursor.insertText("    ", format);
     }

     for (int day = 1; day <= date.daysInMonth(); ++day) {
         int weekDay = QDate(year, month, day).dayOfWeek();

         if (QDate(year, month, day) == date)
             cursor.insertText(QString("%1 ").arg(day, 3), boldFormat);
         else
             cursor.insertText(QString("%1 ").arg(day, 3), format);

         if (weekDay == 7) {
             cursor.insertBlock();
             cursor.insertText(" ", format);
         }
     }

The above example demonstrates how simple it is to quickly generate new rich text documents using a minimum amount of code. Although we have generated a crude fixed-pitch calendar to avoid quoting too much code, Scribe provides much more sophisticated layout and formatting features.