QML Multi-line Texts Handling

From Qt Wiki
Jump to navigation Jump to search
This article may require cleanup to meet the Qt Wiki's quality standards. Reason: Auto-imported from ExpressionEngine.
Please improve this article if you can. Remove the {{cleanup}} tag and add this page to Updated pages list after it's clean.

English

QML Multi-line Texts Handling

Introduction

There are many situations where we need to handle relatively lengthy fragments of text, for example in help systems. Qt framework has its Assistance help system implemented in Qt C++. As another example we may consider learning content for which we need a flexible interface to multi-line blocks of text.

Don't confuse this with text processing; we are considering options for easy structuring of text in blocks and their rendering through a flexible QML interface.

At first look, QML with its Text element, is oriented to handling short text portions like labels, titles, dialogs, etc. Combining rich text formatting options in QML with its software architecture possibilities we may have interesting solutions.

The starting point is the requirement to separate text blocks from basic QML code, encapsulating the text blocks in autonomous units, and providing a powerful interface. It is clear that a text block can change very often - editing of the text, translation in other languages and so on. The solution is to form a QML component and instantiate it in the basic application code in places where we need it.

Text Component

Let us recall how we define a component in QML. The next snippet demonstrates our text component, which is used in all examples that follow. The text component is placed in a file named TextTemplate.qml. That is the name of our text component, which is used in main QML file. This name appears in next illustrating code fragments. You have needed explanations in comments.

TextTemplate.qml

//This file forms a text component to be included as template in other files
//Note that the name of the component file has to begin with a capital letter
//The component file is placed in the same directory as files, which will use it

import QtQuick 1.0

Item {id:container
//This Item is not needed really. The Text element could form the component
//directly. It is used to demonstrate how to get Text element properties at top
//level to be able to control them programmatically. You may need a container if you
//have two (or more) Text elements and/or combine a Text element with others element.
 width:500; height:500

//Bind Text element properties with properties at top level
 property string wrap:"Text.WordWrap"
 property real topwidth:300
 property string label:"Sample text"
 property color paint

 Text { id:template
 width:topwidth //Some properties of Text element take effect only
 //if width property is defined
 color:paint
 font.pointSize:12
 wrapMode:wrap //If the text line length exceeds the width property
 //value the text will be wrapped (on word boundaries)
 text:label //A text property value is anything that results in to a string
 }
}

Text Component Handling

As a first solution (and trivial one) for text blocks structuring we may define as many components as many text blocks we have. In the application code we include components in places where we need to display respective text blocks.

The second approach uses the possibility to bind a value to a QML element property at a later time - not when the element is defined. And we want to do this programmatically. In our text component we bind text property to another top level property (note it is of type string). This way the text component becomes a generic entity where different text property values could be bound when the component is used in the basic QML code. In other words, we use the text component as a template, instantiate it in different places and bind different text property values. That is what we call programmatically altering of text values.

The next point is how we could assign values in QML? As we know QML is a declarative language and it has no explicit operation assignment you may know from other procedural (imperative) languages. In QML we could bind a value in property definition. We may think about this as initializing of the property - the initial value could be changed later. This is similar as well for pre-defined properties as for own defined ones.

Another option is to construct a script block where assignment operation is allowed. There a few cases where we could define a script block. For example the value of an attached property of a signal could be a script block and what we will apply in the following examples.

Following the above ideas for text property altering we could have several approaches as explained bellow.

Direct Binding of a Value

The name of the main QML file is LargeTexts.qml.

LargeTexts.qml

//Main file

//*'''''' Scenario #1 ''''''
//We may bind a text when we instantiate the component TextTemplate
 TextTemplate {
 label: "Another sample text - Hello World"
 }
// End of Scenario #1

// End of main file

Use of Properties Defined at Top Level

//Main file

//Section of definition of top level entities
 property string topproperty:"Locate text block at the beginning of the file"

//*'''''' Scenario #2 ''''''
//We want to encapsulate the text block at the beginning of the file
 TextTemplate {
 label: topproperty

 }
// End of Scenario #2

// End of main file

Bind a Value Using QML Binding Element

//Main file

