=> Главная База Знаний Qt Создание подкласса qtablewidgetltem


Создание подкласса qtablewidgetltem

Класс Cell наследует QTableWidgetltem. Этот класс спроектирован для удобства работы с Spreadsheet, но он не имеет никаких особых связей с данным классом электронной таблицы и теоретически может применяться для любого объекта QTableWidget. Ниже приводится заголовочный файл:

01 #ifndef CELL_H

02 #define CELL_H

03 #include <QTableWidgetItem>


04 class Cell : public QTableWidgetltem

05 {

06 public:

07 Cell();

08 QTableWidgetltem *clone() const;

09 void setData(int role, const QVariant &value);

10 QVariant data(int role) const;

11 void setFormula(const QString &formula);

12 QString formula() const;

13 void setDirty();


14 private:

15 QVariant value() const;

16 QVariant evalExpression(const QString &str, int &pos) const;

17 QVariant evalTerm(const QString &str, int &pos) const;

18 QVariant evalFactor(const QString &str, int &pos) const;

19 mutable QVariant cachedValue;

20 mutable bool cacheIsDirty;

21 };

22 #endif

Класс Cell расширяет QTableWidgetltem, добавляя две закрытые переменные:

• переменная cachedValue кэширует значение ячейки в виде значения типа QVariant;

• переменная cacheIsDirty принимает значение true, если кэшируемое значение устарело.

Мы используем QVariant, поскольку некоторые ячейки имеют тип числа двойной точности double, а другие имеют тип строки QString.

При объявлении переменных cachedValue и cacheIsDirty используется ключевое слово mutable языка С++. Это позволяет нам модифицировать эти переменные в функциях с модификатором const. Мы могли бы поступить по-другому и заново выполнять расчет при каждом вызове функции text(), но эта неэффективность будет не оправдана.

Следует отметить, что в определении класса не используется макрос Q_OBJECT. Класс Cell является «чистым» классом С++, который не имеет сигналов и слотов. На самом деле из-за того, что QTableWidgetltem не является наследником QObject, мы не можем использовать в Cell как таковые сигналы и слоты. Классы элементов Qt не наследуют QObject, чтобы свести к минимуму затраты на их обработку. Если сигналы и слоты необходимы, они могут быть реализованы в виджете, содержащем элементы, или (в виде исключения) при помощи множественного наследования класса QObject.

Теперь мы перейдем к написанию cell.cpp:

01 #include <QtGui>

02 #include "cell.h"

03 Cell::Cell()

04 {

05 setDirty();

06 }

В конструкторе нам необходимо установить признак «dirty» («грязный») только для кэша. Передавать родительский объект нет необходимости; когда делается вставка ячейки в QTableWidget с помощью setItem(), QTableWidget автоматически станет ее владельцем.

Каждый элемент QTableWidgetltem может иметь некоторые данные — до одного типа QVariant на каждую «роль» данных. Наиболее распространенными ролями являются Qt::EditRole и Qt::DisplayRole (роль правки и роль отображения). Роль правки используется для данных, которые должны редактироваться, а роль отображения — для данных, которые должны отображаться на экране. Часто обе роли используются для одних и тех же данных, однако в Cell роль правки соответствует формуле ячейки, а роль отображения — значению ячейки (результату вычисления формулы).

02 QTableWidgetltem *Cell::clone() const

03 {

04 return new Cell(*this);

05 }

Функция clone() вызывается в QTableWidget, когда необходимо создать новую ячейку, например когда пользователь начинает вводить данные в пустую ячейку, которая до сих пор не использовалась. Переданный функции QTableWidget::setItemPrototype() экземпляр является дубликатом. Поскольку для копирования Cell можно ограничиться функцией—членом, мы полагаемся на используемый по умолчанию конструктор копирования, автоматически создаваемый С++ при создании экземпляров новых ячеек Cell в функции clone().

06 void Cell::setFormula(const QString &formula)

07 {

08 setData(Qt::EditRole, formula);

09 }

Функция setFormula() задает формулу ячейки. Это просто удобная функция для вызова setData() с указанием роли правки. Она вызывается из функции Spreadsheet::setFormula().

10 QString Cell::formula() const

11 {

12 return data(Qt::EditRole).toString();

13 }

Функция formula() вызывается из Spreadsheet::formula(). Подобно setFormula() этой функцией удобно пользоваться на этот раз для получения данных EditRole заданного элемента.

14 void Cell::setData(int role, const QVariant &value)

15 {

16 QTableWidgetltem::setData(role, value);

17 if (role == Qt::EditRole)

18 setDirty();

19 }

