QString and QVariant

Strings are used by every GUI program, not only for the user interface, but often also as data structures.

C++ natively provides two kinds of strings: traditional C-style ''-terminated character arrays and the string class. Qt's QString class is more powerful than either of them. The QString class holds 16-bit Unicode values. Unicode contains ASCII and Latin-1 as a subset, with their usual numeric values. But since QString is 16-bit, it can represent thousands of other characters for writing most of the world's languages. See Chapter 15 for more information about Unicode.

QString provides a binary + operator to concatenate two strings and a += operator to append one string to another. Here's an example that combines both:

QString str = "User: "; str += userName + " ";

There is also a QString::append() function that does the same thing as the += operator:

str = "User: "; str.append(userName); str.append(" ");

A completely different way of combining strings is to use QString's sprintf() function:

str.sprintf("%s %.1f%%", "perfect competition", 100.0);

This function supports the same format specifiers as the C++ library's sprintf() function. In the example above, str is assigned "perfect competition 100.0%".

Yet another way of building a string from other strings or from numbers is to use arg():

str = QString("%1 %2 (%3s-%4s)") .arg("permissive").arg("society").arg(1950).arg(1970);

In this example, "%1" is replaced by "permissive", "%2" is replaced by "society", "%3" is replaced by "1950", and "%4" is replaced by "1970". The result is "permissive society (1950s-1970s)". There are arg() overloads to handle various data types. Some overloads have extra parameters for controlling the field width, the numerical base, or the floating-point precision. In general, arg() is a much better solution than sprintf(), because it is type-safe, fully supports Unicode, and allows translators to change the order of the "%n" parameters.

QString can convert numbers into strings using the QString::number() static function:

str = QString::number(59.6);

Or using the setNum() function:

str.setNum(59.6);

The reverse conversion, from a string to a number, is achieved using toInt(), toLongLong(), toDouble(), and so on. For example:

bool ok; double d = str.toDouble(&ok);

These functions also accept an optional pointer to a bool and set the bool to true or false depending on the success of the conversion. When the conversion fails, these functions always return 0.

Once we have a string, we often want to extract parts of it. The mid() function returns the substring starting at a given position and of a given length. For example, the following code prints "pays" to the console:

QString str = "polluter pays principle"; cerr << str.mid(9, 4).ascii() << endl;

If we omit the second argument (or pass 1), mid() returns the substring starting at a given position and ending at the end of the string. For example, the following code prints "pays principle" to the console:

QString str = "polluter pays principle"; cerr << str.mid(9).ascii() << endl;

There are also left() and right() functions that perform a similar job. Both accept a number of characters, n, and return the first or last n characters of the string. For example, the following code prints "polluter principle" to the console:

QString str = "polluter pays principle"; cerr << str.left(8).ascii() << " " << str.right(9).ascii() << endl;

If we want to check if a string starts or ends with something, we can use the startsWith() and endsWith() functions:

if (uri.startsWith("http:") && uri.endsWith(".png")) ...

This is both simpler and faster than this:

if (uri.left(5) == "http:" && uri.right(4) == ".png") ...

String comparison with the == operator is case sensitive. For case insensitive comparisons, we can use upper() or lower(). For example:

if (fileName.lower() == "readme.txt") ...

If we want to replace a certain part of a string by another string, we can use replace():

QString str = "a sunny day"; str.replace(2, 5, "cloudy");

The result is "a cloudy day". The code can be rewritten to use remove() and insert():

str.remove(2, 5); str.insert(2, "cloudy");

First, we remove five characters starting at position 2, resulting in the string "a day" (with two spaces), then we insert "cloudy" at position 2.

There are overloaded versions of replace() that replace all occurrences of their first argument with their second argument. For example, here's how to replace all occurrences of "&" with "&" in a string:

str.replace("&", "&");

One very frequent need is to strip the whitespace (such as spaces, tabs, and newlines) from a string. QString has a function that strips whitespace from both ends of a string:

QString str = " BOB THE DOG "; cerr << str.stripWhiteSpace().ascii() << endl;

String str can be depicted as

The string returned by stripWhiteSpace() is

When handling user input, we often also want to replace every sequence of one or more internal whitespace characters with single spaces, in addition to stripping whitespace from both ends. This is what the simplifyWhiteSpace() function does:

QString str = " BOB THE DOG "; cerr << str.simplifyWhiteSpace().ascii() << endl;

The string returned by simplifyWhiteSpace() is

A string can be split into substrings using QStringList::split():

QString str = "polluter pays principle"; QStringList words = QStringList::split(" ", str);

In the example above, we split the string "polluter pays principle" into three substrings: "polluter", "pays", and "principle". The split() function has an optional bool third argument that specifies whether empty substrings should be ignored (the default) or not.

The elements in a QStringList can be joined to form a single string using join(). The argument to join() is inserted between each pair of joined strings. For example, here's how to create a single string that is composed of all the strings contained in a QStringList sorted into alphabetical order and separated by newlines:

words.sort(); str = words.join(" ");

When dealing with strings, we often need to determine whether a string is empty or not. One way of testing this is to call isEmpty(); another way is to check whether length() is 0.

QString distinguishes between null strings and empty strings. This distinction has its roots in the C language, which differentiates between 0 (a null pointer) and "" (an empty string). To test whether a string is null, we can call isNull(). For most applications, what matters is whether or not a string contains any characters. The isEmpty() function provides this information, returning true if a string has no characters (is null or empty), and false otherwise.

