Kevin Ottens
This first part will help you discover the base tools used to build a KDE application
main.cpp
file containing the main()
function, only displaying a debug message using qDebug()
CMakeLists.txt
file for this project named spreadsheet
Hint: For now, it is in fact easier to work outside of QtCreator
Create a QApplication and an empty QWidget as main window
Now, the basics being in place, we’ll build our GUI. This way, you’ll be able to test the classes used to build a standard window.
SpreadSheetMainWindow
which inherits from KXmlGuiWindow
, use it as
main windowSpreadSheetMainWindow
Hint: Use the KActionCollection and the method setupGUI() provided by KXmlGuiWindow.
QTableWidget
as the window central widget (make it a 100 by 100 table)QTableWidget
context menuQLabel
s to SpreadSheetMainWindow
’s status bar (they will be used to indicate the
coordinates of the current cell, and the formula contained)
Important: In SpreadSheetMainWindow
, the pointer to the QTableWidget
will be of type
QTableView*
So far, we have only put building blocks together. Let’s put in place the basic GUI features now. Our first automated unit test will appear.
Populate the table with the following data:
William | Adama | 1947-02-24 |
Saul | Tigh | 1949-03-22 |
Lee | Adama | 1973-04-03 |
Kara | Thrace | 1980-04-08 |
Laura | Roslin | 1952-04-28 |
Gaius | Baltar | 1971-06-04 |
static QString locationFromIndex(int row, int column)
method which
converts (0, 0) in A1, (0, 1) in B1, etc.locationFromIndex()
We will now create our own data model for the spreadsheet. Most features will be implemented in the model. The TDD approach will be fully applied.
SpreadSheetModel
inheriting from QAbtractTableModel
SpreadSheetModel
itselfQTableWidget
with a QTableView
, provide it an empty SpreadSheetModel
instance
Information: rowCount()
and columnCount()
must return 0
when the QModelIndex
given as parameter
is valid (see QAbstractItemModel
documentation)
static QString rowNameFromIndex(int)
in SpreadSheetModel
"1"
for 0
, "2"
for 1
, "10"
for 9
, etc.rowNameFromIndex()
static QString columnNameFromIndex(int)
in SpreadSheetModel
"A"
for 0
, "B"
for 1
, "Z"
for 25, "AA"
for 26
, "BA"
for 52
, etc.columnNameFromIndex()
headerData()
in SpreadSheeModel
and use the newly implemented static methods…
hey! the tests first!Modify locationFromIndex()
in SpreadSheetMainWindow
to eliminate the code duplication
Information: This latest modification should be done without modifying any test, this
is your first refactoring with the unit tests safety net
In this section, the questions will only be the unit tests that the model must pass.
void testThatTextIsStored()
{
SpreadSheetModel m;
QModelIndex index = m.index(21, 0);
m.setData(index, "A string");
QCOMPARE(m.data(index).toString(), QString("A string"));
m.setData(index, "A different string");
QCOMPARE(m.data(index).toString(), QString("A different string"));
m.setData(index, "");
QCOMPARE(m.data(index).toString(), QString(""));
}
void testThatManyCellsExist()
{
SpreadSheetModel m;
QModelIndex a = m.index(0, 0);
QModelIndex b = m.index(23, 26);
QModelIndex c = m.index(699, 900);
m.setData(a, "One");
m.setData(b, "Two");
m.setData(c, "Three");
QCOMPARE(m.data(a).toString(), QString("One"));
QCOMPARE(m.data(b).toString(), QString("Two"));
QCOMPARE(m.data(c).toString(), QString("Three"));
m.setData(a, "Four");
QCOMPARE(m.data(a).toString(), QString("Four"));
QCOMPARE(m.data(b).toString(), QString("Two"));
QCOMPARE(m.data(c).toString(), QString("Three"));
}
void testNumericCells()
{
SpreadSheetModel m;
QModelIndex index = m.index(0, 20);
m.setData(index, "X99"); // String
QCOMPARE(m.data(index).toString(), QString("X99"));
m.setData(index, "14"); // Number
QCOMPARE(m.data(index).toString(), QString("14"));
QCOMPARE(m.data(index).toInt(), 14);
m.setData(index, "99 X"); // Whole string must be numeric
QCOMPARE(m.data(index).toString(), QString("99 X"));
bool ok;
m.data(index).toInt(&ok);
QVERIFY(!ok);
m.setData(index, " 1234 "); // Blanks ignored
QCOMPARE(m.data(index).toString(), QString("1234"));
QCOMPARE(m.data(index).toInt(), 1234);
m.setData(index, " "); // Just a blank
QCOMPARE(m.data(index).toString(), QString(" "));
}
Hint: Add the asserts one by one
void testAccessToLiteralForEditing()
{
SpreadSheetModel m;
QModelIndex index = m.index(0, 20);
m.setData(index, "Some string");
QCOMPARE(m.data(index, Qt::EditRole).toString(), QString("Some string"));
m.setData(index, " 1234 ");
QCOMPARE(m.data(index, Qt::EditRole).toString(), QString(" 1234 "));
m.setData(index, "=7");
QCOMPARE(m.data(index, Qt::EditRole).toString(), QString("=7"));
}
In this section, it is wise to add the asserts of each proposed test one by one or to separate them in several different tests, or also to add tests. It really depends on your confidence level about parsing.
void testFormulaBasics()
{
SpreadSheetModel m;
QModelIndex index = m.index(2, 10);
m.setData(index, " =7"); // note leading space, not a formula
QCOMPARE(m.data(index).toString(), QString("=7"));
QCOMPARE(m.data(index, Qt::EditRole).toString(), QString(" =7"));
m.setData(index, "=7"); // constant formula
QCOMPARE(m.data(index).toString(), QString("7"));
QCOMPARE(m.data(index, Qt::EditRole).toString(), QString("=7"));
// Adding intermediate tests here to guide you is fine
// Go ahead!
m.setData(index, "=(7)"); // parenthesis
QCOMPARE(m.data(index).toString(), QString("7"));
m.setData(index, "=(((10)))"); // more parenthesis
QCOMPARE(m.data(index).toString(), QString("10"));
m.setData(index, "=2*3*4"); // multiply
QCOMPARE(m.data(index).toString(), QString("24"));
m.setData(index, "=12+3+4"); // add
QCOMPARE(m.data(index).toString(), QString("19"));
m.setData(index, "=4+3*2"); // precedence
QCOMPARE(m.data(index).toString(), QString("10"));
m.setData(index, "=5*(4+3)*(((2+1)))"); // full expression
QCOMPARE(m.data(index).toString(), QString("105"));
}
void testFormulaErrors()
{
SpreadSheetModel m;
QModelIndex index = m.index(0, 0);
m.setData(index, "=5*");
QCOMPARE(m.data(index).toString(), QString("#Error"));
m.setData(index, "=((((5))");
QCOMPARE(m.data(index).toString(), QString("#Error"));
}
The client notices that he forgot to ask for the support of the substract, divide and negate operators…
You now have to add them:
This section will allow us to add the necessary features to really have a usable spreadsheet. Without the support for references, we’d only have a big calculator… Once again, it is wise to add intermediate tests if necessary.
void testThatReferenceWorks()
{
SpreadSheetModel m;
QModelIndex a = m.index(0, 0);
QModelIndex b = m.index(10, 20);
m.setData(a, "8");
m.setData(b, "=A1");
QCOMPARE(m.data(b).toString(), QString("8"));
}
void testThatChangesPropagate()
{
SpreadSheetModel m;
QModelIndex a = m.index(0, 0);
QModelIndex b = m.index(10, 20);
m.setData(a, "8");
m.setData(b, "=A1");
QCOMPARE(m.data(b).toString(), QString("8"));
m.setData(a, "9");
QCOMPARE(m.data(b).toString(), QString("9"));
}
void testThatFormulasRecalculate()
{
SpreadSheetModel m;
QModelIndex a = m.index(0, 0);
QModelIndex b = m.index(1, 0);
QModelIndex c = m.index(0, 1);
m.setData(a, "8");
m.setData(b, "3");
m.setData(c, "=A1*(A1-A2)+A2/3");
QCOMPARE(m.data(c).toString(), QString("41"));
m.setData(b, "6");
QCOMPARE(m.data(c).toString(), QString("18"));
}
void testThatDeepChangesPropagate()
{
SpreadSheetModel m;
QModelIndex a1 = m.index(0, 0);
QModelIndex a2 = m.index(1, 0);
QModelIndex a3 = m.index(2, 0);
QModelIndex a4 = m.index(3, 0);
m.setData(a1, "8");
m.setData(a2, "=A1");
m.setData(a3, "=A2");
m.setData(a4, "=A3");
QCOMPARE(m.data(a4).toString(), QString("8"));
m.setData(a2, "6");
QCOMPARE(m.data(a4).toString(), QString("6"));
}
void testThatFormulasWorkWithManyCells()
{
SpreadSheetModel m;
QModelIndex a1 = m.index(0, 0);
QModelIndex a2 = m.index(1, 0);
QModelIndex a3 = m.index(2, 0);
QModelIndex a4 = m.index(3, 0);
QModelIndex b1 = m.index(0, 1);
QModelIndex b2 = m.index(1, 1);
QModelIndex b3 = m.index(2, 1);
QModelIndex b4 = m.index(3, 1);
m.setData(a1, "10");
m.setData(a2, "=A1+B1");
m.setData(a3, "=A2+B2");
m.setData(a4, "=A3");
m.setData(b1, "7");
m.setData(b2, "=A2");
m.setData(b3, "=A3-A2");
m.setData(b4, "=A4+B3");
QCOMPARE(m.data(a4).toString(), QString("34"));
QCOMPARE(m.data(b4).toString(), QString("51"));
}
void testCircularReferences()
{
SpreadSheetModel m;
QModelIndex a1 = m.index(0, 0);
QModelIndex a2 = m.index(1, 0);
QModelIndex a3 = m.index(2, 0);
m.setData(a1, "=A1");
m.setData(a2, "=A1");
m.setData(a3, "=A2");
QCOMPARE(m.data(a1).toString(), QString("#Error"));
m.setData(a1, "=A3");
QCOMPARE(m.data(a1).toString(), QString("#Error"));
}
Hint: Use the parent/child relationship of QObject
and its name property
Some aspects are ignored in this lab, and it would be necessary to see them to complete the application:
QIODevice
, KIO
and QFileDialog
QAbstractItemModel
signals like dataChanged()
A very decent spreadsheet for the given effort!