Если мы имеем новую формулу, мы устанавливаем cacheIsDirty на значение true, чтобы обеспечить перерасчет ячейки при последующем вызове text().

В Cell нет определения функции text(), хотя мы и вызываем text() для экземпляров Cell в функции Spreadsheet::text(). QTableWidgetltem содержит удобную функцию text(), которая эквивалентна вызову data(Qt::DisplayRole).toString().

20 void Cell::setDirty()

21 {

22 cacheIsDirty = true;

23 }

Функция setDirty() вызывается для принудительного перерасчета значения ячейки. Она просто устанавливает флажок cacheIsDirty на значение true, указывая на то, что значение cachedValue больше не отражает текущее состояние. Перерасчет не будет выполняться до тех пор, пока он не станет действительно необходим.

24 QVariant Cell::data(int role) const

25 {

26 if (role == Qt::DisplayRole) {

27 if (value().isValid()) {

28 return value().toString();

29 } else {

30 return "####";

31 }

32 } else if (role == Qt::TextAlignmentRole) {

33 if (value().type() == QVariant::String) {

34 return int(Qt::AlignLeft | Qt::AlignVCenter);

35 } else {

36 return int(Qt::AlignRight | Qt::AlignVCenter);

37 }

38 } else {

39 return QTableWidgetltem::data(role);

40 }

41 }

Функция data() класса QTableWidgetltem переопределяется. Она возвращает текст, который должен отображаться в электронной таблице, если в вызове указана роль Qt::DisplayRole, или формулу, если в вызове указана роль Qt::EditRole. Она обеспечивает подходящее выравнивание, если вызывается с ролью Qt::TextAlignmentRole. При задании роли DisplayRole она использует функцию value() для расчета значения ячейки. Если нельзя получить достоверное значение (из-за того, что формула неверна), мы возвращаем значение «####».

Функция Cell::value(), используемая в data(), возвращает значение типа QVariant. Объекты типа QVariant могут содержать значения различных типов, например double или QString, и поддерживают функции для преобразования их в другие типы. Например, при вызове toString() для переменной типа QVariant, содержащей значение типа double, в результате мы получим строковое представление числа с двойной точностью. Используемый по умолчанию конструктор QVariant устанавливает значение «invalid» (недопустимое).

42 const QVariant Invalid;


43 QVariant Cell::value() const

44 {

45 if (cacheIsDirty) {

46 cacheIsDirty = false;


47 QString formulaStr = formula();

48 if (formulaStr.startsWith('\'')) {

49 cachedValue = formulaStr.mid(1);

50 } else if (formulaStr.startsWith('=')) {

51 cachedValue = Invalid;

52 QString expr = formulaStr.mid(1);

53 expr.replace(" ", "");

54 expr.append(QChar::Null);


55 int pos = 0;

56 cachedValue = evalExpression(expr, pos);

57 if (expr[pos] != QChar::Null)

58 cachedValue = Invalid;

59 } else {

60 bool ok;

61 double d = formulaStr.toDouble(&ok);

62 if (ok) {

63 cachedValue = d;

64 } else {

65 cachedValue = formulaStr;

66 }

67 }

68 }

69 return cachedValue;

70 }

Закрытая функция value() возвращает значение ячейки. Если флажок cacheIsDirty имеет значение true, нам необходимо выполнить перерасчет значения.

