Nova Flow OS
KDE Developer Platform
KDE Developer Platform
  • KDE Developer Platform
    • Getting started
      • Building KDE software
        • KDE software
        • Where to find the development team
        • Learning more
        • Choose what to work on
        • Source code cross-referencing
        • Installing build dependencies
        • Set up a development environment
        • Building KDE software with kdesrc-build
        • Basic troubleshooting
        • Tips and tricks
        • IDE Configuration
          • Setting up an IDE for KDE development
          • Visual Studio Code
          • Qt Creator
          • Kate
          • KDevelop
          • CLion
          • Sublime Text
        • Building KDE software manually
        • Building KDE software with distrobox and podman
      • Kirigami
        • KDE is ours
        • Setting up and getting started
        • Explaining pages
        • Layouts, ListViews, and Cards
        • Adding actions
        • Adding a dialog
        • Using separate files
        • Next steps
        • Colors and themes in Kirigami
        • Typography
        • Actions based components
        • Page rows and page stacks
        • Scrollable pages and list views
        • Cards
        • Drawers
        • Chips
        • Dialog types
        • Controls and interactive elements
        • Form layouts
        • Inline messages
        • Action toolbars
        • Progress bars and indicators
        • List views
        • Understanding CMakeLists
        • Figuring out main.cpp
        • Connect logic to your QML user interface
        • Connect models to your QML user interface
        • About page
        • Introduction to Kirigami Addons
        • FormCard About pages
        • Form delegates in your settings pages
      • KXmlGui
        • Getting started with KXmlGui
        • Hello World!
        • Creating the main window
        • Using actions
        • Saving and loading
        • Command line interface
      • Python with Kirigami
        • Apps with QML and Python
        • Your first Python + Kirigami application
        • Creating a Python package
        • Creating a Flatpak
      • Common programming mistakes
      • Adding a new KDE project
    • Features
      • Icons
      • Configuration
        • The KConfig Framework
        • Introduction to KConfig
        • Using KConfig XT
        • KDE Frameworks 6 porting guide
        • Settings module (KCM) development
        • KConfigDialog
      • D-Bus
        • What is D-Bus practically useful for?
        • Introduction to D-Bus
        • Accessing D-Bus interfaces
        • Intermediate D-Bus
        • Creating D-Bus interfaces
        • Using custom types with D-Bus
        • D-Bus autostart services
      • Create your own mouse cursor theme
      • Session management
      • Archives
      • Desktop file
      • KAuth
        • Privilege Escalation
        • Using actions in your applications
      • KIdleTime
      • Akonadi: personal information management
        • Debugging Akonadi Resources
        • Using Akonadi in applications
      • Concurrent programming
      • Solid
      • Sonnet
    • Plasma themes and plugins
      • Getting started
      • Plasma Widget tutorial
        • How to create a plasmoid
        • Setup
        • Porting Plasmoids to KF6
        • Testing
        • QML
        • Plasma's QML API
        • Widget Properties
        • Configuration
        • Translations / i18n
        • Examples
        • C++ API
      • KWin Effects
      • Plasma Desktop scripting
        • Javascript Interaction With Plasma Shells
        • Templates
        • Examples
        • API documentation
        • Configuration keys
      • Plasma Style tutorial
        • Creating a Plasma Style quickstart
        • Understanding Plasma Styles
        • SVG elements and Inkscape
        • Background SVG format
        • System and accent colors
        • Theme elements reference
        • Porting themes to Plasma 5
        • Porting themes to Plasma 6
      • Aurorae window decorations
      • KWin scripting tutorial
        • Quick start
        • KWin scripting API
      • Wallpapers
      • Plasma comic
        • Tutorial
        • Testing and debugging
        • Examples
      • Create a custom Window Switcher
      • KRunner C++ Plugin
        • Basic Anatomy of a Runner
        • KRunner metadata format
    • Applications
      • Creating sensor faces
      • Dolphin
        • Creating Dolphin service menus
      • Kate
        • Kate plugin tutorial
      • KMines
        • Making a KMines theme
      • Writing tests
        • Appium automation testing
    • Packaging
      • Android
        • KDE on Android
        • Building applications for Android
        • Packaging and publishing applications for Android
        • Publishing on Google Play
          • Introduction
          • Packaging your app
          • Adding your app to Google Play
          • Publishing your app
          • Releasing new versions of old apps
        • Porting applications to Android
          • Basic porting
          • Making applications run well on Android
          • Metadata
      • Windows
        • Packaging and publishing applications for Windows
        • Publish your app in the Microsoft Store
          • Packaging your app for the Microsoft Store
          • Submitting your app to the Microsoft Store
      • Plasma Mobile
        • KDE on mobile devices
        • Porting a new device to Plasma Mobile
        • KDE Telephony stack
          • General Overview
          • Kernel layer
          • System daemons
            • General overview
            • Developing Telephony functionality
            • ModemManager Telephony functions
          • Session daemons
          • QML declarative plugin layer
          • KDE application layer
        • Execute applications
      • Distributing KDE software as Flatpak
        • Your first Flatpak
        • Extending your package
        • Nightly Flatpaks and Flathub
        • Testing your Flatpak
    • System administration
      • Shell scripting with KDE dialogs
      • Kiosk: Simple configuration management for large deployment
        • Abstract
        • Introduction to Kiosk
        • Kiosk keys
    • Contribute to the documentation
    • About
      • Readme
      • License
        • Creative Commons Attribution-ShareAlike 4.0 International
        • GNU General Public License 3.0 or later
