Qtテキストエディタ実装における機能連携と状態管理

未保存データの検出メカニズム

QPlainTextEditのシグナル活用

QPlainTextEditはテキスト編集に関する各種シグナルを発行する機能を持っています。特にtextChanged()シグナルは、テキスト領域の内容が変化するたびにエミットされるため、編集状態の管理に最適です。

実装アプローチ

未保存データの検出は、以下の手順で実現します。まず、onTextChanged()という名称のプライベートスロット関数を定義し、textChanged()シグナルと接続します。次に、m_isTextChangedというbool型のメンバ変数を用意し、コンストラクタでfalseに初期化します。テキスト変更時には、このフラグをtrueに設定します。アプリケーション終了時やファイル操作時には、このフラグの状態を確認し、必要に応じて保存処理を実行します。

テキストエディタの機能拡張

ファイル操作の実装

テキストエディタの中核となるファイル操作機能について解説します。ファイル操作には、新規作成、開く、上書き保存、名前を指定して保存の4つの基本機能が含まれます。これらの機能は、メニューバーとツールバーの両方からアクセス可能とし、ユーザーインターフェースの一貫性を確保します。

ファイル操作の実装においては、現在編集中のファイルパスを管理するm_filePathメンバ変数が重要な役割を果たします。この変数により、ユーザーは上書き保存時にファイルダイアログを表示することなく、既存のファイルにデータを書き込むことができます。

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QMenuBar>
#include <QMenu>
#include <QAction>
#include <QToolBar>
#include <QIcon>
#include <QSize>
#include <QStatusBar>
#include <QLabel>
#include <QPlainTextEdit>
#include <QFileDialog>
#include <QString>

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QPlainTextEdit textEditor;
    QLabel positionLabel;
    QString currentFilePath;
    bool contentModified;

    explicit MainWindow(QWidget *parent = nullptr);
    MainWindow(const MainWindow&) = delete;
    MainWindow& operator=(const MainWindow&) = delete;
    bool initialize();

    bool setupMenuBar();
    bool setupToolBar();
    bool setupStatusBar();
    bool setupEditor();

    bool createFileMenu(QMenuBar *menuBar);
    bool createEditMenu(QMenuBar *menuBar);
    bool createFormatMenu(QMenuBar *menuBar);
    bool createViewMenu(QMenuBar *menuBar);
    bool createHelpMenu(QMenuBar *menuBar);

    bool addToolBarActions(QToolBar *toolBar);
    bool addEditToolActions(QToolBar *toolBar);
    bool addFormatToolActions(QToolBar *toolBar);
    bool addViewToolActions(QToolBar *toolBar);

    bool generateMenuAction(QAction *&action, QMenu *menu, 
                           const QString &text, QKeySequence key);
    bool generateToolAction(QAction *&action, QToolBar *toolBar,
                           const QString &tooltip, const QString &iconPath);

    QString displayFileDialog(QFileDialog::AcceptMode mode, const QString &title);
    void displayError(const QString &message);
    int displayConfirmDialog(const QString &message);
    bool persistData(const QString &filePath = QString());
    void handlePreContentChange();

private slots:
    void createNewFile();
    void openExistingFile();
    void saveFile();
    void saveFileAs();
    void onContentModified();

public:
    static MainWindow* createInstance();
    ~MainWindow() override;
};

#endif // MAINWINDOW_H

mainwindow_ui.cpp

#include "mainwindow.h"
#include <QDebug>
#include <QVBoxLayout>
#include <QHBoxLayout>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , textEditor(this)
    , positionLabel(this)
{
    currentFilePath = QString();
    contentModified = false;
    setWindowTitle("簡易テキストエディタ - [新規作成]");
}

bool MainWindow::initialize()
{
    bool result = true;
    result = result && setupMenuBar();
    result = result && setupToolBar();
    result = result && setupStatusBar();
    result = result && setupEditor();
    return result;
}

MainWindow* MainWindow::createInstance()
{
    MainWindow *instance = new MainWindow();

    if (instance == nullptr || !instance->initialize()) {
        delete instance;
        instance = nullptr;
    }

    return instance;
}