//Section of definition of top level entities
 property string newProperty:"Note that value of 'property' item in 'Binding'"''
"element is a string - double quotes."

//*'''''' Scenario #3 ''''''
//The same as Scenario #2, but using element Binding
 Binding {target:bind; property:"label"; value:newProperty}
 TextTemplate {id:bind}

// End of Scenario #3

// End of main file

Use onCompleted Attached Property

//Main file

//*'''* The following definitions are used by Scenario #4'''
 property string signalText
 property string arrayText
 Rectangle {
 id:startup
 Component.onCompleted: {
 var complete = "We are using onCompleted property";
 signalText=complete;
 var elements= ["text1","text2", "text3"];
 arrayText=elements[0];
 }
 }
//*''''''* End of definitions ''''''

//*'''''' Scenario #4 ''''''
//We use 'onCompleted' attached property to have a script element and
//assign some values

TextTemplate {id:top1
 label: signalText
 }
 TextTemplate {
 y:50
 paint:"red"
 label: arrayText
 }

// End of Scenario #4

// End of main file

Apply a JavaScript Inline Function

//Main file

//*'''* The following definitions are used by Scenario #5'''
 property string functAssignment:assign(2)
 function assign(i) {
 var list = ["textFragment1", "textFragment2", "textFragment3"];
 return list[i];
 }
//*''''''* End of definitions ''''''

//*'''''' Scenario #5 ''''''
//An inline JavaScript function - assign() - is used to assign a value

TextTemplate {
 label: functAssignment
 }

// End of Scenario #5

// End of main file

The third approach uses JavaScript constructs to define text blocks in a separate .js file. We recall that text property accepts as values anything that result in to a string. For example text property value could be an expression evaluated to a string. Particularly this value could be a string element of a JavaScript array.  The scheme is as follows:

//Main file
import QtQuick 1.0
import "arrayExt.js" as FileExt //This file is used by Scenario #6

//*'''''' Scenario #6 ''''''
//An external file - arrayExt.js - contains text blocks

TextTemplate {
 label: FileExt.externalArray[2]
 }

// End of Scenario #6

// End of main file

//External javaScript file - arrayExt.js
/* This external JavaScript file contains the text blocks */
 var externalArray = ["textblock1","textblock2","textblock3 -  use backslash to continue"];

Do you really need an array? If you prefer you may have a variable for each text block:

var text1 = "textblock1";
var text2 = "textblock2";

How to Format a Multi-line Text

As you know QML supports plain text as well as rich one. There is an essential difference here. QML Text element has built-in properties for text formatting like font.bold, font.pointSize, etc. When we use them they apply to the whole string being value of the text property. If you want to format a text fragment in your text block or even a separate word (symbol) you should use QML rich text mode. This of course supposes you have a background in HTML. Be aware that QML supports a sub-set of HTML/CSS - consult for supported HTML/CSS constructs. The QML runtime includes a JavaScript engine and supports JavaScript, HTML and CSS. For instance, if you create a HTML file with your favorite WYSIWYG HTML editor (without deep HTML coding understanding) it is most probably that QML will accept this file when you define it as a text block. For example this HTML file from my HTML editor works with QML:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html >

<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title>Untitled 1</title>
<style type="text/css">
 .auto-style2 {
 font-family: "Times New Roman", Times, serif;
 font-size: small;
 }
 .auto-style3 {
 background-color: #FFFF00;
 }
</style>
</head>

<body>
 <p><b>Header text</b><br/></p>
 <span class="auto-style3">This is paragraph text</span> 

 <hr />
</body> 

</html>

Now return to the case of plain text. Suppose we have a text header and a paragraph and want to format them differently- for example the header to be bold and colored. The simplest solution is to have two separate Text elements - one for the header and one for the paragraph. The better solution is to encapsulate somehow the both constructs. To do this we may nest Text elements like this:

import QtQuick 1.0

Rectangle {
 width: 360
 height: 360
 Text {id:top
 width:parent.width
 color:"lightblue"
 font.bold:true
 text: "Header Level"
 Text {id:nested
 anchors.top:parent.bottom
 anchors.topMargin:20
 width:parent.width
 text:"Paragraph Level"
 }
 }
 }