Powered by GitBook
On this page
  • Introduction
  • The Code
  • Explanation
  1. KDE Developer Platform
  2. Getting started
  3. KXmlGui

Saving and loading

Introduces the KIO library while adding loading and saving support to our application.

PreviousUsing actionsNextCommand line interface

Last updated 8 months ago

Introduction

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 data storage, save files that have been created/edited, and create new files.

KDE Frameworks provides a number of classes for working with files that make life a lot easier for developers. allows you to easily access files through network-transparent protocols. Qt also provides standard file dialogs for opening and saving files.

The Code

main.cpp

We don't need to change anything in here.

#include <QApplication>
#include <QCommandLineParser>
#include <KAboutData>
#include <KLocalizedString>
#include "mainwindow.h"

int main (int argc, char *argv[])
{
    using namespace Qt::Literals::StringLiterals;

    QApplication app(argc, argv);
    KLocalizedString::setApplicationDomain("texteditor");
    KAboutData aboutData(
        u"texteditor"_s,
        i18n("Text Editor"),
        u"1.0"_s,
        i18n("A simple editor capable of saving and loading"),
        KAboutLicense::GPL,
        i18n("(c) 2024"),
        i18n("Educational application..."),
        u"https://apps.kde.org/someappname/"_s,
        u"submit@bugs.kde.org"_s);

    aboutData.addAuthor(
        i18n("John Doe"),
        i18n("Tutorial learner"),
        u"john.doe@example.com"_s,
        u"https://john-doe.example.com"_s,
        u"johndoe"_s);

    KAboutData::setApplicationData(aboutData);

    QCommandLineParser parser;
    aboutData.setupCommandLine(&parser);
    parser.process(app);
    aboutData.processCommandLine(&parser);

    MainWindow *window = new MainWindow();
    window->show();

    return app.exec();
}

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();
    void saveFileToDisk(const QString &outputFileName);

private Q_SLOTS:
    void newFile();
    void openFile();
    void saveFile();
    void saveFileAs();

    void downloadFinished(KJob *job);

private:
    KTextEdit *textArea;
    QString fileName;
};

#endif // MAINWINDOW_H

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/StoredTransferJob>
#include "mainwindow.h"

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

void MainWindow::setupActions()
{
    using namespace Qt::Literals::StringLiterals;

    QAction *clearAction = new QAction(this);
    clearAction->setText(i18n("&Clear"));
    clearAction->setIcon(QIcon::fromTheme(u"document-new-symbolic"_s));
    actionCollection()->addAction(u"clear"_s, clearAction);
    actionCollection()->setDefaultShortcut(clearAction, Qt::CTRL | Qt::Key_L);
    connect(clearAction, &QAction::triggered, textArea, &KTextEdit::clear);

    KStandardAction::quit(qApp, &QCoreApplication::quit, actionCollection());
    KStandardAction::open(this, &MainWindow::openFile, actionCollection());
    KStandardAction::save(this, &MainWindow::saveFile, actionCollection());
    KStandardAction::saveAs(this, &MainWindow::saveFileAs, actionCollection());
    KStandardAction::openNew(this, &MainWindow::newFile, actionCollection());

    setupGUI(Default, u"texteditorui.rc"_s);
}

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

