Development/Tutorials/Saving and loading: Difference between revisions

From KDE TechBase
m (Remove 'under construction' signs since it's mostly complete)
(update to newer code variants)
(20 intermediate revisions by 12 users not shown)
Line 1: Line 1:
{{Template:I18n/Language Navigation Bar|Development/Tutorials/Saving_and_loading}}
 


{{TutorialBrowser|
{{TutorialBrowser|
Line 7: Line 7:
name=Loading and saving files|
name=Loading and saving files|


pre=[[Development/Tutorials/Using_KActions|Tutorial 3 - KActions]]|
pre=[[Development/Tutorials/Using_Actions|Tutorial 3 - Actions]]|


next=KCmdLineArgs (TODO [[User:milliams]])|  
next=[[Development/Tutorials/CommandLineArguments|Tutorial 5 - Using Command Line Arguments]]|  


reading=KIO::{{class|NetAccess}} {{qt|QFile}}
reading=[[Development/Tutorials/KIO Slaves/Using KIO Slaves in your Program|Tutorial: Using KIO Slaves in your Program]] KIO::{{class|NetAccess}} {{qt|QFile}}
}}
}}


Line 18: Line 18:
Now that we have a basic text editor interface, it's time to make it do something useful. At the most basic, a text editor needs to be able to load files from disc, save files that you've created/edited and create new files.
Now that we have a basic text editor interface, it's time to make it do something useful. At the most basic, a text editor needs to be able to load files from disc, save files that you've created/edited and create new files.


KDE provides a number of classes for working with files which make life a lot easier for developers. The KIO library allows you to easily access files through network-transparent protocols as well as providing standard file dialogs.
The KDE Frameworks provides a number of classes for working with files which make life a lot easier for developers. The KIO framework allows you to easily access files through network-transparent protocols. At the same time, Qt also provides  standard file dialogs for opening and saving files.


[[image:introtokdetutorial4.png|frame|center]]
[[image:tutorial4-kf5.png|frame|center]]


== The Code ==
== The Code ==


===main.cpp===
===main.cpp===
<code cppqt n>
<syntaxhighlight lang="cpp-qt">
#include <KApplication>
#include <cstdlib>
#include <QApplication>
#include <QCommandLineParser>
 
#include <KAboutData>
#include <KAboutData>
#include <KCmdLineArgs>
#include <KLocalizedString>
 
#include "mainwindow.h"
#include "mainwindow.h"
   
   
int main (int argc, char *argv[])
int main (int argc, char *argv[])
{
{
  KAboutData aboutData( "tutorial4", "tutorial4",
    QApplication app(argc, argv);
      ki18n("Tutorial 4"), "1.0",
   
      ki18n("A simple text area which can load and save."),
    KLocalizedString::setApplicationDomain("tutorial4");
      KAboutData::License_GPL,
   
      ki18n("Copyright (c) 2007 Developer") );
    KAboutData aboutData(
  KCmdLineArgs::init( argc, argv, &aboutData );
                        // The program name used internally. (componentName)
  KApplication app;
                        QStringLiteral("tutorial4"),
                        // A displayable program name string. (displayName)
                        i18n("Tutorial 4"),
                        // The program version string. (version)
                        QStringLiteral("1.0"),
                        // Short description of what the app does. (shortDescription)
                        i18n("A simple text area which can load and save."),
                        // The license this code is released under
                        KAboutLicense::GPL,
                        // Copyright Statement (copyrightStatement = QString())
                        i18n("(c) 2015"),
                        // Optional text shown in the About box.
                        // Can contain any information desired. (otherText)
                        i18n("Some text..."),
                        // The program homepage string. (homePageAddress = QString())
                        QStringLiteral("http://example.com/"),
                        // The bug report email address
                        // (bugsEmailAddress = QLatin1String("[email protected]")
                        QStringLiteral("[email protected]"));
    aboutData.addAuthor(i18n("Name"), i18n("Task"), QStringLiteral("[email protected]"),
                        QStringLiteral("http://your.website.com"), QStringLiteral("OSC Username"));
    KAboutData::setApplicationData(aboutData);
   
   
  MainWindow* window = new MainWindow();
    QCommandLineParser parser;
  window->show();
    aboutData.setupCommandLine(&parser);
  return app.exec();
    parser.process(app);
    aboutData.processCommandLine(&parser);
   
    MainWindow* window = new MainWindow();
    window->show();
   
    return app.exec();
}
}
</code>
</syntaxhighlight>
<tt>main.cpp</tt> hasn't changed from tutorial 3 except to change any reference to tutorial 3 to tutorial 4.
<tt>main.cpp</tt> hasn't changed from tutorial 3 except to change any reference from tutorial 3 to tutorial 4.


===mainwindow.h===
===mainwindow.h===
<code cppqt n>
<syntaxhighlight lang="cpp-qt">
#ifndef MAINWINDOW_H
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#define MAINWINDOW_H
 
#include <KXmlGuiWindow>
#include <KXmlGuiWindow>
#include <KTextEdit>


class KTextEdit;
class KJob;
class MainWindow : public KXmlGuiWindow
class MainWindow : public KXmlGuiWindow
{
{
  Q_OBJECT //new from tutorial3
    Q_OBJECT
 
   
   public:
   public:
     MainWindow(QWidget *parent=0);
     explicit MainWindow(QWidget *parent = nullptr);
 
   private:
   private:
    KTextEdit* textArea;
     void setupActions();
     void setupActions();
     QString fileName; //new
  private slots:
     void newFile();
    void openFile();
    void saveFile();
    void saveFileAs();
    void saveFileAs(const QString &outputFileName);
   
    void downloadFinished(KJob* job);


   private slots: //new
   private:
     void newFile(); //new
     KTextEdit* textArea;
     void openFile(); //new
     QString fileName;
    void saveFile(); //new
    void saveFileAs(); //new
    void saveFileAs(const QString &outputFileName); //new
};
};
 
#endif
#endif
</code>
</syntaxhighlight>
Since we want to add the ability to load and save files, we must add the functions which will do the work. Since the functions will be called through Qt's [http://doc.trolltech.com/latest/signalsandslots.html signal/slot] mechanism we must specify that these functions are slots as we do on line 19. Since we are using slots in this header file, we must also add the [http://doc.trolltech.com/latest/qobject.html#Q_OBJECT <tt>Q_OBJECT</tt>] macro.
Since we want to add the ability to load and save files, we must add the functions which will do the work. Since the functions will be called through Qt's [http://doc.qt.io/qt-5/signalsandslots.html signal/slot] mechanism we must specify that these functions are slots as we do on line 19. Since we are using slots in this header file, we must also add the [http://doc.qt.io/qt-5/qobject.html#Q_OBJECT <tt>Q_OBJECT</tt>] macro.


We also want to keep track of the filename of the currently opened file so we declare a <tt>{{qt|QString}} fileName</tt>.
We also want to keep track of the filename of the currently opened file so we declare a <tt>[http://doc.qt.io/qt-5/qstring.html QString] fileName</tt>.


===mainwindow.cpp===
===mainwindow.cpp===
<code cppqt n>
<syntaxhighlight lang="cpp-qt">
#include "mainwindow.h"
#include <QApplication>
#include <QAction>
#include <QSaveFile>
#include <QFileDialog>
#include <QTextStream>
#include <QByteArray>


#include <KApplication>
#include <KTextEdit>
#include <KAction>
#include <KLocalizedString>
#include <KLocale>
#include <KActionCollection>
#include <KActionCollection>
#include <KStandardAction>
#include <KStandardAction>
#include <KFileDialog> //new
#include <KMessageBox>
#include <KMessageBox> //new
#include <KIO/Job>
#include <KIO/NetAccess> //new
 
#include <KSaveFile> //new
#include "mainwindow.h"
#include <QTextStream> //new
 
MainWindow::MainWindow(QWidget *parent) : KXmlGuiWindow(parent), fileName(QString())
MainWindow::MainWindow(QWidget *parent)
    : KXmlGuiWindow(parent),
      fileName(QString()) //new
{
{
   textArea = new KTextEdit;
   textArea = new KTextEdit();
   setCentralWidget(textArea);
   setCentralWidget(textArea);
 
   setupActions();
   setupActions();
}
}
 
void MainWindow::setupActions()
void MainWindow::setupActions()
{
{
  KAction* clearAction = new KAction(this);
    QAction* clearAction = new QAction(this);
  clearAction->setText(i18n("Clear"));
    clearAction->setText(i18n("&Clear"));
  clearAction->setIcon(KIcon("document-new"));
    clearAction->setIcon(QIcon::fromTheme("document-new"));
  clearAction->setShortcut(Qt::CTRL + Qt::Key_W);
    actionCollection()->setDefaultShortcut(clearAction, Qt::CTRL + Qt::Key_W);
  actionCollection()->addAction("clear", clearAction);
    actionCollection()->addAction("clear", clearAction);
  connect(clearAction, SIGNAL(triggered(bool)),
    connect(clearAction, SIGNAL(triggered(bool)), textArea, SLOT(clear()));
          textArea, SLOT(clear()));
   
    KStandardAction::quit(qApp, SLOT(quit()), actionCollection());
   
    KStandardAction::open(this, SLOT(openFile()), actionCollection());
   
   
  KStandardAction::quit(kapp, SLOT(quit()),
    KStandardAction::save(this, SLOT(saveFile()), actionCollection());
                        actionCollection());
   
   
  KStandardAction::open(this, SLOT(openFile()),
    KStandardAction::saveAs(this, SLOT(saveFileAs()), actionCollection());
                        actionCollection()); //new
   
   
  KStandardAction::save(this, SLOT(saveFile()),
    KStandardAction::openNew(this, SLOT(newFile()), actionCollection());
                        actionCollection()); //new
   
    setupGUI(Default, "tutorial4ui.rc");
  KStandardAction::saveAs(this, SLOT(saveFileAs()),
                        actionCollection()); //new
  KStandardAction::openNew(this, SLOT(newFile()),
                        actionCollection()); //new
  setupGUI();
}
}
//New from here on


void MainWindow::newFile()
void MainWindow::newFile()
{
{
  fileName.clear();
    fileName.clear();
  textArea->clear();
    textArea->clear();
}
}


void MainWindow::saveFileAs(const QString &outputFileName)
void MainWindow::saveFileAs(const QString &outputFileName)
{
{
  KSaveFile file(outputFileName);
    if (!outputFileName.isNull())
  file.open();
    {
 
        QSaveFile file(outputFileName);
  QByteArray outputByteArray;
        file.open(QIODevice::WriteOnly);
  outputByteArray.append(textArea->toPlainText());
       
  file.write(outputByteArray);
        QByteArray outputByteArray;
  file.finalize();
        outputByteArray.append(textArea->toPlainText().toUtf8());
  file.close();
        file.write(outputByteArray);
 
        file.commit();
  fileName = outputFileName;
 
        fileName = outputFileName;
    }
}
}


void MainWindow::saveFileAs()
void MainWindow::saveFileAs()
{
{
  saveFileAs(KFileDialog::getSaveFileName());
    saveFileAs(QFileDialog::getSaveFileName(this, i18n("Save File As")));
}
}


void MainWindow::saveFile()
void MainWindow::saveFile()
{
{
  if(!fileName.isEmpty())
    if (!fileName.isEmpty())
  {
    {
    saveFileAs(fileName);
        saveFileAs(fileName);
  }
    }
  else
    else
  {
    {
    saveFileAs();
        saveFileAs();
  }
    }
}
}


void MainWindow::openFile()
void MainWindow::openFile()
{
{
  QString fileNameFromDialog = KFileDialog::getOpenFileName();
    QUrl fileNameFromDialog = QFileDialog::getOpenFileUrl(this, i18n("Open File"));
   
    if (!fileNameFromDialog.isEmpty())
    {
        KIO::Job* job = KIO::storedGet(fileNameFromDialog);
        fileName = fileNameFromDialog.toLocalFile();


  QString tmpFile;
        connect(job, SIGNAL(result(KJob*)), this, SLOT(downloadFinished(KJob*)));
  if(KIO::NetAccess::download(fileNameFromDialog, tmpFile,  
       
        this))
        job->exec();
  {
     }
    QFile file(tmpFile);
}
    file.open(QIODevice::ReadOnly);
    textArea->setPlainText(QTextStream(&file).readAll());
     fileName = fileNameFromDialog;


    KIO::NetAccess::removeTempFile(tmpFile);
void MainWindow::downloadFinished(KJob* job)
  }
{
  else
    if (job->error())
  {
    {
    KMessageBox::error(this,  
        KMessageBox::error(this, job->errorString());
         KIO::NetAccess::lastErrorString());
        fileName.clear();
  }
         return;
    }
   
    KIO::StoredTransferJob* storedJob = (KIO::StoredTransferJob*)job;
    textArea->setPlainText(QTextStream(storedJob->data(), QIODevice::ReadOnly).readAll());
}
}
</code>
</syntaxhighlight>
We'll get into the details of <tt>mainwindow.cpp</tt> in a while.


===tutorial4ui.rc===
===tutorial4ui.rc===
<code xml n>
<syntaxhighlight lang="xml">
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE kpartgui SYSTEM "kpartgui.dtd">
<gui name="tutorial4"
<gui name="tutorial4" version="1">
    version="1"
  <ToolBar name="mainToolBar" >
    xmlns="http://www.kde.org/standards/kxmlgui/1.0"
    <text>Main Toolbar</text>
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    <Action name="clear" />
    xsi:schemaLocation="http://www.kde.org/standards/kxmlgui/1.0
  </ToolBar>
                        http://www.kde.org/standards/kxmlgui/1.0/kxmlgui.xsd" >
   <MenuBar>
   <MenuBar>
     <Menu name="file" >
     <Menu name="file" >
Line 212: Line 253:
     </Menu>
     </Menu>
   </MenuBar>
   </MenuBar>
  <ToolBar name="mainToolBar" >
    <text>Main Toolbar</text>
    <Action name="clear" />
  </ToolBar>
</gui>
</gui>
</code>
</syntaxhighlight>
This is identical to <tt>tutorial3ui.rc</tt> from tutorial 3 except the <tt>name</tt> has changed to 'tutorial4'. We do not need to add any information about any of the <tt>KStandardAction</tt>s since the placement of those actions is handled automatically by KDE.
This is identical to <tt>tutorial3ui.rc</tt> from tutorial 3 except the <tt>name</tt> has changed to 'tutorial4'. We do not need to add any information about any of the <tt>KStandardAction</tt>s since the placement of those actions is handled automatically by KDE.


Line 220: Line 267:
Okay, now to implement the code that will do the loading and saving. This will all be happening in <tt>mainwindow.cpp</tt>
Okay, now to implement the code that will do the loading and saving. This will all be happening in <tt>mainwindow.cpp</tt>


The first thing we do is add
The first thing we do is add <tt>fileName(QString())</tt> to the <tt>MainWindow</tt> constructor list on line 17. This makes sure that <tt>fileName</tt> is empty right from the beginning.
<code cppqt>
fileName(QString())
</code>
to the <tt>MainWindow</tt> constructor list on line 16. This makes sure that <tt>fileName</tt> is empty right from the beginning.


===Adding the actions===
===Adding the actions===
Line 233: Line 276:


The first function we create is the <tt>newFile()</tt> function.
The first function we create is the <tt>newFile()</tt> function.
<code cppqt>
<syntaxhighlight lang="cpp-qt">
void MainWindow::newFile()
void MainWindow::newFile()
{
{
Line 239: Line 282:
   textArea->clear();
   textArea->clear();
}
}
</code>
</syntaxhighlight>
<tt>fileName.clear()</tt> sets the <tt>fileName</tt> QString to be empty to reflect the fact that this document does not yet have a presence on disc. <tt>textArea->clear()</tt> then clears the central text area using the same function that we connected the <tt>clear</tt> <tt>KAction</tt> to in tutorial 3.
<tt>fileName.clear()</tt> sets the <tt>fileName</tt> QString to be empty to reflect the fact that this document does not yet have a presence on disc. <tt>textArea->clear()</tt> then clears the central text area using the same function that we connected the <tt>clear</tt> <tt>KQction</tt> to in tutorial 3.


===Saving a file===
===Saving a file===
'''NOTE:''' To make this tutorial simple, this example program can only save to local storage even though it can open any file, even those from remote sources.


====saveFileAs(QString)====
====saveFileAs(QString)====


Now we get onto our first file handling code. We're going to implement a function which will save the contents of the text area to the file name given as a parameter. KDE provides a class for safely saving a file called {{class|KSaveFile}} which is derived from Qt's {{qt|QFile}}.
Now we get onto our first file handling code. We're going to implement a function which will save the contents of the text area to the file name given as a parameter. Qt provides a class for safely saving a file called [http://doc.qt.io/qt-5/qsavefile.html QSaveFile].


The function's prototype is
The function's prototype is
<code cppqt>
<syntaxhighlight lang="cpp-qt">
void MainWindow::saveFileAs(const QString &outputFileName)
void MainWindow::saveFileAs(const QString &outputFileName)
</code>
</syntaxhighlight>


We then create our <tt>KSaveFile</tt> object and open it with
We then create our <tt>QSaveFile</tt> object and open it with
<code cppqt>
<syntaxhighlight lang="cpp-qt">
KSaveFile file(outputFileName);
QSaveFile file(outputFileName);
file.open();
file.open(QIODevice::WriteOnly);
</code>
</syntaxhighlight>


Now that we have our file to write to, we need to format the text in the text area to a format which can be written to file. For this, we create a {{qt|QByteArray}} and fill it with the plain text version of whatever is in the text area:
Now that we have our file to write to, we need to format the text in the text area to a format which can be written to file. For this, we create a [http://doc.qt.io/qt-5/qbytearray.html QByteArray] and fill it with the plain text version of whatever is in the text area:
<code cppqt>
<syntaxhighlight lang="cpp-qt">
QByteArray outputByteArray;
QByteArray outputByteArray;
outputByteArray.append(textArea->toPlainText());
outputByteArray.append(textArea->toPlainText().toUtf8());
</code>
</syntaxhighlight>
Now that we have our <tt>QByteArray</tt>, we use it to write to the file with <tt>KSaveFile::write()</tt>. If we were using a normal <tt>QFile</tt>, this would make the changes immediately. However, if a problem occurred partway through writing, the file would become corrupted. For this reason, <tt>KSaveFile</tt> works by first writing to a temporary file and then, when you call <tt>KSaveFile::finalize()</tt> the changes are made to the actual file.
Now that we have our <tt>QByteArray</tt>, we use it to write to the file with <tt>QSaveFile::write()</tt>. If we were using a normal <tt>QFile</tt>, this would make the changes immediately. However, if a problem occurred partway through writing, the file would become corrupted. For this reason, <tt>QSaveFile</tt> works by first writing to a temporary file and then, when you call <tt>QSaveFile::commit()</tt> the changes are made to the actual file. <tt>commit()</tt> also closes the file.
<code cppqt>
<syntaxhighlight lang="cpp-qt">
file.write(outputByteArray);
file.write(outputByteArray);
file.finalize();
file.commit();
file.close();
</syntaxhighlight>
</code>
Finally, we set <tt>MainWindows</tt>'s <tt>fileName</tt> member to point to the file name we just saved to.
Finally, we set <tt>MainWindows</tt>'s <tt>fileName</tt> member to point to the file name we just saved to.
<code cppqt>
<syntaxhighlight lang="cpp-qt">
fileName = outputFileName;
fileName = outputFileName;
</code>
</syntaxhighlight>


====saveFileAs()====
====saveFileAs()====


This is the function that the <tt>saveAs</tt> slot is connected to. It simply calls the generic <tt>saveFileAs(QString)</tt> function and passes the file name returned by <tt>{{class|KFileDialog}}::[http://api.kde.org/4.0-api/kdelibs-apidocs/kio/html/classKFileDialog.html#8891356c249c5911e1ab15cc2739a89b getSaveFileName()]</tt>.
This is the function that the <tt>saveAs</tt> slot is connected to. It simply calls the generic <tt>saveFileAs(QString)</tt> function and passes the file name returned by <tt>[http://doc.qt.io/qt-5/qfiledialog.html QFileDialog]::[http://doc.qt.io/qt-5/qfiledialog.html#getSaveFileName getSaveFileName()]</tt>.


<code cppqt>
<syntaxhighlight lang="cpp-qt">
void MainWindow::saveFileAs()
void MainWindow::saveFileAs()
{
{
   saveFileAs(KFileDialog::getSaveFileName());
   saveFileAs(QFileDialog::getSaveFileName(this, i18n("Save File As")));
}
}
</code>
</syntaxhighlight>


This our first actual use of the KIO library. {{class|KFileDialog}} provides a number of static functions for displaying the common file dialog that is used by all KDE applications. Calling <tt>KFileDialog::getSaveFileName()</tt> will display a dialog where the user can select the name of the file to save to or choose a new name. The function returns the full file name, which we then pass to <tt>saveFileAs(QString)</tt>.
[http://doc.qt.io/qt-5/qfiledialog.html QFileDialog] provides a number of static functions for displaying the common file dialog that is used by all KDE applications. Calling <tt>QFileDialog::getSaveFileName()</tt> will display a dialog where the user can select the name of the file to save to or choose a new name. The function returns the full file name, which we then pass to <tt>saveFileAs(QString)</tt>.


====saveFile()====
====saveFile()====


<code cppqt>
<syntaxhighlight lang="cpp-qt">
void MainWindow::saveFile()
void MainWindow::saveFile()
{
{
Line 302: Line 346:
   }
   }
}
}
</code>
</syntaxhighlight>


There's nothing exciting or new in this function, just the logic to decide whether or not to show the save dialog. If <tt>fileName</tt> is not empty, then the file is saved to <tt>fileName</tt>. But if it is, then the dialog is shown to allow the user to select a file name.
There's nothing exciting or new in this function, just the logic to decide whether or not to show the save dialog. If <tt>fileName</tt> is not empty, then the file is saved to <tt>fileName</tt>. But if it is, then the dialog is shown to allow the user to select a file name.
Line 308: Line 352:
===Loading a file===
===Loading a file===


Finally, we get round to being able to load a file from disc. The code for this is all contained in <tt>MainWindow::openFile()</tt>.
Finally, we get round to being able to load a file, from disc or from a remote location like an FTP server. The code for this is all contained in <tt>MainWindow::openFile()</tt>.
 
First we must ask the user for the name of the file they wish to open. We do this using another one of the <tt>QFileDialog</tt> functions, this time <tt>getOpenFileName()</tt>:
<syntaxhighlight lang="cpp-qt">
QUrl fileNameFromDialog = QFileDialog::getOpenFileUrl(this, i18n("Open File"));
</syntaxhighlight>
Here we use the QUrl class to handle files from remote locations.
 
Then we use the KIO library to retrieve our file. This allows us to open the file normally even if it's stored in a remote location like an FTP site. We make the following call to the <tt>[http://api.kde.org/frameworks-api/frameworks5-apidocs/kio/html/namespaceKIO.html#a17631774b47cddb0127d8a3c1fc2315c KIO::storedGet()]</tt> function with an argument for the file you wish to open or download:
<syntaxhighlight lang="cpp-qt">
KIO::Job* job = KIO::storedGet(fileNameFromDialog);
</syntaxhighlight>


First we must ask the user for the name of the file they wish to open. We do this using another one of the <tt>KFileDialog</tt> functions, this time <tt>getOpenFileName()</tt>:
The function returns a handle to a <tt>KIO::Job</tt>, which we first connect to our <tt>downloadFinished()</tt> slot before "running" the job.
<code cppqt>
<syntaxhighlight lang="cpp-qt">
QString fileNameFromDialog = KFileDialog::getOpenFileName();
connect(job, SIGNAL(result(KJob*)), this, SLOT(downloadFinished(KJob*)));
</code>


Then we use the KIO library to retrieve our file. This allows us to open the file with QFile even if it's stored in a remote location like an FTP site. We make the following call to {{class|NetAccess}}'s <tt>download()</tt> function
job->exec();
<code cppqt>
</syntaxhighlight>
KIO::NetAccess::download(fileNameFromDialog, tmpFile, this)
</code>
The first argument is the name of the file you wish to download. The second is a QString which, after the download is complete, will contain the location of the temporary copy of the file. It is this <tt>tmpFile</tt> we will work with from now on.


The function returns <tt>true</tt> or <tt>false</tt> depending on whether the transfer was successful. If it failed, we display a message box giving the error:
The rest of the work happens in the <tt>downloadFinished()</tt> slot. First, the job is checked for errors. If it failed, we display a message box giving the error. We also make sure to clear the fileName, since the file wasn't opened successfully:
<code cppqt>
<syntaxhighlight lang="cpp-qt">
KMessageBox::error(this, KIO::NetAccess::lastErrorString());
KMessageBox::error(this, job->errorString());
</code>
fileName.clear();
</syntaxhighlight>


Otherwise, we continue with opening the file.
Otherwise, we continue with opening the file.


We create a QFile by passing the temporary file created by <tt>NetAccess::download()</tt> to its constructor and then open it in read-only mode
The data that <tt>storedGet()</tt> successfully downloaded, in this case the contents of our text file, is stored in the <tt>data</tt> member of a <tt>[http://api.kde.org/frameworks-api/frameworks5-apidocs/kio/html/classKIO_1_1StoredTransferJob.html KIO::StoredTransferJob]</tt> class. But in order to display the contents of the file at text, we must use a [http://doc.qt.io/qt-5/qtextstream.html QTextStream]. We create one by passing the the data of the <tt>StoredTransferJob</tt> to its constructor and then call its <tt>readAll()</tt> function to get the text from the file. This is then passed to the <tt>setPlainText()</tt> function of our text area.
<code cppqt>
 
QFile file(tmpFile);
<syntaxhighlight lang="cpp-qt">
file.open(QIODevice::ReadOnly);
KIO::StoredTransferJob* storedJob = (KIO::StoredTransferJob*)job;
</code>
textArea->setPlainText(QTextStream(storedJob->data(), QIODevice::ReadOnly).readAll());
</syntaxhighlight>


In order to display the contents of the file, we must use a {{class|QTextStream}}. We create one by passing the contents of our file to its constructor and then call QFile's <tt>readAll()</tt> function to get the text from the file. This is then passed to the <tt>setPlainText()</tt> function of our text area.
'''NOTE:''' Again, for simplicity's sake, this tutorial only saves text files to local disk. When you open a remote file for viewing and try to save it, the program will behave as if you were calling Save As on a completely new file.


<code cppqt>
==Make, Install, and Run==
textArea->setPlainText(QTextStream(&file).readAll());
</code>


We then store the path of the file we just opened:
===CMakeLists.txt===
<code cppqt>
<syntaxhighlight lang="cmake">
fileName = fileNameFromDialog;
cmake_minimum_required(VERSION 3.0)
</code>
and finally, we remove the temporary file that was created by <tt>NetAccess::download()</tt>:
<code cppqt>
KIO::NetAccess::removeTempFile(tmpFile);
</code>


==Make, Install And Run==
project (tutorial4)


===CMakeLists.txt===
set(QT_MIN_VERSION "5.3.0")
<code ini n>
set(KF5_MIN_VERSION "5.2.0")
project(tutorial4)
 
find_package(ECM 1.0.0 REQUIRED NO_MODULE)
find_package(KDE4 REQUIRED)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include_directories(${KDE4_INCLUDES})
 
include(KDEInstallDirs)
set(tutorial4_SRCS  
include(KDECMakeSettings)
  main.cpp
include(KDECompilerSettings NO_POLICY_SCOPE)
  mainwindow.cpp
include(FeatureSummary)
 
find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
    Core    # QCommandLineParser, QStringLiteral, QSaveFile, QTextStream, QByteArray
    Widgets # QApplication, QAction, QFileDialog
)
 
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
    CoreAddons      # KAboutData
    I18n            # KLocalizedString
    XmlGui          # KXmlGuiWindow, KActionCollection
    TextWidgets    # KTextEdit
    ConfigWidgets  # KStandardActions
    WidgetsAddons  # KMessageBox
    KIO            # KIO
)
   
 
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
   
set(tutorial4_SRCS main.cpp mainwindow.cpp)
 
add_executable(tutorial4 ${tutorial4_SRCS})
 
target_link_libraries(tutorial4
    Qt5::Widgets
    KF5::CoreAddons
    KF5::I18n
    KF5::XmlGui
    KF5::TextWidgets
    KF5::ConfigWidgets
    KF5::WidgetsAddons
    KF5::KIOCore
)
)
kde4_add_executable(tutorial4 ${tutorial4_SRCS})
target_link_libraries(tutorial4 ${KDE4_KDEUI_LIBS}
                                ${KDE4_KIO_LIBS})
install(TARGETS tutorial4 DESTINATION ${BIN_INSTALL_DIR})
install(FILES tutorial4ui.rc
        DESTINATION ${DATA_INSTALL_DIR}/tutorial4)
</code>
Since we are now using the KIO library, we must tell CMake to link against it. We do this by passing <tt>${KDE4_KIO_LIBS}</tt> to the <tt>target_link_libraries()</tt> function.


With this file, the tutorial can built and run in the same way as tutorial 3. For more information, see tutorial 3.
install(TARGETS tutorial4  ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
 
install(FILES tutorial4ui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/tutorial4)
</syntaxhighlight>
Since we are now using the KIO library, we must tell CMake to link against it. We do this by passing <tt>KIO</tt> to the <tt>find_package()</tt> function and <tt>KF5::KIOCore</tt> to <tt>target_link_libraries()</tt> function.


mkdir build && cd build
With this file, the tutorial can be built and run in the same way as tutorial 3. For more information, see tutorial 3.
cmake .. -DCMAKE_INSTALL_PREFIX=$HOME
<syntaxhighlight lang="bash">
make install
mkdir build && cd build
$HOME/bin/tutorial4
cmake .. -DCMAKE_INSTALL_PREFIX=$HOME
make install
XDG_DATA_DIRS=$HOME/share:$XDG_DATA_DIRS $HOME/bin/tutorial4
</syntaxhighlight>


==Moving On==
==Moving On==
Now you can move on to KCmdLineArgs (TODO [[User:milliams]]).
Now you can move on to the [[Development/Tutorials/CommandLineArguments|command line arguments]] tutorial.


{{Tip||The source code on this page applies only the current KDE Frameworks 5 ("KF5") version. For the older KDE Development Platform ("KDE4"), See [[Development/Tutorials/Saving_and_loading/KDE4]]}}
[[Category:C++]]
[[Category:C++]]

Revision as of 01:15, 6 March 2018


Loading and saving files
Tutorial Series   Beginner Tutorial
Previous   Tutorial 3 - Actions
What's Next   Tutorial 5 - Using Command Line Arguments
Further Reading   Tutorial: Using KIO Slaves in your Program KIO::NetAccess QFile

Abstract

Now that we have a basic text editor interface, it's time to make it do something useful. At the most basic, a text editor needs to be able to load files from disc, save files that you've created/edited and create new files.

The KDE Frameworks provides a number of classes for working with files which make life a lot easier for developers. The KIO framework allows you to easily access files through network-transparent protocols. At the same time, Qt also provides standard file dialogs for opening and saving files.

The Code

main.cpp

#include <cstdlib>
 
#include <QApplication>
#include <QCommandLineParser>

#include <KAboutData>
#include <KLocalizedString>

#include "mainwindow.h"
 
int main (int argc, char *argv[])
{
    QApplication app(argc, argv);
    
    KLocalizedString::setApplicationDomain("tutorial4");
    
    KAboutData aboutData(
                         // The program name used internally. (componentName)
                         QStringLiteral("tutorial4"),
                         // A displayable program name string. (displayName)
                         i18n("Tutorial 4"),
                         // The program version string. (version)
                         QStringLiteral("1.0"),
                         // Short description of what the app does. (shortDescription)
                         i18n("A simple text area which can load and save."),
                         // The license this code is released under
                         KAboutLicense::GPL,
                         // Copyright Statement (copyrightStatement = QString())
                         i18n("(c) 2015"),
                         // Optional text shown in the About box.
                         // Can contain any information desired. (otherText)
                         i18n("Some text..."),
                         // The program homepage string. (homePageAddress = QString())
                         QStringLiteral("http://example.com/"),
                         // The bug report email address
                         // (bugsEmailAddress = QLatin1String("[email protected]")
                         QStringLiteral("[email protected]"));
    aboutData.addAuthor(i18n("Name"), i18n("Task"), QStringLiteral("[email protected]"),
                        QStringLiteral("http://your.website.com"), QStringLiteral("OSC Username"));
    KAboutData::setApplicationData(aboutData);
 
    QCommandLineParser parser;
    aboutData.setupCommandLine(&parser);
    parser.process(app);
    aboutData.processCommandLine(&parser);
    
    MainWindow* window = new MainWindow();
    window->show();
    
    return app.exec();
}

main.cpp hasn't changed from tutorial 3 except to change any reference from tutorial 3 to tutorial 4.

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
 
#include <KXmlGuiWindow>

class KTextEdit;
class KJob;
 
class MainWindow : public KXmlGuiWindow
{
    Q_OBJECT
    
  public:
    explicit MainWindow(QWidget *parent = nullptr);
 
  private:
    void setupActions();
 
  private slots:
    void newFile();
    void openFile();
    void saveFile();
    void saveFileAs();
    void saveFileAs(const QString &outputFileName);
    
    void downloadFinished(KJob* job);

  private:
    KTextEdit* textArea;
    QString fileName;
};
 
#endif

Since we want to add the ability to load and save files, we must add the functions which will do the work. Since the functions will be called through Qt's signal/slot mechanism we must specify that these functions are slots as we do on line 19. Since we are using slots in this header file, we must also add the Q_OBJECT macro.

We also want to keep track of the filename of the currently opened file so we declare a QString fileName.

mainwindow.cpp

#include <QApplication>
#include <QAction>
#include <QSaveFile>
#include <QFileDialog>
#include <QTextStream>
#include <QByteArray>

#include <KTextEdit>
#include <KLocalizedString>
#include <KActionCollection>
#include <KStandardAction>
#include <KMessageBox>
#include <KIO/Job>

#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent) : KXmlGuiWindow(parent), fileName(QString())
{
  textArea = new KTextEdit();
  setCentralWidget(textArea);
  
  setupActions();
}

void MainWindow::setupActions()
{
    QAction* clearAction = new QAction(this);
    clearAction->setText(i18n("&Clear"));
    clearAction->setIcon(QIcon::fromTheme("document-new"));
    actionCollection()->setDefaultShortcut(clearAction, Qt::CTRL + Qt::Key_W);
    actionCollection()->addAction("clear", clearAction);
    connect(clearAction, SIGNAL(triggered(bool)), textArea, SLOT(clear()));
    
    KStandardAction::quit(qApp, SLOT(quit()), actionCollection());
    
    KStandardAction::open(this, SLOT(openFile()), actionCollection());
 
    KStandardAction::save(this, SLOT(saveFile()), actionCollection());
 
    KStandardAction::saveAs(this, SLOT(saveFileAs()), actionCollection());
 
    KStandardAction::openNew(this, SLOT(newFile()), actionCollection());
    
    setupGUI(Default, "tutorial4ui.rc");
}

void MainWindow::newFile()
{
    fileName.clear();
    textArea->clear();
}

void MainWindow::saveFileAs(const QString &outputFileName)
{
    if (!outputFileName.isNull())
    {
        QSaveFile file(outputFileName);
        file.open(QIODevice::WriteOnly);
        
        QByteArray outputByteArray;
        outputByteArray.append(textArea->toPlainText().toUtf8());
        file.write(outputByteArray);
        file.commit();

        fileName = outputFileName;
    }
}

void MainWindow::saveFileAs()
{
    saveFileAs(QFileDialog::getSaveFileName(this, i18n("Save File As")));
}

void MainWindow::saveFile()
{
    if (!fileName.isEmpty())
    {
        saveFileAs(fileName);
    }
    else
    {
        saveFileAs();
    }
}


void MainWindow::openFile()
{
    QUrl fileNameFromDialog = QFileDialog::getOpenFileUrl(this, i18n("Open File"));
    
    if (!fileNameFromDialog.isEmpty())
    {
        KIO::Job* job = KIO::storedGet(fileNameFromDialog);
        fileName = fileNameFromDialog.toLocalFile();

        connect(job, SIGNAL(result(KJob*)), this, SLOT(downloadFinished(KJob*)));
        
        job->exec();
    }
}

void MainWindow::downloadFinished(KJob* job)
{
    if (job->error())
    {
        KMessageBox::error(this, job->errorString());
        fileName.clear();
        return;
    }
    
    KIO::StoredTransferJob* storedJob = (KIO::StoredTransferJob*)job;
    textArea->setPlainText(QTextStream(storedJob->data(), QIODevice::ReadOnly).readAll());
}

We'll get into the details of mainwindow.cpp in a while.

tutorial4ui.rc

<?xml version="1.0" encoding="UTF-8"?>
<gui name="tutorial4"
     version="1"
     xmlns="http://www.kde.org/standards/kxmlgui/1.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://www.kde.org/standards/kxmlgui/1.0
                         http://www.kde.org/standards/kxmlgui/1.0/kxmlgui.xsd" >
 
  <MenuBar>
    <Menu name="file" >
      <Action name="clear" />
    </Menu>
  </MenuBar>
 
  <ToolBar name="mainToolBar" >
    <text>Main Toolbar</text>
    <Action name="clear" />
  </ToolBar>
 
</gui>

This is identical to tutorial3ui.rc from tutorial 3 except the name has changed to 'tutorial4'. We do not need to add any information about any of the KStandardActions since the placement of those actions is handled automatically by KDE.

Explanation

Okay, now to implement the code that will do the loading and saving. This will all be happening in mainwindow.cpp

The first thing we do is add fileName(QString()) to the MainWindow constructor list on line 17. This makes sure that fileName is empty right from the beginning.

Adding the actions

The first thing we are going to do is provide the outward interface for the user so they can tell the application to load and save. Like with the quit action in tutorial 3, we will use KStandardActions. On lines 37 to 47 we add the actions in the same way as for the quit action. For each one, we connect it to the appropriate slot that we declared in the header file.

Creating a new document

The first function we create is the newFile() function.

void MainWindow::newFile()
{
  fileName.clear();
  textArea->clear();
}

fileName.clear() sets the fileName QString to be empty to reflect the fact that this document does not yet have a presence on disc. textArea->clear() then clears the central text area using the same function that we connected the clear KQction to in tutorial 3.

Saving a file

NOTE: To make this tutorial simple, this example program can only save to local storage even though it can open any file, even those from remote sources.

saveFileAs(QString)

Now we get onto our first file handling code. We're going to implement a function which will save the contents of the text area to the file name given as a parameter. Qt provides a class for safely saving a file called QSaveFile.

The function's prototype is

void MainWindow::saveFileAs(const QString &outputFileName)

We then create our QSaveFile object and open it with

QSaveFile file(outputFileName);
file.open(QIODevice::WriteOnly);

Now that we have our file to write to, we need to format the text in the text area to a format which can be written to file. For this, we create a QByteArray and fill it with the plain text version of whatever is in the text area:

QByteArray outputByteArray;
outputByteArray.append(textArea->toPlainText().toUtf8());

Now that we have our QByteArray, we use it to write to the file with QSaveFile::write(). If we were using a normal QFile, this would make the changes immediately. However, if a problem occurred partway through writing, the file would become corrupted. For this reason, QSaveFile works by first writing to a temporary file and then, when you call QSaveFile::commit() the changes are made to the actual file. commit() also closes the file.

file.write(outputByteArray);
file.commit();

Finally, we set MainWindows's fileName member to point to the file name we just saved to.

fileName = outputFileName;

saveFileAs()

This is the function that the saveAs slot is connected to. It simply calls the generic saveFileAs(QString) function and passes the file name returned by QFileDialog::getSaveFileName().

void MainWindow::saveFileAs()
{
  saveFileAs(QFileDialog::getSaveFileName(this, i18n("Save File As")));
}

QFileDialog provides a number of static functions for displaying the common file dialog that is used by all KDE applications. Calling QFileDialog::getSaveFileName() will display a dialog where the user can select the name of the file to save to or choose a new name. The function returns the full file name, which we then pass to saveFileAs(QString).

saveFile()

void MainWindow::saveFile()
{
  if(!fileName.isEmpty())
  {
    saveFileAs(fileName);
  }
  else
  {
    saveFileAs();
  }
}

There's nothing exciting or new in this function, just the logic to decide whether or not to show the save dialog. If fileName is not empty, then the file is saved to fileName. But if it is, then the dialog is shown to allow the user to select a file name.

Loading a file

Finally, we get round to being able to load a file, from disc or from a remote location like an FTP server. The code for this is all contained in MainWindow::openFile().

First we must ask the user for the name of the file they wish to open. We do this using another one of the QFileDialog functions, this time getOpenFileName():

QUrl fileNameFromDialog = QFileDialog::getOpenFileUrl(this, i18n("Open File"));

Here we use the QUrl class to handle files from remote locations.

Then we use the KIO library to retrieve our file. This allows us to open the file normally even if it's stored in a remote location like an FTP site. We make the following call to the KIO::storedGet() function with an argument for the file you wish to open or download:

KIO::Job* job = KIO::storedGet(fileNameFromDialog);

The function returns a handle to a KIO::Job, which we first connect to our downloadFinished() slot before "running" the job.

connect(job, SIGNAL(result(KJob*)), this, SLOT(downloadFinished(KJob*)));

job->exec();

The rest of the work happens in the downloadFinished() slot. First, the job is checked for errors. If it failed, we display a message box giving the error. We also make sure to clear the fileName, since the file wasn't opened successfully:

KMessageBox::error(this, job->errorString());
fileName.clear();

Otherwise, we continue with opening the file.

The data that storedGet() successfully downloaded, in this case the contents of our text file, is stored in the data member of a KIO::StoredTransferJob class. But in order to display the contents of the file at text, we must use a QTextStream. We create one by passing the the data of the StoredTransferJob to its constructor and then call its readAll() function to get the text from the file. This is then passed to the setPlainText() function of our text area.

KIO::StoredTransferJob* storedJob = (KIO::StoredTransferJob*)job;
textArea->setPlainText(QTextStream(storedJob->data(), QIODevice::ReadOnly).readAll());

NOTE: Again, for simplicity's sake, this tutorial only saves text files to local disk. When you open a remote file for viewing and try to save it, the program will behave as if you were calling Save As on a completely new file.

Make, Install, and Run

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)

project (tutorial4)

set(QT_MIN_VERSION "5.3.0")
set(KF5_MIN_VERSION "5.2.0")

find_package(ECM 1.0.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)

include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(FeatureSummary)

find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS 
    Core    # QCommandLineParser, QStringLiteral, QSaveFile, QTextStream, QByteArray
    Widgets # QApplication, QAction, QFileDialog
)

find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
    CoreAddons      # KAboutData
    I18n            # KLocalizedString
    XmlGui          # KXmlGuiWindow, KActionCollection
    TextWidgets     # KTextEdit
    ConfigWidgets   # KStandardActions
    WidgetsAddons   # KMessageBox
    KIO             # KIO
)
    

feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
    
set(tutorial4_SRCS main.cpp mainwindow.cpp)

add_executable(tutorial4 ${tutorial4_SRCS})

target_link_libraries(tutorial4
    Qt5::Widgets
    KF5::CoreAddons
    KF5::I18n
    KF5::XmlGui
    KF5::TextWidgets
    KF5::ConfigWidgets
    KF5::WidgetsAddons
    KF5::KIOCore
)

install(TARGETS tutorial4  ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})

install(FILES tutorial4ui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/tutorial4)

Since we are now using the KIO library, we must tell CMake to link against it. We do this by passing KIO to the find_package() function and KF5::KIOCore to target_link_libraries() function.

With this file, the tutorial can be built and run in the same way as tutorial 3. For more information, see tutorial 3.

mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=$HOME
make install
XDG_DATA_DIRS=$HOME/share:$XDG_DATA_DIRS $HOME/bin/tutorial4

Moving On

Now you can move on to the command line arguments tutorial.

The source code on this page applies only the current KDE Frameworks 5 ("KF5") version. For the older KDE Development Platform ("KDE4"), See Development/Tutorials/Saving_and_loading/KDE4