Lekce 6 - Jednoduchá kalkulačka v Qt a C++ - Model
V minulé lekci, Jednoduchá kalkulačka v Qt a C++ - Layout, jsme si slíbili, že si k naší kalkulačce vytvoříme business logiku - model a přidáme nějakou funkčnost, aby se z návrhu formuláře kalkulačky opravdu stala funkční kalkulačkou.
Model
V modelu se bude nacházet velmi "jednoduchá" logika pro základní funkce
kalkulačky. V podstatě potřebujeme jen předat nějaká data - v tomto
případě dva textové řetězce, typ operace a druh aktuální číselné
soustavy. Řetězce je třeba dále převést na celá čísla a testovat, zda
uživatel nezadal nějaký nesmysl - tedy učinit program "relativně
blbuvzdorný". I když podle zákonů pana Murphyho to je asi nemožné Nakonec to chce nějakou metodu,
která nám vrátí výsledek dané operace.
Naše okno, tedy "controller", jen předá data a model se postará o všechny kontroly a výsledek předá zpět, pokud o to bude požádán.
Do projektu si přidáme novou třídu, např. Model
, odvozenou
od QObject
.
Zděděním QObject
získáme možnost používat sloty a
signály. Tvůrci Qt tento způsob komunikace doporučují a
používají ve svých modelech, obvykle odvozených od třídy QAbstractItemModem
.
Myslím, že to už by byl kanón na vrabce a jak pravil A. Einstein -
Udělej to tak jednoduše, jak to jen jde. Ne víc ani míň.
(parafráze)
Tedy pro předání dat použijeme settery, při chybách budeme posílat signály a výsledek předáme zpět "getterem".
Přidání třídy do projektu
Pravým tlačítkem myši klikneme do Qt průzkumníku souborů projektu a z
kontextového menu zvolíme přidat nový. Vybereme soubor
C++
a typ C++ Class
:

Soubor pojmenujeme Model
, nastavíme Base class na
QObject
a zaškrtneme možnost Include QObject:

model.h
Hlavičkový soubor nového modelu bude mít následující kód, který si hned popíšeme:
#ifndef MODEL_H #define MODEL_H #include <QObject> #include <QString> class Model : public QObject { Q_OBJECT public: explicit Model(QObject *parent = nullptr); static const int OP_PLUS = 0x00; static const int OP_MINUS = 0x01; static const int OP_TIMES = 0x02; static const int OP_DIVIDE = 0x03; static const int SYS_HEX = 0x04; static const int SYS_DEC = 0x05; static const int SYS_OCT = 0x06; static const int SYS_BIN = 0x07; static const int OP_LEFT = 0x08; static const int OP_RIGHT = 0x09; private: int opLeft; int opRight; int operation; int numBasis; bool isLeftOk; bool isRightOk; public: void setLeft(QString value); void setRight(QString value); void setOperator(int operation); void setNumBasis(int numBasis); int calculate(); private: int numberTest(QString number, bool *isOk); signals: void operandFalse(int operand); void zeroDivide(); public slots: }; #endif // MODEL_H
Co jsme to tedy vlastně stvořili v hlavičkovém souboru?
- Zcela na začátek bylo třeba vložit hlavičkový soubor knihovny
QString
- Přidali jsme několik statických konstant, které zastupují jednotlivé operace. Alternativně by šlo také použít výčtového typu. Kdybychom použili konstanty (vlastně makra) druhu `#define C_NAME (VALUE) `, tak ty pak nemají přímou kontrolu nad typem.
- Další sada konstant určuje číselnou soustavu.
- Následně jsme deklarovali několik privátních atributů, které
zastupují operandy (vstupní čísla) a operaci:
int leftOperand...
. Též jsme definovali aktuálně používanou soustavu. Velmi důležité jsou logické atributybool isLeftOk
..., které nám budou detekovat chyby při zadání. Sice vyvolají jen neurčitou chybu, ale budeme vědět, že něco bylo špatně. - Poté bylo třeba uveřejnit několik set/get metod k nastavení atributů
tříd:
void setLeft(QString value)
...,int getLeft()
... - Deklarujeme metodu, která dovede vrátit výsledek operace:
int calculate()
. - Abychom zbytečně nepsali stejný kód na test správnosti vstupu,
napíšeme si jednu funkci navíc:
int numberTest(QString number, bool *isOk);
, kde první parametr předá uživatelský vstup a druhý mám umožní vrátit případnou chybu zadání.
Uživatelské signály
Přesto, že tato část článku patří k hlavičce modelu, je její důležitost natolik významná, že si zasluhuje svůj nadpis.
Zatím jsem používali jen signály, které již někdo předefinoval. Ovšem je dost dobré vědět, že si je můžeme napsat sami a dokonce v nich předávat argumenty různých typů.
V hlavičkovém souboru nalezneme signály v bloku uvozeném klíčovým
slovem signals
, což je nyní synonymum pro public
(dříve bylo definováno jako #define signals protected
).
Jinak jejich deklarace má stejný tvar jako běžná členská funkce, ale
nemusí být v souboru *.cpp
definována.
My budeme posílat signál při chybném vstupu
void operandFalse(int operand)
, kde argument určí pomocí
konstanty, které číslo bylo špatně. Signál void zeroDivide()
bude varovat při dělení nulou, což je v matematice nedefinovaný výsledek.
Prostě se to nedělá
model.cpp
Přejděme do implementačního souboru. Zde máme tedy výkonnou část modelu naší kalkulačky.
Konstruktor
Konstruktor by nám měl preventivně nastavit atributy na výchozí hodnoty, aby nedocházelo k nečekaným chybám. Výchozí situace po vytvoření modelu bude:
- Žádný operand (číslo) nebyl zadán.
- Operace je defaultně sčítání.
- Číselná soustava je dekadická.
- Nedošlo k žádné chybě.
Kód konstruktoru je následující:
Model::Model(QObject *parent) : QObject(parent) { opLeft = 0; opRight = 0; operation = Model::OP_PLUS; numBasis = Model::SYS_DEC; isLeftOk = false; isRightOk = false; }
Settery
Settery operandů využijí členských funkcí třídy QString
a převedou řetězec na celé číslo. Zároveň proběhne test správnosti
vstupu metodou numberTest()
, kterou dodáme později. Výsledek
testu si uložíme pro pozdější použití:
void Model::setLeft(QString value) { opLeft = numberTest(value, &isLeftOk); } void Model::setRight(QString value) { opRight = numberTest(value, &isRightOk); }
Následně si nastavíme typ operace a základ číselné soustavy. Argumentem bude jedna z konstant modelu. Metody jsou velmi triviální a podtržnítko u názvu argumentu je jen snadná finta, jak používat stejné názvy a vyhnout se ukazatelům (pointerům):
void Model::setOperator(int _operation) { operation = _operation; } void Model::setNumBasis(int _numBasis) { numBasis = _numBasis; }
Test vstupů
Podívejme se na funkci pro test vstupu. Předáme jí řetězec, který by
měl představovat číslo, a referenci na atribut výsledku testu. Podle
aktuální číselné soustavy zajistíme převod textu na číslo. Např. pro
šestnáctkovou soustavu řádek retVal = number.toInt(isOk, 16)
znamená: text převeď na celé číslo o základu 16
a pokud vše
je v pořádku, do isOk
ulož true
, jinak
false
.
Hodnotu potom metoda vrátí a to i v případě, že došlo k chybě (tehdy
vrací 0
):
int Model::numberTest(QString number, bool *isOk) { int retVal = 0; switch (numBasis) { case SYS_HEX: retVal = number.toInt(isOk, 16); break; case SYS_DEC: retVal = number.toInt(isOk, 10); break; case SYS_OCT: retVal = number.toInt(isOk, 8); break; case SYS_BIN: retVal = number.toInt(isOk, 2); break; } return retVal; }
Výpočet
Pro samotný výpočet použijeme jednoduchou funkci obsahující jeden
přepínač switch
, který podle typu operace provede požadovanou
akci:
int Model::calculate() { if (!isRightOk) { emit operandFalse(OP_RIGHT); return 0; } if (!isLeftOk) { emit operandFalse(OP_LEFT); return 0; } if (opRight == 0 && operation == OP_DIVIDE) { emit zeroDivide(); return 0; } switch (operation) { case OP_PLUS: return opLeft + opRight; break; case OP_MINUS: return opLeft - opRight; break; case OP_TIMES: return opLeft * opRight; break; case OP_DIVIDE: return opLeft / opRight; break; default: return 0; } }
Nejdříve provedeme několik testů. Předně pokud je chybný některý z
operandů, odešleme signál pomocí příkazu emit
, který z nich
to byl. Pro případ, že dojde k pokusu dělení nulou, opět pošleme signál.
Při chybě automaticky vracíme výsledek 0
:
V příští lekci, Jednoduchá kalkulačka v Qt a C++ - Dokončení, provedeme úpravy hlavního okna a tím kalkulačku zprovozníme.