void MainWindow::saveFileToDisk(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()
{
    saveFileToDisk(QFileDialog::getSaveFileName(this, i18n("Save File As")));
}

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

void MainWindow::openFile()
{
    const QUrl fileNameFromDialog = QFileDialog::getOpenFileUrl(this, i18n("Open File"));

    if (!fileNameFromDialog.isEmpty()) {
        KIO::Job *job = KIO::storedGet(fileNameFromDialog);
        fileName = fileNameFromDialog.toLocalFile();
        connect(job, &KJob::result, this, &MainWindow::downloadFinished);
        job->exec();
    }
}

void MainWindow::downloadFinished(KJob *job)
{
    if (job->error()) {
        KMessageBox::error(this, job->errorString());
        fileName.clear();
        return;
    }

    const KIO::StoredTransferJob *storedJob = qobject_cast<KIO::StoredTransferJob *>(job);

    if (storedJob) {
        textArea->setPlainText(QTextStream(storedJob->data(), QIODevice::ReadOnly).readAll());
    }
}

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

texteditorui.rc

Explanation

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

MainWindow::MainWindow(QWidget *parent) : KXmlGuiWindow(parent), fileName(QString())

Adding the actions

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 string to be empty to reflect the fact that this document is not stored anywhere yet. textArea->clear() then clears the central text area using the same function that we connected the 'clear' action to in the [previous tutorial]({{< relref "using_actions#creating-the-qaction-object" >}}).

Warning This example simply clears the text area without checking if the file has been saved first. It's only meant as a demonstration of file I/O and is _not_ an example of best programming practices.

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 from any location, even those from remote sources.

saveFileToDisk(const QString &)

The function's prototype is:

void MainWindow::saveFileAs(const QString &outputFileName)
QSaveFile file(outputFileName);
file.open(QIODevice::WriteOnly);
QByteArray outputByteArray;
outputByteArray.append(textArea->toPlainText().toUtf8());
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()

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

saveFile()

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

There's nothing exciting or new about 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 around to being able to load a file, from local storage or from a remote location like an FTP server. The code for this is all contained in MainWindow::openFile().

const QUrl fileNameFromDialog = QFileDialog::getOpenFileUrl(this, i18n("Open File"));
KIO::Job *job = KIO::storedGet(fileNameFromDialog);
connect(job, &KJob::result, this, &MainWindow::downloadFinished);
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.

const KIO::StoredTransferJob *storedJob = qobject_cast<KIO::StoredTransferJob *>(job);

if (storedJob) {
    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.

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)

project(texteditor)

set(QT_MIN_VERSION "6.6.0")
set(KF_MIN_VERSION "6.0.0")

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

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

find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
    Core    # QCommandLineParser, QStringLiteral
    Widgets # QApplication, QAction
)

find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS
    CoreAddons      # KAboutData
    I18n            # KLocalizedString
    XmlGui          # KXmlGuiWindow, KActionCollection
    TextWidgets     # KTextEdit
    ConfigWidgets   # KStandardActions

    WidgetsAddons   # KMessageBox

    KIO             # KIO

)

add_executable(texteditor)

target_sources(texteditor
    PRIVATE
    main.cpp
    mainwindow.cpp
)

target_link_libraries(texteditor
    Qt6::Widgets
    KF6::CoreAddons
    KF6::I18n
    KF6::XmlGui
    KF6::TextWidgets
    KF6::ConfigWidgets

    KF6::WidgetsAddons

    KF6::KIOCore

)

install(TARGETS texteditor ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES texteditorui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/texteditor)

feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)

Running our application

Once again, you can repeat the same steps provided in {{< ref "hello_world#kxmlgui-running" >}} to build and install the application. You can then run the project with:

kdesrc-build --run --exec texteditor kxmlgui-tutorial

or

texteditor

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 mechanism we must specify that these functions are slots using Q_SLOTS. Since we are using slots in this header file, we must also add the macro, as only can have signals and slots.

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

This is identical to the texteditorui.rc from the [previous tutorial]({{< relref "using_actions/#texteditoruirc" >}}). We do not need to add any information about any of the methods since the placement of those actions is handled automatically by the system.

The first thing we do is to initialize fileName(QString()) in the MainWindow's to make sure that fileName is empty right from the beginning.

We will then provide the outward interface for the user so they can tell the application to load and save. Like with the quit action in the [previous tutorial]({{< relref "using_actions/#kstandardaction" >}}), we will use . We add the actions in the same way we did for the quit action and, for each one, we connect it to the appropriate slot that we declared in the header file.

Now we get onto our first file handling code. We are 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 .

We then create our object and open it with:

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 to serve as our temporary string buffer and fill it with the plain text version of whatever is in the text area:

Now that we have our , we use it to write to the file with . If we were using a normal , this would make the changes immediately. However, if a problem occurred partway through writing, the file would become corrupted. For this reason, works by first writing to a temporary file and then, when calling , the changes are made to the actual file and the file is then closed.

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

provides a number of static functions for displaying the common file dialog that is used by all KDE applications. Calling 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 saveFileToDisk(QString).

First we must ask the user for the name of the file they wish to open. We do this using :

Here we use to handle files from remote locations.

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

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

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

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

Qt's signal/slot
Q_OBJECT
Q_OBJECTs
QString
KStandardAction
KXmlGui
constructor initializer list
KStandardAction
QSaveFile
QSaveFile
QByteArray
QByteArray
QSaveFile::write()
QFile
QSaveFile
QSaveFile::commit()
QFileDialog::getSaveFileName()
QFileDialog
QFileDialog::getSaveFileName()
QFileDialog::getOpenFileUrl()
QUrl
KIO
KIO::storedGet
KIO::Job
KIO::StoredTransferJob
QTextStream
KIO::StoredTransferJob
QTextStream::readAll()
KIO
find_package()
target_link_libraries()
KIO