Если формула начинается с одиночной кавычки (например, «'12345»), то одиночная кавычка занимает позицию 0, а значение представляет собой строку в позициях с 1 до последней.

Если формула начинается со знака равенства («=»), мы выделяем строку, начиная с позиции 1, и удаляем из нее любые пробелы. Затем мы вызываем функцию evalExpression() для вычисления значения выражения. Аргумент pos передается по ссылке; он задает позицию символа, с которого должен начинаться синтаксический анализ выражения. После вызова функции evalExpression() в позиции pos нами должен быть установлен символ QChar::Null, если синтаксический анализ завершился успешно. Если синтаксический анализ не закончился успешно, мы устанавливаем cachedValue на значение Invalid.

Если формула не начинается с одиночной кавычки или знака равенства, мы пытаемся преобразовать ее в число с плавающей точкой, используя функцию toDouble(). Если преобразование удается выполнить, мы устанавливаем cachedValue на полученное значение; в противном случае мы устанавливаем cachedValue на строку формулы. Например, формула «1.50» приводит к тому, что функция toDouble() устанавливает переменную ok на значение true и возвращает 1.5, а формула «World Population» (население Земли) приводит к тому, что функция toDouble() устанавливает переменную ok на значение false и возвращает 0.0.

Благодаря заданному в функции toDouble() указателю на булево значение мы можем отличать строку преобразования, представляющую числовое значение 0.0, от ошибки преобразования (в последнем случае также возвращается 0.0, но булева переменная устанавливается в значение false). Иногда нулевое значение при неудачном преобразовании оказывается именно тем, что нам нужно; в этом случае нет необходимости передавать указать на переменную типа bool. По причинам, связанным с производительностью и переносимостью, в Qt никогда не используются исключения С++ для вывода сообщений об ошибках. Это не значит, что вы не можете использовать их в своих Qt—программах, если ваш компилятор поддерживает исключения С++.

Функция value() объявлена с модификатором const. При объявлении переменных cachedValue и cacheIsValid мы использовали ключевое слово mutable, чтобы компилятор позволял нам модифицировать эти переменные в функциях типа const. Может показаться заманчивой возможность сделать функцию value() не типа const и удалить ключевые слова mutable, но это не пропустит компилятор, поскольку мы вызываем value() из data() — функции с модификатором const.


Теперь можно считать, что мы завершили приложение Электронная таблица, если не брать в расчет синтаксический анализ формул. В остальной части данного раздела рассматриваются функция evalExpression() и две вспомогательные функции evalTerm() и evalFactor(). Их программный код немного сложен, но он включен сюда, чтобы приложение имело законченный вид. Поскольку этот программный код не относится к программированию графического интерфейса, вы можете спокойно его пропустить и продолжить чтение с главы 5.

Функция evalExpression() возвращает значение выражения из ячейки электронной таблицы. Выражение состоит из одного или нескольких термов, разделенных знаками операций «+» или «—». Термы состоят из одного или нескольких факторов (factors), разделенных знаками операций «*» или «/». Разбивая выражения на термы, а термы на факторы, мы обеспечиваем правильную последовательность выполнения операций.

Например, «2*C5+D6» является выражением, первый терм которого будет «2*C5», а второй терм — «D6». «2*C5» является термом, первый фактор которого будет «2», а второй фактор — «C5»; «D6» состоит из одного фактора — «D6». Фактором могут быть число («2»), обозначение ячейки («C5») или выражение в скобках, перед которым может стоять знак минуса.

Рис. 4.10. Блок—схема синтаксического анализа выражений электронной таблицы.

Блок—схема синтаксического анализа выражений электронной таблицы представлена на рис. 4.10. Для каждого грамматического символа (Expression, Term и Factor — выражение, терм и фактор) имеется соответствующая функция—член, которая выполняет его синтаксический анализ и структура которой очень хорошо отражает его грамматику. Построенные таким образом синтаксические анализаторы называются парсерами с рекурсивным спуском (recursive—descent parsers).

Давайте начнем с evalExpression(), то есть с функции, которая выполняет синтаксический разбор выражения:

01 QVariant Cell::evalExpression(const QString &str, int &pos) const

02 {

03 QVariant result = evalTerm(str, pos);

04 while (str[pos] != QChar::Null) {

05 QChar op = str[pos];

06 if (op != '+' && op != '-') return result;

07 ++pos;


08 QVariant term = evalTerm(str, pos);

09 if (result.type() == QVariant::Double

10 && term.type() == QVariant::Double) {

11 if (op == '+') {

12 result = result.toDouble() + term.toDouble();

13 } else {

14 result= result.toDouble() - term.toDouble();

15 }

16 } else {

17 result = Invalid;

18 }

19 }

20 return result;

21 }

Во-первых, мы вызываем функцию evalTerm() для получения значения первого терма. Если за ним идет символ «+» или «—», мы вызываем второй раз evalTerm(); в противном случае выражение состоит из единственного терма, и мы возвращаем его значение в качестве значения всего выражения. После получения значений первых двух термов мы вычисляем результат операции в зависимости от оператора. Если при оценке обоих термов их значения будут иметь тип double, мы рассчитываем результат в виде числа типа double; в противном случае мы устанавливаем результат на значение Invalid.

Мы продолжаем эту процедуру, пока не закончатся термы. Это даст правильный результат, потому что операции сложения и вычитания обладают свойством «ассоциативности слева» (left—associative), то есть «1—2—3» означает «(1—2)—3», а не «1—(2—3)».

01 QVariant Cell::evalTerm(const QString &str, int &pos) const

02 {

03 QVariant result = evalFactor(str, pos);

04 while (str[pos] != QChar::Null) {

05 QChar op = str[pos];

06 if (op != '*' && op != '/')

07 return result;

08 ++pos;


09 QVariant factor = evalFactor(str, pos);

10 if (result.type() == QVariant::Double &&

11 factor.type() == QVariant::Double) {

12 if (op == '*') {

13 result = result.toDouble() * factor.toDouble();

14 } else {

15 if (factor.toDouble() == 0.0) {

16 result = Invalid;

17 } else {

18 result = result.toDouble() / factor.toDouble();

19 }

20 }

21 } else {

22 result = Invalid;

23 }

24 }

25 return result;

26 }

Функция evalTerm() очень напоминает функцию evalExpression(), но, в отличие от последней, она имеет дело с операциями умножения и деления. В функции evalTerm() необходимо учитывать одну тонкость, а именно: нельзя допускать деления на нуль, так как это приводит к ошибке на некоторых процессорах. Хотя не рекомендуется проверять равенство чисел с плавающей точкой из-за ошибки округления, можно спокойно делать проверку на равенство значению 0.0 для предотвращения деления на нуль.

01 QVariant Cell::evalFactor(const QString &str, int &pos) const

02 {

03 QVariant result;

04 bool negative = false;

05 if (str[pos] == '-') {

06 negative = true;

07 ++pos;

08 }

09 if (str[pos] == '(') {

10 ++pos;

11 result = evalExpression(str, pos);

12 if (str[pos] != ')')

13 result = Invalid;

14 ++pos;

15 } else {

16 QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}");

17 QString token;

18 while (str[pos].isLetterOrNumber() || str[pos] == '.') {

19 token += str[pos];

20 ++pos;

21 }


22 if (regExp.exactMatch(token)) {

23 int column = token[0].toUpper().unicode() - 'A';

24 int row = token.mid(1).toInt() - 1;

25 Cell *c = static_cast<Cell *>(tableWidget()->item(row, column));

26 if (c) {

27 result = c->value();

28 } else {

29 result = 0.0;

30 }

31 } else {

32 bool ok;

33 result = token.toDouble(&ok);

34 if (!ok)

35 result = Invalid;

36 }

37 }


38 if (negative) {

39 if (result.type() == QVariant::Double) {

40 result = -result.toDouble();

41 } else {

42 result = Invalid;

43 }

44 }

45 return result;

46 }