bool MainWindow::setupMenuBar()
{
    bool result = true;
    QMenuBar *menuBarArea = menuBar();

    result = result && createFileMenu(menuBarArea);
    result = result && createEditMenu(menuBarArea);
    result = result && createFormatMenu(menuBarArea);
    result = result && createViewMenu(menuBarArea);
    result = result && createHelpMenu(menuBarArea);

    return result;
}

bool MainWindow::setupToolBar()
{
    bool result = true;
    QToolBar *toolbar = addToolBar("標準ツールバー");

    if (toolbar) {
        toolbar->setIconSize(QSize(20, 20));
        result = result && addToolBarActions(toolbar);
        toolbar->addSeparator();
        result = result && addEditToolActions(toolbar);
        toolbar->addSeparator();
        result = result && addFormatToolActions(toolbar);
        toolbar->addSeparator();
        result = result && addViewToolActions(toolbar);
    }

    return result;
}

bool MainWindow::setupStatusBar()
{
    bool result = true;
    QStatusBar *statusArea = statusBar();

    QLabel *leftLabel = new QLabel("Editor v1.0");

    if (leftLabel) {
        positionLabel.setMinimumWidth(150);
        positionLabel.setAlignment(Qt::AlignCenter);
        positionLabel.setText("行: 1  列: 1");

        leftLabel->setMinimumWidth(150);
        leftLabel->setAlignment(Qt::AlignCenter);

        statusArea->addPermanentWidget(new QLabel());
        statusArea->addPermanentWidget(&positionLabel);
        statusArea->addPermanentWidget(leftLabel);
    } else {
        result = false;
    }

    return result;
}

bool MainWindow::setupEditor()
{
    bool result = true;
    textEditor.setParent(this);
    setCentralWidget(&textEditor);

    connect(&textEditor, &QPlainTextEdit::textChanged,
            this, &MainWindow::onContentModified);

    return result;
}

bool MainWindow::createFileMenu(QMenuBar *menuBar)
{
    bool result = true;
    QMenu *fileMenu = new QMenu(tr("ファイル(&F)"));

    result = (fileMenu != nullptr);

    if (result) {
        QAction *action = nullptr;

        result = result && generateMenuAction(action, fileMenu, 
                                              tr("新規作成(&N)"), 
                                              QKeySequence::New);
        if (result) {
            connect(action, &QAction::triggered,
                    this, &MainWindow::createNewFile);
            fileMenu->addAction(action);
        }

        fileMenu->addSeparator();

        result = result && generateMenuAction(action, fileMenu,
                                              tr("開く(&O)..."),
                                              QKeySequence::Open);
        if (result) {
            connect(action, &QAction::triggered,
                    this, &MainWindow::openExistingFile);
            fileMenu->addAction(action);
        }

        fileMenu->addSeparator();

        result = result && generateMenuAction(action, fileMenu,
                                              tr("上書き保存(&S)"),
                                              QKeySequence::Save);
        if (result) {
            connect(action, &QAction::triggered,
                    this, &MainWindow::saveFile);
            fileMenu->addAction(action);
        }

        fileMenu->addSeparator();

        result = result && generateMenuAction(action, fileMenu,
                                              tr("名前を付けて保存(&A)..."),
                                              QKeySequence());
        if (result) {
            connect(action, &QAction::triggered,
                    this, &MainWindow::saveFileAs);
            fileMenu->addAction(action);
        }

        fileMenu->addSeparator();

        result = result && generateMenuAction(action, fileMenu,
                                              tr("印刷(&P)..."),
                                              QKeySequence::Print);
        if (result) {
            fileMenu->addAction(action);
        }

        fileMenu->addSeparator();

        result = result && generateMenuAction(action, fileMenu,
                                              tr("終了(&X)"),
                                              QKeySequence());
        if (result) {
            fileMenu->addAction(action);
        }
    }

    if (result) {
        menuBar->addMenu(fileMenu);
    } else {
        delete fileMenu;
    }

    return result;
}