The conversions between const char * strings and QString is automatic in most cases, for example:

str += " (1870)";

Here we add a const char * to a QString without formality.

In some situations, it is necessary to explicitly convert between const char * and QString. To convert a QString to a const char *, use ascii() or latin1(). To convert the other way, use a QString cast.

When we call ascii() or latin1() on a QString, or when we let the automatic conversion to const char * do its work, the returned string is owned by the QString object. This means that we don't need to worry about memory leaks; Qt will reclaim the memory for us. On the other hand, we must be careful not to use the pointer for too long. For example, if we modify the original QString, the pointer is not guaranteed to remain valid. If we need to store the const char * for any length of time, we can assign it to a variable of type QByteArray or QCString. These will hold a complete copy of the data.

QString is implicitly shared. This means that copying a QString is about as fast as copying a single pointer. Only if one of the copies is changed is data actually copiedand this is all handled automatically behind the scenes. For this reason, implicit sharing is sometimes referred to as "copy on write".

The beauty of implicit sharing is that it is an optimization that we don't have to think about; it simply works, without requiring any programmer intervention.

Qt uses implicit sharing for many other classes, including QBrush, QFont, QPen, QPixmap, QMap, QValueList, and QValueVector. This makes these classes very efficient to pass by value, both as function parameters and as return values.

C++ is a strongly typed language, and this provides many benefits, including type safety and efficiency. However, in some situations, it is useful to be able to store data more generically, and one conventional way of doing so is to use strings. For example, a string could hold a textual value or a numeric value in string form. Qt provides a much cleaner way of handling variables that can hold different types: QVariant.

The QVariant class can hold values of many Qt types, including QBrush, QColor, QCursor, QDateTime, QFont, QKeySequence, QPalette, QPen, QPixmap, QPoint, QRect, QRegion, QSize, and QString. The QVariant class can also hold containers: QMap, QVariant>, QStringList, and QValueList. We used a QVariant in the implementation of the Spreadsheet application in Chapter 4 to hold the value of a cell, which could be either a QString, a double, or an invalid value.

One common use of variants is in a map that uses strings as keys and variants as values. Configuration data is normally saved and retrieved using QSettings, but some applications may handle this data directly, perhaps storing it in a database. QMap, QVariant> is ideal for such situations:

QMap config; config["Width"] = 890; config["Height"] = 645; config["ForegroundColor"] = black; config["BackgroundColor"] = lightGray; config["SavedDate"] = QDateTime::currentDateTime(); QStringList files; files << "2003-05.dat" << "2003-06.dat" << "2003-07.dat"; config["RecentFiles"] = files;

How Implicit Sharing Works

Implicit sharing works automatically behind the scenes, so when we use classes that are implicitly shared, we don't have to do anything in our code to make this optimization happen. But since it's nice to know how things work, we will study an example and see what happens under the hood.

QString str1 = "Humpty"; QString str2 = str1;

We set str1 to "Humpty" and str2 to be equal to str1. At this point, both QStrings point to the same data structure in memory (of type QStringData). Along with the character data, the data structure holds a reference count that indicates how many QStrings point to the same data structure. Since both str1 and str2 point to the same data, the reference count is 2.

str2[0] = 'D';

When we modify str2, it first makes a deep copy of the data, to ensure that str1 and str2 point to different data structures, and it then applies the change to its own copy of the data. The reference count of str1's data ("Humpty") becomes 1, and the reference count of str2's data ("Dumpty") is set to 1.A reference count of 1 means that the data isn't shared.

str2.truncate(4);

If we modify str2 again, no copying takes place because the reference count of str2's data is 1. The truncate() function operates directly on str2's data, resulting in the string "Dump". The reference count stays at 1.

str1 = str2;

When we assign str2 to str1, the reference count for str1's data goes down to 0, which means that no QString is using the "Humpty" data anymore. The data is then freed from memory. Both QStrings now point to "Dump", which now has a reference count of 2.

Writing implicitly shared classes isn't very difficult. The Qt Quarterly article "Data Sharing with Class", available online at http://doc.trolltech.com/qq/qq02-data-sharing-with-class.html, explains how to do it.

Iterating over a map that holds variant values can be slightly tricky if some of the values are containers. We need to use type() to check the type that a variant holds so that we can respond appropriately:

QMap::const_iterator it = config.begin(); while (it != config.end()) { QString str; if (it.data().type() == QVariant::StringList) str = it.data().toStringList().join(", "); else str = it.data().toString(); cerr << it.key().ascii() << ": " << str.ascii() << endl; ++it; }

It is possible to create arbitrarily complex data structures using QVariant by holding values of container types:

QMap price; price["Orange"] = 2.10; price["Pear"].asMap()["Standard"] = 1.95; price["Pear"].asMap()["Organic"] = 2.25; price["Pineapple"] = 3.85;

Here we have created a map with string keys (product names) and values that are either floating-point numbers (prices) or maps. The top level map contains three keys: "Orange", "Pear", and "Pineapple". The value associated with the "Pear" key is a map that contains two keys ("Standard" and "Organic").

Creating data structures like this can be very seductive since we can structure the data in any way we like. But the convenience of QVariant comes at a price. For the sake of readability, it is usually worth defining a proper C++ class to store our data. A custom class provides type safety and will also be more speed- and memory-efficient than using QVariant.

Chapter 12 Databases

Категории