建立QTableWidgetItem子類
Cell類繼承自QTableWidgetItem。這個類被設計用於Spreadsheet並能很好的工作,但它對該類沒有特別的依賴,所以理倫上能用於所有QTableWidget。下面是它的標頭檔:
#ifndef CELL_H#define CELL_H#include <QTableWidgetItem>class Cell : public QTableWidgetItem{public: Cell(); QTableWidgetItem *clone() const; void setData(int role, const QVariant &value); QVariant data(int role) const; void setFormula(const QString &formula); QString formula() const; void setDirty();private: QVariant value() const; QVariant evalExpression(const QString &str, int &pos) const; QVariant evalTerm(const QString &str, int &pos) const; QVariant evalFactor(const QString &str, int &pos) const; mutable QVariant cachedValue; mutable bool cacheIsDirty;};#endif
Cell類通過添加兩個私人變數擴充了QTableWidgetItem:
cachedValue緩衝儲存格的值,把它當作Qvariant類型。
cacheIsDirty 如果緩衝的值不是最新的則它是true。
因為一些儲存格是double類型的值,而另一些是QString類型的值,所有我們使用了Qvariant。
cachedValue 和 cacheIsDirty變數是使用c++ mutable關鍵字聲明的。這允許我們在常量型函數中修改這些變數。另一種選擇是,我們在每次text()調用的時候重新計算 ,但這應該是不必要的麻煩。
注意在該類的定義中沒有OBJECT宏。Cell是一個普通C++類,沒有訊號或者槽。事實上,因為QTableWidgetItem不是繼承自QObject,在Cell中不能有訊號和槽。Qt的項類並不是繼承自QOIbject以保證他們的額外開支最少。如果需要訊號和槽,他們可以在包含這些項的物件類中實現,或者,更特別地,可以與QObject一塊使用多繼承。
下面是cell.cpp的開始部分:
#include <QtGui>
#include "cell.h"
Cell::Cell()
{
setDirty();
}
在建構函式中,我們僅需要把緩衝設為髒資料。這裡不需要傳遞一個父物件。因為這個儲存格被用setItem()插入到QTableWidget中,QTableWidget會自動獲得對它的擁有權。
每個QTableWidgetItem 都能儲存一些資料,每個資料“角色”為一個QVariant。最常用的角色是Qt::EditRole 和 Qt::DisplayRole。編輯角色表示要被編輯的資料,顯示角色表示要被顯示的資料。通過他們倆是一樣的資料,但是在儲存格中,編輯角色對應於儲存格的公式,顯示角色對應於儲存格的值(對評估公式的結果)。
QTableWidgetItem *Cell::clone() const
{
return new Cell(*this);
}
在QTableWidget要建立一個新的儲存格的時候,clone()函數將被調用。例如,當使用者向一個從未使用過的空儲存格中輸入時文本時。傳遞到QTableWidget::setItemPrototype()的執行個體是複製出來的。因為對Cell來說成員級拷貝就足夠了,所以我們就在clone()函數中使用C++自動建立的預設拷貝建構函式來建立新的Cell執行個體。
void Cell::setFormula(const QString &formula)
{
setData(Qt::EditRole, formula);
}
setFormula()函數設定儲存格的公式。它是調用使用編輯角色的setData()的一個簡單方便的函數。它在Spreadsheet::setFormula()中被調用。
formula()函數在Spreadsheet::formula()中被調用。像setFormula()一樣,它也是一個方便函數,不過這次是擷取項的EditRole資料。
void Cell::setData(int role, const QVariant &value)
{
QTableWidgetItem::setData(role, value);
if (role == Qt::EditRole)
setDirty();
}
如果我們有一個新的公式,我們把cacheIsDirty設為true來保證儲存格的值在下次調用text()的時候被重新計算。
雖然我們在Spreadsheet::text()中調用Cell執行個體的text()方法,但其實Cell中並沒有定義text()函數。該text()函數是一個由QTableWidgetItem提供的便利函數。它等坐於調用 data(Qt::Display-Role).toString()。
void Cell::setDirty()
{
cacheIsDirty = true;
}
setDirty()函數調用用於強制重新計算儲存格的值。它只需簡單地把cacheIsDirty 為true,這意味著cacheValue不是最新的了。重算過程直到必要的時候才被執行。
QVariant Cell::data(int role) const
{
if (role == Qt::DisplayRole) {
if (value().isValid()) {
return value().toString();
} else {
return "####";
}
} else if (role == Qt::TextAlignmentRole) {
if (value().type() == QVariant::String) {
return int(Qt::AlignLeft | Qt::AlignVCenter);
} else {
return int(Qt::AlignRight | Qt::AlignVCenter);
}
} else {
return QTableWidgetItem::data(role);
}
}
data()函數是QTableWidgetItem中的函數的重新實現。如果使用Qt::DisplayRole它會返回顯示在試算表中的文本,如果使用Qt::EditRole調用則會返回公式,如果使用Qt::TextAlignmentRole調用則返回一個合適的對齊。在DisplayRole情況下,它依靠value()來計算相應儲存格的值。如果此值無效(因為公式是錯誤的)我們就返回“####”。
用在data()中的Cell::value()返回一個QVariant。一個QVariant能儲存不同類型的值,如double和QString,並提供了一些函數把它轉換為其他類型。例如,調用一個儲存了double型值的變數的toString()將會產生一個表示該double值的字串。使用預設建構函式構造的QVariant是一個無效變數。
const QVariant Invalid;
QVariant Cell::value() const
{
if (cacheIsDirty) {
cacheIsDirty = false;
QString formulaStr = formula();
if (formulaStr.startsWith(''')) {
cachedValue = formulaStr.mid(1);
} else if (formulaStr.startsWith('=')) {
cachedValue = Invalid;
QString expr = formulaStr.mid(1);
expr.replace(" ", "");
expr.append(QChar::Null);
int pos = 0;
cachedValue = evalExpression(expr, pos);
if (expr[pos] != QChar::Null)
cachedValue = Invalid;
} else {
bool ok;
double d = formulaStr.toDouble(&ok);
if (ok) {
cachedValue = d;
} else {
cachedValue = formulaStr;
}
}
}
return cachedValue;
}
私人函數value()返回儲存格的值。如果cacheIsDirty為true,我們必須重新計算它的值。
如果公式以一個單引號開始(如,”’12345”),單引號佔據位置0,而值則是公位置1到最後的字串。
如果公式以一個等號(‘=’)開始,我們抽取從位置1開始的字串並刪除其中的空格。然後我們調用evalExpression()計算這個運算式的值。Pos參數以引用形式傳遞。它表示解析開始字元的位置。在完成evalExpression()調用後,如果解析是成功的,則在pos 處的字元應該是我們添加的QChar::null字元。如果解析在結束前失敗,我們把cachedValue設為無效的。
如果公式即不以單引號開始,也不以等號開始,我們試著使用toDouble()把它轉換為一個浮點數值。如果轉換成功,我們把cachedValue設定為此結果。否則我們把cachedValue設定為公式字串。例如,“1.50”導致toDouble()設定ok為true並返回1.5,而”World population“會導致toDouble()把ok設為false並返回0.0。
通過給toDouble ()一個指向bool型的指標,我們就能表示數字值0.0的字串轉換和轉換錯誤(0.0同樣被返回但該bool值被設為false)。有時轉換錯誤時返回0正是我們希望的,這種情況下我們就不需要傳入一個指向bool型的指標。由於 效能和可移植性原因,Qt從不使用C++的異常來報告失敗。但在Qt程式中這並不會阻止你使用他們,假如你的編譯器支援他們的話。
Value()函數被聲明為const類型。我們必須把cachedValue和cacheIsValid聲明為mutable變數,這樣編譯器才能允許我們在常量型函數中修改他們。可能把value()改為非常量類型並去掉mutable關鍵字非常誘人,但這樣就不能通過編譯,因為我們從常量函數data()中調用了value()。
現在我們已經完成了試算表程式的公式解析部分。本節其餘部分包括evalExpression()和兩個協助函數evalTerm() 和 evalFactor()。因為這些代碼與GUI編程無關,你可以放心地跳過它並在第5章繼續閱讀。
evalExpression()函數返回試算表運算式的值。運算式被定義為一個或者多個由’+’或’+--‘(是不是應該為’-‘)操作符分隔的項。這些項被定義為一個或者多個由’*’或者’/’操作符分隔的因子。通過把運算式分解為項,把項分解為因子,我們保證操作符的正確優先順序。
例如,"2*C5+D6"是一個運算式,"2*C5" 是它的第一個項, "D6" 是它的第二個項。項"2*C5"有第一個因子"2"和第二個因子"C5",而項"D6"只由一個因子"D6"組成。因子可以是一個資料("2"),一個座標("C5"),或者一個括弧中的運算式,該運算式前還可放單個減號。
圖4.10定義了試算表的句法。對文法中的每個符號(運算式,項和因子),都有一個相應的成員函數用於解析它們並且這些函數的結構與文法密切相差。寫成這樣的分析器被叫做向下遞迴分析器。
圖4.10 試算表的句法圖
讓我們從分析運算式的函數evalExpression()開始吧:
QVariant Cell::evalExpression(const QString &str, int &pos) const
{
QVariant result = evalTerm(str, pos);
while (str[pos] != QChar::Null) {
QChar op = str[pos];
if (op != '+' && op != '-')
return result;
++pos;
QVariant term = evalTerm(str, pos);
if (result.type() == QVariant::Double
&& term.type() == QVariant::Double) {
if (op == '+') {
result = result.toDouble() + term.toDouble();
} else {
result = result.toDouble() - term.toDouble();
}
} else {
result = Invalid;
}
}
return result;
}
首先,我們調用evalTerm()來擷取第一個項的值。如果接下來的字元為’+’或者’+--‘,我們繼續調用evalTerm()。否則,該運算式是由單個項組成的,那麼我們就返回它的值作為整個運算式的值。在我們有了前兩個項的值後,我們計算該操作的結果,這依賴於當前的操作符。如果兩個項都是double類型的,我們的計算結果也是double類型的。否則,我們把結果設定為無效的。
我們像這樣一直繼續下去直到沒有更多的項。這工作的很正常,因為加減是左結合的。這就是說"1-2-3" 意味著 "(1-2)-3", 而不是 "1-(2-3)"。
QVariant Cell::evalTerm(const QString &str, int &pos) const
{
QVariant result = evalFactor(str, pos);
while (str[pos] != QChar::Null) {
QChar op = str[pos];
if (op != '*' && op != '/')
return result;
++pos;
QVariant factor = evalFactor(str, pos);
if (result.type() == QVariant::Double
&& factor.type() == QVariant::Double) {
if (op == '*') {
result = result.toDouble() * factor.toDouble();
} else {
if (factor.toDouble() == 0.0) {
result = Invalid;
} else {
result = result.toDouble() / factor.toDouble();
}
}
} else {
result = Invalid;
}
}
return result;
}
evalTerm()函數與evalExpression()非常相似,但它處理的是乘法和除法。在evalTerm()中僅有的一個比較巧妙的是我們要避免除0問題,因為在某些處理器上這會產生一個錯誤。由於圓整錯誤,浮點數的相等比較是非常不明智的,但與0.0進行相等比較是來防止除0錯誤是安全的。
QVariant Cell::evalFactor(const QString &str, int &pos) const
{
QVariant result;
bool negative = false;
if (str[pos] == '-') {
negative = true;
++pos;
}
if (str[pos] == '(') {
++pos;
result = evalExpression(str, pos);
if (str[pos] != ')')
result = Invalid;
++pos;
} else {
QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}");
QString token;
while (str[pos].isLetterOrNumber() || str[pos] == '.') {
token += str[pos];
++pos;
}
if (regExp.exactMatch(token)) {
int column = token[0].toUpper().unicode() - 'A';
int row = token.mid(1).toInt() - 1;
Cell *c = static_cast<Cell *>(
tableWidget()->item(row, column));
if (c) {
result = c->value();
} else {
result = 0.0;
}
} else {
bool ok;
result = token.toDouble(&ok);
if (!ok)
result = Invalid;
}
}
if (negative) {
if (result.type() == QVariant::Double) {
result = -result.toDouble();
} else {
result = Invalid;
}
}
return result;
}
evalFactor()函數比evalExpression() 和evalTerm().稍微複雜一點。我們先要注意因子是否為負。然後看一下它是否以一個開括弧開始。如果是,我們通過調用evalExpression()把括弧中的內容當作一個運算式來計算。在分析一個被括起來的運算式的時候,evalExpression()再調用evalTerm(),它又會調用eval-Factor(),而它又會再調用evalExpression()。這就是分析器中遞迴指定。
如果因子不是一個嵌套的運算式,我們抽取下一個序列,它應該是個儲存格座標或者是個數字。如果這個序列匹配相應QRegExp,我們把它當作一個儲存格參照並以給定的座標調用它的value()函數。這個儲存格可以是試算表中的任何一個,並且它還可以依賴於其他的儲存格。依賴不是問題。他們將會簡單的觸發更多的value()調用和(對“髒“儲存格)更多的分析直到所有的附屬單元都被求值。如果該序列不是一個儲存格座標,我們把它當作一個數字。
如果儲存格A1包含公式"=A1"會發生什麼呢?或者如果儲存格A1包含"=A2"並且儲存格A2又包含"=A1"又會發生什麼呢。儘管我們沒有編寫任何特別的代碼來檢測循環相依性,分析器通過返回一個無效的QVariant值優美地處理了這些情況。這能夠工作的很好,因為在我們調用evalExpression()之前已經把cacheIsDirty設定為false,並把cachedValue在value()中設定為無效的了。如果evalExpression()遞迴地對同一個儲存格調用value(),它會立即返回無效值,並且整個運算式的值的計算結果也是無效的。
我們現在已經完成了公式分析器。它應該能通過擴充因子的定義講法來簡單地擴充為處理預定義的試算表程式函數,像sum()" 和"avg()"。另一個簡單的擴充方式是實現字串的'+'操作符(作為串連用)。而這並不需要修改因子定義文法。