11. Adatmentés és –visszatöltés modell használatakor
11.2. A FileOperator osztály reinkarnációja
A fileOperator objektum még sehol nincs persze, sőt, az osztálya sem, azaz, ha ki
szeretnénk próbálni, hogy eddig minden rendben-e, akkor a tagfüggvény törzsének utolsó sorát alakítsuk megjegyzéssé.
11.2. A FileOperator osztály reinkarnációja
Végre hozzákezdünk ahhoz, ami a fejezet valódi témája. Adjunk a projekthez új osztály, FileOperator néven, és ezúttal ne a QObject, hanem a QWidget legyen az ősosztály, mert, ahogy arra bizonyára emlékszünk, a fájl-választó ablakok ősének mindenképp QWidget osztályúnak kell lennie. Az osztályt a főablakból fogjuk példányosítani, és a konstruktorának egyik paramétereként adjuk át a modellünk mutatóját. Azaz az osztály konstruktora ilyesmi lesz:
FileOperator::FileOperator(QStandardItemModel *toDoListModel, QWidget
*parent) :
QWidget(parent)
{ model = toDoListModel;
timer = new QTimer(this);
}
A timer objektum természetesen az automatikus mentés miatt kell majd. Apropó,
automatikus mentés: írjuk már meg gyorsan azt a fránya FileOperator::adjustAutoSave() függvényt, aminek a hívását az imént szégyenszemre megjegyzéssé kellett alakítanunk. A tagfüggvénynek nem lesz más dolga, mint a beállítások beolvasása, és a timer objektum megfelelő felparaméterezése. Az automatikus mentés kivitelezése egy másik tagfüggvény feladata lesz majd, ami ráadásul nem csak tagfüggvény, de slot is lesz, így hozzá tudjuk kötni a timer objektum ki(el?)süléséhez. Íme a tagfüggvényünk:
Qt strandkönyv ǀ 11. Adatmentés és -visszatöltés... ǀ
void FileOperator::adjustAutoSave()
{ if(settings.value("Files/AutoSaveEnabled", "false").toBool()) timer‑>start(settings.value("Files/AutoSaveMinutes").toInt()
*60000);
elsetimer‑>stop();
}
Végezzük el a FileOperator osztály példányosítását a főablakban. Olyan helyen kell megejtenünk, ahol már van modell, de még a restoreApplicationState() tagfüggvény hívása előtt, mert különben nem indul be az automatikus mentés.
Ha eddig minden klappol, nekikezdünk a mentést végző tagfüggvény elkészítésének. Ezúttal a fájlnevet paraméterként kapja meg. A fájl megnyitása és használatba vétele a már megismert módon történik, és a modell mentése közben figyelünk arra, hogy üres teendőjű feladatot – azaz az olyan sorokat, amelyek első oszlopa üres – nem mentünk. Sikeres mentés esetén a beállításokban tároljuk a fájlnevet. A hibaüzeneteket ezúttal külön tagfüggvényben helyezzük el, hiszen mentéskor és megnyitáskor nagyon hasonlóan magyaráztuk el a felhasználónak, hogy baj történt. A hibajelzést végző tagfüggvény paraméterként kapja meg, hogy mikor is (mentéskor vagy megnyitáskor) történt baleset. A mentést végző privát tagfüggvény végül ilyenre sikeredett:
bool FileOperator::performFileSaveOperation(QString fileName) { QFile file(fileName);
bool success = false;
if (file.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) { QTextStream out(&file);
for(int i = 0; i < model‑>rowCount(); i++){
QString job = model‑>index(i,0).data().toString().
simplified();
if(!job.isEmpty())
out << job << "|" << model‑>index(i,1).data().
toString() << endl;
}
if(!file.error()) success = true;
}if(success){
settings.setValue("Files/LastFileName", fileName);
}else{
fileOperationErrorMessage(tr("Save"));
}return success; //can be "false" too }
Gyorsan megírjuk a fileOperationErrorMessage() privát tagfüggvényt is:
Qt strandkönyv ǀ 11. Adatmentés és -visszatöltés... ǀ
oid FileOperator::fileOperationErrorMessage(QString operation) { QMessageBox mb;
mb.setIcon(QMessageBox::Critical);
mb.setText(operation + " " + tr("not succeeded."));
mb.setInformativeText(tr("Try and do something wise."));
mb.exec();
}
Az előző alkalommal úgy gondolkodtunk, hogy a felhasználó alighanem az előző fájl mellé mentené az újat is, ha meg nem volt előző fájl, akkor valahova a saját cókmókja közé. Ráadásul azt is feltételeztük, hogy hasonló gondolatmenet fut át az agyán a mentett fájl megnyitásakor is: hátha az előző mellől nyitna meg másikat, mert mondjuk az imént mellékattintott. Így aztán mindkét alkalommal ilyen megfontolásból kiindulva állítottuk elő azt az elérési útvonalat, amelyet felhasználva megnyitottuk a fájlválasztó párbeszédablakot. Ha viszont kétszer is kellett, ugye van értelme külön tagfüggvénybe pakolni a fejtegetést? A nagyrabecsült Olvasó gesztusát bólintásként értelmezzük, és már itt is az alapértelmezett elérési útvonalat javasló privát tagfüggvény:
QString FileOperator::suggestDefaultPath() { QString path = QDir::homePath();
QString lastFileName = settings.value("Files/LastFileName", "").
toString();
if(lastFileName != ""){
QFileInfo info(lastFileName);
path = info.absolutePath();
}return path;
}
Minden készen áll ahhoz, hogy megírjuk azt a tagfüggvényt is, amelyik előbb beszerzi a szükséges fájlnevet, és aztán ennek ismeretében végezteti el a mentést. A múltkor jól bevált módszer szerint, ha a felhasználó nem kegyeskedett megadni a fájl kiterjesztését, akkor majd mi megadjuk. Íme:
bool FileOperator::fileSave()
{ QString newFileName = QFileDialog::getSaveFileName(
this,
tr("Save as..."), suggestDefaultPath(),
tr("ToDoList files (*.tdolst);;text files (*.txt);;all files (*)")
);
if(!newFileName.isEmpty()){
QFileInfo info(newFileName);
QString ext = info.suffix();
if(ext != "tdolst" && ext != "txt") newFileName += ".tdolst";
return performFileSaveOperation(newFileName);
}return false; }
Qt strandkönyv ǀ 11. Adatmentés és -visszatöltés... ǀ
Törjük kicsit a fejünket. Ha a felhasználó Ctrl+S billentyűkombinációt nyom, és van fájlnév (ki tudjuk olvasni a beállításokból), akkor csuklás nélkül hívjuk a performFileSaveOpration() tagfüggvényt. Ha nincs fájlné, akkor a fileSave() tagfüggvényt hívjuk, megtudjuk a fájlnevet és ennek ismeretében megint csak a
performFileSaveOpration()-ra lőcsöljük a melót. Ha a felhasználó a Save as... menüpontot választaná, akkor tekintet nélkül arra, hogy van-e fájlnevünk vagy nincs, mindenképp újat kell kérnünk, hiszen a Save as... menüpontnak pont ez a lényege. Végiggondoltunk minden lehetőséget? Talán. Akkor megírhatjuk a két QAction osztályú objektum (mármint a Save és a Save as...) slot-ját. Hol volna érdemes? A főablakban egyszerűbb, a FileOperator osztályban elegánsabb. Mi elegánsak leszünk, és publikus slot-ként a FileOperator-ban helyezzük el a két következő szösszenetet. A függvények elnevezése nagyon hasonló ahhoz, amit automatikus létrehozásukkal kapnánk, de szándékosan nem teljesen ugyanolyan, ugyanis a moc (a Meta Object Compiler) azonnal hőbörögne, hogy látja ő, hogy itt a slot, de hol a signal? (Mi persze tudjuk, hogy egy másik osztályban, de a moc-nak nincs ám annyi esze, hogy megkérdezze tőlünk.) És akkor most mihez kössön, meg micsodát? Mi meg nem bírjuk, ha valaki hőbörög, így inkább picit változtatunk a neveken.
void FileOperator::onActionSaveTriggered()
{ QString fileName = settings.value("Files/LastFileName", "").
toString();
if(!fileName.isEmpty())
performFileSaveOperation(fileName);
elsefileSave();
}
void FileOperator::onActionSaveAsTriggered() { fileSave();
}
Már csak az a dolgunk velük, hogy a főablakban kiadjuk a megfelelő connect utasításokat:
connect(ui‑>actionSave, SIGNAL(triggered()), fileOperator, SLOT(onActionSaveTriggered()));
connect(ui‑>actionSave_as, SIGNAL(triggered()), fileOperator, SLOT(onActionSaveAsTriggered()));
Természetesen nincs sok teteje a dolognak, ha még a fileOperator példányosítása elé írjuk a sorokat, de a példányosítást követően azonnal jöhetnek. Elgondolkodhatunk azon is, hogy közvetlenül a fileSave() tagfüggvényt tesszük slot-tá, és azt kötjük a Save as... menühöz. Nem sok ellenérv van ellene, talán az, hogy egyesek számára a jelenlegi forma áttekinthetőbb. Ez viszont egy igen-igen fontos érv, mindenkit meggyőzhet, kivéve esetleg azokat, akik szerint a másik forma áttekinthetőbb. A hörcsög még nem nyilvánította ki véleményét az ügyben, úgyhogy a meccs még nem lefutott.
Megnyitás következik, pedig ez a könyv nem is a sakkról szól. A megnyitást végző
tagfüggvények természetesen nagyon hasonlítanak majd a mentést végzőkhöz, és ugyanúgy párban járnak. Itt következik mind a kettő:
Qt strandkönyv ǀ 11. Adatmentés és -visszatöltés... ǀ
bool FileOperator::fileOpen()
{ QString newFileName = QFileDialog::getOpenFileName(
this,
tr("Open file..."), suggestDefaultPath(),
tr("ToDoList files (*.tdolst);;text files(*.txt);;all files (*)")
);
if(!newFileName.isEmpty())
return performFileOpenOperation(newFileName);
return false; }
bool FileOperator::performFileOpenOperation(QString fileName) { model‑>clear();
model‑>setHorizontalHeaderLabels(QStringList() << tr("job") <<
tr("category"));
QFile file(fileName);
bool success = false;
if (file.open(QIODevice::ReadOnly | QIODevice::Text)){
QTextStream in(&file);
while (!in.atEnd()){
QStringList sl = in.readLine().split("|");
QStandardItem *item = new QStandardItem(sl.at(0));
model‑>setItem(model‑>rowCount(), 0, item);
item = new QStandardItem(sl.at(1));
model‑>setItem(model‑>rowCount()‑1, 1, item);
}
if(!file.error()) success = true;
}if(success){
settings.setValue("Files/LastFileName", fileName);
QList<QStandardItem* > newRow;
model‑>appendRow(newRow);
}else{
fileOperationErrorMessage(tr("Open"));
}return success; //can be "false" too }
A performFileOpenOperation() tagfüggvényt a fileOpen() tagfüggvény hívja, és még ki? Mert, ha senki, akkor minek külön tagfüggvény? Nos, a másik hely, ahonnan hívni fogják, a program indításakori automatikus fájlbetöltés lesz. Úgyhogy van létjogosultsága a dolognak.
Viszont a fileOpen() tagfüggvény hívása körül van még egy icuri kis probléma.
Alighanem eszünkbe ötlik még, hogy a kényelem mián ModelManager osztály berkeiben megírtuk azt a slot-ot, amelyik az utolsó sor első oszlopában lévő elem megváltozásakor azonnal létrehoz egy üres sort. Hamarosan bajba is keveredtünk vele, olyankor is elő-előkerült egy-egy üres sor, amikor az legkevésbé sem szolgálta óhajainkat. Meg is írtuk a nagy ki-be kapcsolgató connectItemChangedSignalToSlot() tagfüggvényt, amelyről már akkor előrevetítettük,
Qt strandkönyv ǀ 11. Adatmentés és -visszatöltés... ǀ
hogy a fájlbetöltéskor még jó hasznát vesszük majd. Nos, ez az idő – mármint a fájlbetöltés, meg a probléma előjövetele – itt van. Szeretnénk hívni a tagfüggvényt, de ugye a fileOperator objektum nem látja. Kénytelenek leszünk szégyenszemre ezt az egy slot-ot a főablakban megvalósítani? Azt már nem!
Úgy módosítjuk a FileOperator osztály konstruktorát, hogy ne csak a modell, hanem az egész modelManager objektum mutatóját vegye át. Íme:
FileOperator::FileOperator(ModelManager *modelManager, QWidget
*parent) :
QWidget(parent)
{ this‑>modelManager = modelManager;
model = modelManager‑>toDoListModel;
timer = new QTimer(this);
}
A törzs első sora azt teszi lehetővé, hogy a FileOperator osztályon belül is
modelManager néven hivatkozhassunk az objektumunkra. Persze át kell alakítanunk a fileOperator objektumot példányosító sort is a főablak forrásában, sőt, a
ModelManager::connectItemChangedSignalToSlot() tagfüggvényt is publikussá kell tennünk, merthogy eddig privát volt, a szentem. Végre megírható a megnyitást végző slot:
void FileOperator::onActionOpenTriggered()
{ modelManager‑>connectItemChangedSignalToSlot(false);
fileOpen();
modelManager‑>connectItemChangedSignalToSlot(true);
}
És, igen, most sem hagyjuk el a connect utasítást a főablakból:
connect(ui‑>actionOpen, SIGNAL(triggered()),
fileOperator, SLOT(onActionOpenTriggered()));
Akkor ez is megvan. Bueno.