When we are dealing with a multi-line text in QML an important issue is how text is wrapped. Use QML Text element property wrapMode. It takes several enumerated values . For example you may use Text.WordWrap as it is done in our text component example. In this mode the text is wrapped on words boundaries. You have to define the Text element width property if want wrapMode to take effect.

You may fix the length of each line in a multi-line text using so called hard breaks. In a plain text use control symbol ' to mark line end. In a rich text we use
tag.

Text {id:text4
 text:"Plain text - first line\n Plain text - second line\n"
}
Text {
 anchors.top:text4.bottom
 text:"<b>Rich text - third line</b><br><i>Rich text - Fourth line</i>"
}

This approach is not recommended especially in cases the text changes often. Instead define the width property of the Text element and apply wrapMode property to have text block adapted to the text frame automatically.

Mixing QML, JavaScript and HTML/CSS constructs it is very easy to run into some syntax issues with some control symbols. The first problem comes from double quotes. They are used in QML as well in HTML/CSS also. The simplest solution seems to be replacing double quotes in text property with single ones or vice-verse. Of course you may use escape sequence ' also like this:

Rectangle {
 width: 360
 height: 360
 Text {id:text1
 width:parent.width
 text: '<p> <span >Sample text</span></p>'
 }
 Text {id:text2
 anchors.top:text1.bottom
 text: "<p> <span >Sample text</span></p>"
 }
 Text {id:text3
 anchors.top:text2.bottom
 text: "<p> <span style=quot;color:blackquot;>Sample text</span></p>"
 }
}

When composing your text block in Qt Quick or other editors you will want to break each line of the text to fit on the screen without any scrolling. If such a block is defined in a script element we have to keep to rules for string splitting in multiple lines. Otherwise you get a message like "Parse error". The proposed solution is: before breaking a text line add a control symbol for string continuation - _. Probably you know it from other programming languages.

Rectangle {
 property string new1
 property string split
 property string how:"String bound to a property
is split in two lines"
 width: 360
 height: 360

Text {id:text0
 width:parent.width
 text:how
 }

Rectangle {
 Component.onCompleted: {new1="Not allowed
splitting"}
 }

Rectangle {
 Component.onCompleted: {split="Use backslash to split a string defined in a script element"}
 }
 Text {id:text00
 anchors.top:text0.bottom
 text:split
 }
}

Make Your Text Block Flickable

It is obvious that for bigger text blocks we should make them somehow scrollable. QML offers a very useful construct Flickable for these purposes. Recall the Flickable construct from. The main question here is what should be the width and height dimensions of the flickable area. You may fix them, but it is not recommendable. The width property depends on the page design rather. Inherit the width value from Flickable parents. For height property we may use properties binding and calculate Flickable height programmatically. Assume we have three Text elements included in Flickable element that have IDs like this: id:element1, id:element2 and id:element3. Then Flickable element height property could be calculated as:

Flicable {

height:element1.height+element2.height+element3.height+200

}  //end of Flickable

  Note: You may get a demo file illustrating the discussed constructs. Download it at Syncho Server

Another Flickable example

The basic Qt Flickable documentation is quite poor and can lead to hours of frustration. Here is a real world stripped-down example, a component which can be placed e.g. in a Dialog as a contentItem. The flickable text area size scales automatically when the window size changes and the text is wrapped.

It's important to find a working combination of properties of the Flickable, its parent and the Text item. The clip property of the Flickable is important: without it the Text is visible even outside the Flickable area so that the button seems to float over the text. Only the left and right anchors of the Text should be set. If the top and bottom anchors are anchored to the parent, the text won't scroll. Similarly, if there's a Layout inside the Flickable and the Text is inside it, only fillWidth should be set.

Item {
    ColumnLayout {
        id: columnLayout1
        anchors.fill: parent
        Flickable {
            id: flickable1
            clip: true
            Layout.fillHeight: true
            Layout.fillWidth: true
            contentWidth: parent.width
            contentHeight: text1.height
            Text {
                id: text1
                text: "Text placeholder"
                anchors.left: parent.left
                anchors.right: parent.right
                wrapMode: Text.WordWrap
            }
        }
        Button {id: b1}
    }
}