bool MainWindow::createEditMenu(QMenuBar *menuBar)
{
    bool result = true;
    QMenu *editMenu = new QMenu(tr("編集(&E)"));

    result = (editMenu != nullptr);

    if (result) {
        QAction *action = nullptr;

        result = result && generateMenuAction(action, editMenu,
                                              tr("元に戻す(&U)"),
                                              QKeySequence::Undo);
        if (result) {
            editMenu->addAction(action);
        }

        editMenu->addSeparator();

        result = result && generateMenuAction(action, editMenu,
                                              tr("やり直し(&R)"),
                                              QKeySequence::Redo);
        if (result) {
            editMenu->addAction(action);
        }

        editMenu->addSeparator();

        result = result && generateMenuAction(action, editMenu,
                                              tr("切り取り(&T)"),
                                              QKeySequence::Cut);
        if (result) {
            editMenu->addAction(action);
        }

        editMenu->addSeparator();

        result = result && generateMenuAction(action, editMenu,
                                              tr("コピー(&C)"),
                                              QKeySequence::Copy);
        if (result) {
            editMenu->addAction(action);
        }

        editMenu->addSeparator();

        result = result && generateMenuAction(action, editMenu,
                                              tr("貼り付け(&P)"),
                                              QKeySequence::Paste);
        if (result) {
            editMenu->addAction(action);
        }

        editMenu->addSeparator();

        result = result && generateMenuAction(action, editMenu,
                                              tr("すべて選択(&A)"),
                                              QKeySequence::SelectAll);
        if (result) {
            editMenu->addAction(action);
        }
    }

    if (result) {
        menuBar->addMenu(editMenu);
    } else {
        delete editMenu;
    }

    return result;
}

bool MainWindow::createFormatMenu(QMenuBar *menuBar)
{
    bool result = true;
    QMenu *formatMenu = new QMenu(tr("書式(&O)"));

    result = (formatMenu != nullptr);

    if (result) {
        QAction *action = nullptr;

        result = result && generateMenuAction(action, formatMenu,
                                              tr("右端で折り返す(&W)"),
                                              QKeySequence());
        if (result) {
            formatMenu->addAction(action);
        }

        formatMenu->addSeparator();

        result = result && generateMenuAction(action, formatMenu,
                                              tr("フォント(&F)..."),
                                              QKeySequence());
        if (result) {
            formatMenu->addAction(action);
        }
    }

    if (result) {
        menuBar->addMenu(formatMenu);
    } else {
        delete formatMenu;
    }

    return result;
}

bool MainWindow::createViewMenu(QMenuBar *menuBar)
{
    bool result = true;
    QMenu *viewMenu = new QMenu(tr("表示(&V)"));

    result = (viewMenu != nullptr);

    if (result) {
        QAction *action = nullptr;

        result = result && generateMenuAction(action, viewMenu,
                                              tr("ツールバー(&T)"),
                                              QKeySequence());
        if (result) {
            viewMenu->addAction(action);
        }

        viewMenu->addSeparator();

        result = result && generateMenuAction(action, viewMenu,
                                              tr("ステータスバー(&S)"),
                                              QKeySequence());
        if (result) {
            viewMenu->addAction(action);
        }
    }

    if (result) {
        menuBar->addMenu(viewMenu);
    } else {
        delete viewMenu;
    }

    return result;
}

bool MainWindow::createHelpMenu(QMenuBar *menuBar)
{
    bool result = true;
    QMenu *helpMenu = new QMenu(tr("ヘルプ(&H)"));

    result = (helpMenu != nullptr);

    if (result) {
        QAction *action = nullptr;

        result = result && generateMenuAction(action, helpMenu,
                                              tr("操作方法"),
                                              QKeySequence());
        if (result) {
            helpMenu->addAction(action);
        }

        helpMenu->addSeparator();

        result = result && generateMenuAction(action, helpMenu,
                                              tr("バージョン情報..."),
                                              QKeySequence());
        if (result) {
            helpMenu->addAction(action);
        }
    }

    if (result) {
        menuBar->addMenu(helpMenu);
    } else {
        delete helpMenu;
    }

    return result;
}

bool MainWindow::addToolBarActions(QToolBar *toolBar)
{
    bool result = true;
    QAction *action = nullptr;

    result = result && generateToolAction(action, toolBar,
                                          tr("新規作成"), ":/icons/new.png");
    if (result) {
        connect(action, &QAction::triggered,
                this, &MainWindow::createNewFile);
        toolBar->addAction(action);
    }

    result = result && generateToolAction(action, toolBar,
                                          tr("開く"), ":/icons/open.png");
    if (result) {
        connect(action, &QAction::triggered,
                this, &MainWindow::openExistingFile);
        toolBar->addAction(action);
    }

    result = result && generateToolAction(action, toolBar,
                                          tr("保存"), ":/icons/save.png");
    if (result) {
        connect(action, &QAction::triggered,
                this, &MainWindow::saveFile);
        toolBar->addAction(action);
    }

    result = result && generateToolAction(action, toolBar,
                                          tr("名前を付けて保存"), ":/icons/save_as.png");
    if (result) {
        connect(action, &QAction::triggered,
                this, &MainWindow::saveFileAs);
        toolBar->addAction(action);
    }

    return result;
}

