Qtアプリの設定を保存する方法(QSettings)

#ご挨拶

Qt Advent Calendar 2018での4回目の投稿となりますKATO Kanryuです。
よろしくお願いします。

##まず宣伝

- 世界最速の画像ビューアー、[QuickViewer](https://github.com/kanryu/quickviewer)をQtで作りました。
- [QActionManager](https://github.com/kanryu/qactionmanager)
    - アプリにQt Creatorと同等のキーボード／マウスショートカットのカスタマイズ機能を提供します
    - マウスカスタマイズ機能はオリジナル
- [QLanguageSelector](https://github.com/kanryu/qlanguageselector)
    - 言語切り替えUI（メニュー)を自動生成
    - Qtアプリを再ビルドなしに翻訳言語を増やせるようになります
    - Qtアプリをテキストエディタだけでリアルタイムに翻訳できるようにします
- [QFullscreenFrame](https://github.com/kanryu/qfullscreenframe)
    - アプリをフルスクリーン表示させたときに、メインメニューやツールバーをスライド表示したいときがあると思います。それを実現するやつです。イベントハンドラを細かく設定することになるのでC++11推奨
- [QNamedPipe](https://github.com/kanryu/qnamedpipe)
    - アプリケーションを複数起動防止しつつ、2つ目以降のプロセスの起動オプションを1つ目のプロセスに引き渡したりする処理って単純ながら実装が面倒なものです。この問題を各OSのNamedPipeを使ってシンプルに解決するライブラリです。
    - 本家のQNamedPipeと異なり、QNetworkなどのコンポーネントは不要です。

##先行記事のご紹介

本稿については既に先行する記事があるので、先にご紹介しておきます。

- 【Qt】QSettingsでアプリケーションの設定を保存する
    - http://taurano.hatenablog.com/entry/2015/12/17/014741

こちらの記事のほうが内容が簡潔ですので、QSettingsなんて全く触ったことないよ、という方はこちらを先にご覧になったほうがいいかもしれません。

##設定ファイルの重要性

アプリケーションを開発するとき、全くカスタマイズ性が無く毎回同じ動きでいいのだ、ということは少ないのではないでしょうか。前回開いたファイルを履歴に残したいでしょうし、ツールバーやメニューの表示状態、ウィンドウの位置や大きさも復元したいかもしれません。そういうとき、それらの設定値をどこかに保存し、次回の起動時にそれを読み込んで復元する処理が必要になります。

QtではQSettingsという単純ながら奥深い設定保存用クラスがあり、様々な設定値を比較的高速に読み書きできます。

##QSettingsの仕組み

QSettingsはKey/Value型のオンメモリデータベースを構成し、更に複数のSectionをもたせたりそれを切り替えたりできます。

SectionやKeyにはもっぱら文字列を使用しますが、Valueにはbool, int, double, QString, QStringList, Json等、様々なデータ形式が格納可能です。それらは全て1行の文字列としてシリアライズされます。

そういったデータベースの内容をiniファイルやレジストリなどに書き出したり、読み込んだりすることができます。QSettingsはレジストリを読み書きする際に本領を発揮しますが、まずはiniファイルを読み書きするケースを見てみましょう。

##QSettingsによるiniファイルの読み書き

QSettingsの公式マニュアルを見て、一番面食らうのはコンストラクタではないでしょうか。
http://doc.qt.io/qt-5/qsettings.html

```C++
QSettings(const QString &organization, const QString &application = QString(), QObject *parent = nullptr)
QSettings(QSettings::Scope scope, const QString &organization, const QString &application = QString(), QObject *parent = nullptr)
QSettings(QSettings::Format format, QSettings::Scope scope, const QString &organization, const QString &application = QString(), QObject *parent = nullptr)
QSettings(const QString &fileName, QSettings::Format format, QObject *parent = nullptr)
```

なんでこんなにたくさんあって引数もこんなにたくさんあるの…（しかもほとんど省略できない)ってなりません？
ともあれ、iniファイルを読む分には使用するコンストラクタは4つ目のみです。

```C++
QSettings settings("some_app.ini", QSettings::IniFormat);
settings.setIniCodec(QTextCodec::codecForName("UTF-8"));
```
アプリケーションを普通に起動している場合はこれだけでカレントディレクトリの"some_app.ini"を読み書きできます。文字コードは必ずUTF-8にしておきましょう。文字コードで余計な問題は起こしたくないので！

##書き出し方

iniファイル中にSectionを設定する場合、例えばQuickViewerではこのような書き方をしています。

```C++
m_settings.beginGroup("File");
m_settings.setValue("AutoLoaded", m_autoLoaded);
m_settings.setValue("MaxHistoryCount", m_maxHistoryCount);
m_settings.setValue("History", QVariant::fromValue(m_history));
m_settings.setValue("MaxBookmarkCount", m_maxBookmarkCount);
m_settings.setValue("Bookmarks", QVariant::fromValue(m_bookmarks));
m_settings.setValue("ProhibitMultipleRunning", m_prohibitMultipleRunning);
m_settings.setValue("LastViewPath", m_lastViewPath);
m_settings.setValue("DontSavingHistory", m_dontSavingHistory);
m_settings.setValue("ExtractSolidArchiveToTemporaryDir", m_extractSolidArchiveToTemporaryDir);
m_settings.setValue("LastOpenedFolderPath", m_lastOpenedFolderPath);
m_settings.endGroup();
```

すると、"some_app.ini"にはこのような形で書き出されます。

```ini
[File]
AutoLoaded=true
MaxHistoryCount=36
History=C:/Users/kanryu/Pictures
MaxBookmarkCount=20
Bookmarks=C:/Users/kanryu/Pictures/IMG_1248.JPG
ProhibitMultipleRunning=false
LastViewPath=C:/Users/kanryu/Pictures/DESKTOP-TCV56FK.jpg
DontSavingHistory=false
ExtractSolidArchiveToTemporaryDir=true
LastOpenedFolderPath=
```
ところどころQVariant::fromValue()という形で変換がかかっていますが、これらの中身はQStringListで、1項目の場合はそのままStringに。2項目以上の場合はリスト形式で自動変換してくれます。

全てのSectionを書き出し終わったら、

```C++
m_settings.sync();
```

忘れずにファイル書き出しをしておきましょう。

##読み込み方

逆に、読み込む場合はこのように記述します。

```C++
m_settings.beginGroup("File");
m_autoLoaded  = m_settings.value("AutoLoaded", true).toBool();
m_history = m_settings.value("History", QStringList()).value<QStringList>();
m_maxHistoryCount = m_settings.value("MaxHistoryCount", 36).toInt();
m_bookmarks = m_settings.value("Bookmarks", QStringList()).value<QStringList>();
m_maxBookmarkCount = m_settings.value("MaxBookmarkCount", 20).toInt();
m_prohibitMultipleRunning  = m_settings.value("ProhibitMultipleRunning", false).toBool();
m_lastViewPath = m_settings.value("LastViewPath", "").toString();
m_dontSavingHistory  = m_settings.value("DontSavingHistory", false).toBool();
m_extractSolidArchiveToTemporaryDir = m_settings.value("ExtractSolidArchiveToTemporaryDir", true).toBool();
m_lastOpenedFolderPath = m_settings.value("LastOpenedFolderPath", "").toString();
m_settings.endGroup();
```

QSettings->value()メソッドは

```C++
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const
```

という引数を取っており、第1引数にkey名、第2引数にデフォルト値を設定しておきます。
取得結果はQVariantとなり、そのままでは使いづらいので、適宜 toBool()、toInt()、toString()などで変換します。

大抵の場合直感どおり書けば素直に動いてくれるでしょう。

##enum型の読み書き

しかし、enum型となると素直には行きません。そもそもenumは識別子であって文字列ではないので、どうやってiniファイルに書きだせというのでしょう？　実は、頑張ればできるんです。

例えば

```C++
class qvEnums : public QObject
{
    Q_OBJECT
public:
    enum FolderViewSort {
        OrderByName,
        OrderByUpdatedAt,
    };
    Q_ENUM(FolderViewSort)
}
```
という形でenumが定義されている場合、読み込みは

```C++
QString folderSortModestring = m_settings.value("FolderSortMode", "OrderByName").toString();
int enumIdx = qvEnums::staticMetaObject.indexOfEnumerator("FolderViewSort");
m_folderSortMode = (qvEnums::FolderViewSort)qvEnums::staticMetaObject.enumerator(enumIdx).keysToValue(folderSortModestring.toLatin1().data());
```

書き出しは

```C++
int enumIdx = qvEnums::staticMetaObject.indexOfEnumerator("FolderViewSort");
QString folderSortModestring = QString(qvEnums::staticMetaObject.enumerator(enumIdx).valueToKey(m_folderSortMode));
m_settings.setValue("FolderSortMode", folderSortModestring);

```
でできます。大変なので、何らかの形でモジュール化したほうがいいかもしれません。

##QSettingsでレジストリを読み書きする

QSettingsはiniの読み書きができ、それだけで十分高性能です。(読み書きが高速なところも気に入ってます)
しかし本領を発揮するのはレジストリを読み書きする場合で、これは他のクラスでは代替できません。

QtでWindowsアプリを開発する場合、例えば拡張子の関連づけ等で否が応でもレジストリを書き換える必要が出てくることがあります。どうやって記述したらいいのかサンプルが全然見つからず、かなり苦労したのでこの記事で紹介しておこうと思います。

```C++
#define APPLICATION_ID                "QuickViewer"
#define REGKEY_SOFTWARE               "HKEY_LOCAL_MACHINE\\SOFTWARE"
#define REGKEY_CLASSES                REGKEY_SOFTWARE "\\Classes"
#define REGKEYFORMAT_CLASSES          REGKEY_SOFTWARE "\\Classes\\%1"
#define REGKEYFORMAT_ASSOCFILE        APPLICATION_ID ".AssocFile.%1"
#define REGKEYFORMAT_ASSOCPATH        REGKEY_CLASSES  "\\" APPLICATION_ID ".AssocFile.%1"
#define REGKEY_REGISTEREDAPPLICATIONS REGKEY_SOFTWARE "\\RegisteredApplications"
#define REGKEY_APPLICATION            REGKEY_SOFTWARE "\\" APPLICATION_ID
#define REGKEY_APPLICATION_INAPP      REGKEY_CLASSES  "\\Applications\\" APPLICATION_ID ".exe"

QSettings settings(REGKEY_CLASSES, RegFormat);
settings.beginGroup(QString(REGKEYFORMAT_ASSOCFILE).arg(fmt));
settings.setValue(".", m_assocs[fmt].Description);
if(!m_assocs[fmt].IconName.isEmpty()) {
	settings.beginGroup("DefaultIcon");
	settings.setValue(".", getIconPath(m_assocs[fmt].IconName));
	settings.endGroup();
}
settings.beginGroup("shell");
settings.beginGroup("open");
settings.setValue(".", tr("&View with QuickViewer", "Menu displayed when right clicking on file in Explorer"));
settings.beginGroup("command");
settings.setValue(".", getExecuteApplication());
settings.endGroup();
settings.endGroup();
settings.endGroup();
settings.endGroup();
settings.sync();
```
[ソースコードの全文](https://github.com/kanryu/quickviewer/blob/master/AssociateFilesWithQuickViewer/fileassocdialog.cpp)

上記の例では、PCのデフォルトの拡張子関連付けを新規追加し、同時に拡張子ごとのアイコンを新規追加しています。

この記事が参考になるでしょうか。
http://ascii.jp/elem/000/001/203/1203385/

HKEY_LOCAL_MACHINEのレジストリは書き換えるために管理者権限が必要になりますので、アプリから特権レベル実行用の別プロセスを起動し、そこで実施します。アプリ本体とレジストリ設定用のアプリは別の実行ファイルにしたほうがよいでしょう。

レジストリは値を細かく変更するというケースは少なく、大抵は存在しないKeyのツリーを新規に作る、または自分が以前に作ったKeyのツリーをまるごと削除する、ということが多いのではないでしょうか。今回紹介したソースコードで両方実現していますので参考になれば幸いです。

##まとめ

- QSettingsを使えばiniファイルに設定データを読み書きできる(すごい!)
- QSettingsを使えばWindowsのレジストリを読み書きできる(Wonderful!)

終わり。