Функция evalFactor() немного сложнее, чем evalExpression() и evalTerm(). Мы начинаем с проверки, не является ли фактор отрицательным. Затем мы проверяем наличие открытой скобки. Если она имеется, мы анализируем значение внутри скобок как выражение, вызывая evalExpression(). При анализе выражения в скобках evalExpression() вызывает функцию evalTerm(), которая вызывает функцию evalFactor(), которая вновь вызывает функцию evalExpression(). Именно в этом месте осуществляется рекурсия при синтаксическом анализе.

Если фактором не является вложенное выражение, мы выделяем следующую лексему (token), и она должна задавать обозначение ячейки или быть числом. Если эта лексема удовлетворяет регулярному выражению в переменной QRegExp, мы считаем, что она является ссылкой на ячейку, и вызываем функцию value() для этой ячейки. Ячейка может располагаться в любом месте в электронной таблице, и она может ссылаться на другие ячейки. Такая зависимость не вызывает проблемы и просто приводит к дополнительным вызовам функции value() и к дополнительному синтаксическому анализу ячеек с признаком «dirty» («грязный») для перерасчета значений всех зависимых ячеек. Если лексема не является ссылкой на ячейку, мы рассматриваем ее как число.

Что произойдет, если ячейка A1 содержит формулу «=A1»? Или если ячейка A1 содержит «=A2», а ячейка A2 содержит «=A1»? Хотя нами не написан специальный программный код для обнаружения бесконечных циклов в рекурсивных зависимостях, парсер прекрасно справится с этой ситуацией и возвратит недопустимое значение переменной типа QVariant. Это даст нужный результат, поскольку мы устанавливаем флажок cacheIsDirty на значение false и переменную cachedValue на значение Invalid в функции value() перед вызовом evalExpression(). Если evalExpression() рекурсивно вызывает функцию value() для той же ячейки, она немедленно возвращает значение Invalid, и тогда все выражение принимает значение Invalid.

Теперь мы завершили программу синтаксического анализа формул. Ее можно легко модифицировать для обработки стандартных функций электронной таблицы, например «sum()» и «avg()», расширяя грамматическое определение фактора. Можно также легко расширить эту реализацию, обеспечив возможность выполнения операции «+» над строковыми операндами (для их конкатенации); это не потребует внесения изменений в грамматику.