bool MainWindow::addEditToolActions(QToolBar *toolBar)
{
    bool result = true;
    QAction *action = nullptr;

    result = result && generateToolAction(action, toolBar,
                                          tr("切り取り"), ":/icons/cut.png");
    if (result) {
        toolBar->addAction(action);
    }

    result = result && generateToolAction(action, toolBar,
                                          tr("コピー"), ":/icons/copy.png");
    if (result) {
        toolBar->addAction(action);
    }

    result = result && generateToolAction(action, toolBar,
                                          tr("貼り付け"), ":/icons/paste.png");
    if (result) {
        toolBar->addAction(action);
    }

    return result;
}

bool MainWindow::addFormatToolActions(QToolBar *toolBar)
{
    bool result = true;
    QAction *action = nullptr;

    result = result && generateToolAction(action, toolBar,
                                          tr("右端で折り返す"), ":/icons/wrap.png");
    if (result) {
        toolBar->addAction(action);
    }

    return result;
}

bool MainWindow::addViewToolActions(QToolBar *toolBar)
{
    bool result = true;
    QAction *action = nullptr;

    result = result && generateToolAction(action, toolBar,
                                          tr("ツールバー"), ":/icons/toolbar.png");
    if (result) {
        toolBar->addAction(action);
    }

    return result;
}

bool MainWindow::generateMenuAction(QAction *&action, QMenu *menu,
                                    const QString &text, QKeySequence key)
{
    bool result = true;
    action = new QAction(text, menu);

    if (action) {
        action->setShortcut(key);
    } else {
        result = false;
    }

    return result;
}

bool MainWindow::generateToolAction(QAction *&action, QToolBar *toolBar,
                                    const QString &tooltip, const QString &iconPath)
{
    bool result = true;
    action = new QAction(QString(), toolBar);

    if (action) {
        action->setToolTip(tooltip);
        action->setIcon(QIcon(iconPath));
    } else {
        result = false;
    }

    return result;
}

MainWindow::~MainWindow()
{
}

mainwindow_slots.cpp

#include "mainwindow.h"
#include <QFileDialog>
#include <QFile>
#include <QTextStream>
#include <QMessageBox>
#include <QDebug>

QString MainWindow::displayFileDialog(QFileDialog::AcceptMode mode, 
                                      const QString &title)
{
    QString resultPath;
    QFileDialog dialog;

    dialog.setWindowTitle(title);
    dialog.setAcceptMode(mode);

    QStringList nameFilters;
    nameFilters << tr("テキストファイル (*.txt)")
                << tr("すべてのファイル (*.*)");
    dialog.setNameFilters(nameFilters);

    if (mode == QFileDialog::AcceptOpen) {
        dialog.setFileMode(QFileDialog::ExistingFile);
    }

    if (dialog.exec() == QFileDialog::Accepted) {
        resultPath = dialog.selectedFiles().first();
    }

    return resultPath;
}

void MainWindow::displayError(const QString &message)
{
    QMessageBox msgBox(this);
    msgBox.setWindowTitle(tr("エラー"));
    msgBox.setText(message);
    msgBox.setIcon(QMessageBox::Critical);
    msgBox.setStandardButtons(QMessageBox::Ok);
    msgBox.exec();
}

int MainWindow::displayConfirmDialog(const QString &message)
{
    QMessageBox msgBox(this);
    msgBox.setWindowTitle(tr("確認"));
    msgBox.setText(message);
    msgBox.setIcon(QMessageBox::Question);
    msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No | 
                              QMessageBox::Cancel);

    return msgBox.exec();
}

