Two main goals
- Discover the possibilities of Qt and KDE Frameworks
- Practice TDD on the application
Approach
- Setup the project infrastructure
- Discover the tools while creating the GUI
- Apply TDD on the application model
Project Setup
This first part will help you discover the base tools used to build a KDE application
Exercise 1: CMake and first source file
- Create a
main.cpp
file containing the main()
function, only displaying a debug message using qDebug()
- Create a
CMakeLists.txt
file for this project named spreadsheet
- Create the build directory, and compile the project using CMake
**Hint**: For now, it is in fact easier to work *outside* of QtCreator
Exercise 2: QApplication and first display
Create a QApplication and an empty QWidget as main window
Main Window Creation
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.
Exercise 3: Toward a standard window
- Add a class named
SpreadSheetMainWindow
which inherits from KXmlGuiWindow
, use it as
main window
- Put in place the following actions: New, Open, Save, SaveAs, Quit, Cut, Copy, Paste
- Add one slot by action in
SpreadSheetMainWindow
- Make sure the slots are called when the actions are triggered by the user (implement only
the Quit behavior, other actions just show a debug message)
**Hint**: Use the KActionCollection and the method setupGUI() provided by KXmlGuiWindow.
Exercise 4: Spreadsheet interface
- Add a
QTableWidget
as the window central widget (make it a 100 by 100 table)
- Add the actions Cut, Copy and Paste in the
QTableWidget
context menu
- Add two
QLabel
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*`
Basic Features
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.
Exercise 5: Canned data
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 |
Exercise 6: Display the location
- Write a test for a
static QString locationFromIndex(int row, int column)
method which
converts (0, 0) in A1, (0, 1) in B1, etc.
- Implement the method while increasing the amount of data for the test
- Implement the necessary to update the status bar depending on the current cell using
locationFromIndex()
Exercise 7: Data manipulation
- Implement Copy, Cut and Paste for one cell at a time
- Extend it for contiguous cell zones (requires to force the behavior of selections in
the view), pasting starts at the current cell
Spreadsheet using Model / View
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.
Exercise 8: Setting up Model / View
- Implement a test for a new class
SpreadSheetModel
inheriting from QAbtractTableModel
- Test must verify that at creation the model is an empty matrix of 216
by 216 cells
- Implement
SpreadSheetModel
itself
- Replace the
QTableWidget
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)
Exercise 9.1: Lines and columns labelling
- Write unit tests for
static QString rowNameFromIndex(int)
in SpreadSheetModel
- We verify that we obtain
"1"
for 0
, "2"
for 1
, "10"
for 9
, etc.
- Implement
rowNameFromIndex()
- Write unit tests for
static QString columnNameFromIndex(int)
in SpreadSheetModel
- We verify that we obtain
"A"
for 0
, "B"
for 1
, "Z"
for 25, "AA"
for 26
, "BA"
for 52
, etc.
- Implement
columnNameFromIndex()
- Reimplement
headerData()
in SpreadSheeModel
and use the newly implemented static methods…
hey! the tests first!
Exercise 9.2: First refactoring
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
Make the Spreadsheet Usable
In this section, the questions will only be the unit tests that the model must pass.
Exercise 10: Store text
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(""));
}
Exercise 11: Verify that many cells exist
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"));
}
Exercise 12: Store numeric data
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
Exercise 13: Access to the original data for editing
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"));
}
Exercise 15: Error management
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"));
}
Exercise 16: Substract, divide, negate
The client notices that he forgot to ask for the support of the substract, divide and negate operators…
You now have to add them:
- Add tests for substract and then implement
- Add tests for divide and then implement
- Add tests for negate and then implement
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.
Exercise 17: Verify that a reference works
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"));
}
Exercise 18: Verify that changes propagate
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"));
}
Exercise 20: Verify that changes propagate on several levels
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"));
}
Exercise 22: Verify that the circular references give an error
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"));
}
Finalizing the Application
Exercise 23: GUI cleanup
- If not done yet, remove the initialization of the table which was implemented in exercise 5
- If necessary, modify Cut, Copy and Paste so that they recopy formulas and not results
- Add a test to ensure that the status bar displays the formula of the current cell, not its value
- Add the code necessary to make the test pass
**Hint**: Use the parent/child relationship of `QObject` and its name property
To go further…
Some aspects are ignored in this lab, and it would be necessary to see them to complete the
application:
- saving, for this it’d be required to take a look at
QIODevice
, KIO
and QFileDialog
- some small display issues tied to the lack of events emitted by the model, then it’d be
necessary to take care of the
QAbstractItemModel
signals like dataChanged()
- function support in formulas
- and probably more…
- Application is 402 lines of code in two classes only
- Tests are 342 lines of code and cover the whole model as well as parts of the GUI (could cover even more)
A very decent spreadsheet for the given effort!
Lessons Learned
- Safe progress using baby steps, one constraint at a time
- TDD ensures we avoid regressions now and in the future
- First taste of Qt and KDE Frameworks
Hope you enjoyed it
Have fun!