bool MainWindow::persistData(const QString &filePath)
{
    QString targetPath = filePath;

    if (targetPath.isEmpty()) {
        targetPath = displayFileDialog(QFileDialog::AcceptSave, 
                                       tr("名前を付けて保存"));
    }

    if (!targetPath.isEmpty()) {
        QFile targetFile(targetPath);

        if (targetFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
            QTextStream writer(&targetFile);
            writer << textEditor.toPlainText();
            targetFile.close();

            setWindowTitle(tr("簡易テキストエディタ - [%1]")
                          .arg(targetPath));

            currentFilePath = targetPath;
            contentModified = false;

            return true;
        } else {
            displayError(tr("ファイルを保存できませんでした。\n\n%1")
                        .arg(targetPath));
        }
    }

    return false;
}

void MainWindow::handlePreContentChange()
{
    if (contentModified) {
        int choice = displayConfirmDialog(
            tr("変更内容を保存しますか?"));

        switch (choice) {
        case QMessageBox::Yes:
            persistData(currentFilePath);
            break;
        case QMessageBox::No:
            contentModified = false;
            break;
        case QMessageBox::Cancel:
        default:
            break;
        }
    }
}

void MainWindow::createNewFile()
{
    handlePreContentChange();

    if (!contentModified) {
        textEditor.clear();
        currentFilePath.clear();
        contentModified = false;
        setWindowTitle(tr("簡易テキストエディタ - [新規作成]"));
    }
}

void MainWindow::openExistingFile()
{
    handlePreContentChange();

    if (!contentModified) {
        QString filePath = displayFileDialog(QFileDialog::AcceptOpen,
                                             tr("ファイルを開く"));

        if (!filePath.isEmpty()) {
            QFile sourceFile(filePath);

            if (sourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
                textEditor.setPlainText(sourceFile.readAll());
                sourceFile.close();

                currentFilePath = filePath;
                setWindowTitle(tr("簡易テキストエディタ - [%1]")
                              .arg(currentFilePath));
            } else {
                displayError(tr("ファイルを開けませんでした。\n\n%1")
                            .arg(filePath));
            }
        }
    }
}

void MainWindow::saveFile()
{
    if (!currentFilePath.isEmpty()) {
        if (persistData(currentFilePath)) {
            // 保存成功
        }
    } else {
        saveFileAs();
    }
}

void MainWindow::saveFileAs()
{
    persistData();
}

void MainWindow::onContentModified()
{
    if (!contentModified) {
        setWindowTitle("*" + windowTitle());
    }
    contentModified = true;
}

main.cpp

#include <QApplication>
#include <QMainWindow>
#include <QTextCodec>
#include "mainwindow.h"

int main(int argc, char *argv[])
{
    QApplication application(argc, argv);

    QTextCodec *codec = QTextCodec::codecForName("UTF-8");
    QTextCodec::setCodecForLocale(codec);
    QTextCodec::setCodecForTr(codec);
    QTextCodec::setCodecForCStrings(codec);

    MainWindow *window = MainWindow::createInstance();
    int exitCode = -1;

    if (window) {
        window->show();
        exitCode = application.exec();
    }

    delete window;
    return exitCode;
}

実装のポイント

Qtフレームワークにおけるテキストエディタの実装では、Qtのシグナルとスロット機構を効果的に活用することが重要です。QPlainTextEditのtextChanged()シグナルを監視することで、テキストデータの変更を即座に検出し、contentModifiedフラグを更新します。このフラグの状態に基づいて、ウィンドウタイトルにアスタリスクを表示したり、アプリケーション終了時に保存確認ダイアログを表示したりします。

ファイル操作の実装では、QFileDialogクラスを使用してユーザーにファイル選択のインタフェースを提供します。開く操作の場合は既存のファイルのみを選択可能とし、保存操作の場合は新規ファイル名を入力できます。QFileとQTextStreamクラスを使用して、テキストデータのファイルへの読み書きを効率的に処理します。

Qt5以降では、従来のSIGNAL/SLOTマクロ構文に加え、Qt5の新しい接続構文(関数ポインタベースの接続)が利用可能になりました。新たな構文では、コンパイル時に接続の妥当性を検証できるため、より堅牢なコードを実現できます。本稿のコード例ではQt5の新しい構文を採用し、型安全なシグナルとスロットの接続を行っています。

タグ: Qt QPlainTextEdit QMainWindow シグナルとスロット テキストエディタ

5月13日 18:39 投稿