Qtで卓駆っぽいファイラーを作ろう 01

長年愛用してきた卓駆★、今でも現役で活躍してくれているのですが、世の中64bit化されているなか32bitの世界に取り残されている卓駆★君だと主にコンテキストメニューで上手く連携できないこともしばしば。

ってことで、Qtライブラリにおんぶにだっこ状態で卓駆っぽいファイラーを作ってしまおう企画開始します。

# 要求スペック

## 卓駆っぽさ
- キーボードからのコマンド直接実行機能を持たせる ("a"で全選択とか、"e"で編集など)
- ファイル一覧表示でウィンドウ内に収まらない場合の展開方向は列方向にする。  
  (エクスプローラーの一覧表示と同じですね、更新日時とかも表示されます。)
- ビュー形式は、左にフォルダツリー、右にフォルダリストとして、選択位置は同期させる。
- ルートフォルダはドライブ単位の選択式。 (デスクトップが先頭じゃない)
- 圧縮ファイル操作を装備
- テキスト / 画像の簡易ビューアー装備
- ファイルの一括操作をコマンドを装備

## 追加で欲しい機能
- ファイルコンテキストメニューは64bit Windows Shellのものを利用
- ファイルコピーもWindows Shellを利用予定 (最近のエクスプローラのファイルコピーがお気に入り♪)
- タブ対応したい
- ファイルリストの2ペイン表示

# 素のQtでどこまで出きるか？
## その1 フォルダツリー表示をしよう
ファイラーなのでフォルダ/ファイル構造を表示できなければ初まらないってことで、
まずはフォルダツリーに挑戦です。
これは比較的簡単でQtのサンプル集をあさると簡単に見つけることができた。

サンプルコードだと見栄えを良くするため余計な処理も入っていたが、エッセンスだけ抜きだすと以下の通り

```cpp
#include <QtWidgets/QApplication>
#include <QFileSystemModel>
#include <QTreeView>

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

    QFileSystemModel model;
    model.setRootPath(u8"C:/");

    QTreeView *tree = new QTreeView();
    tree->setModel(&model);
    tree->show();

    return app.exec();
}
```

これだけでフォルダツリーViewができる。  
メモリリークしてるじゃん！とか言わないように意図的ですw

![フォルダツリー](https://qiita-image-store.s3.amazonaws.com/0/181706/9257f4ca-4d2f-4bcd-7ac1-c309d5542c51.png)

内容を説明すると、
- QFileSytemModel  
  ファイルシステムから情報を抽出してくれる人。  
  エクスプローラみたいにデスクトップからのツリーじゃなく、ドライブからのツリーしか作れないが
  今回の要求仕様通りなので便利。  
  setRootPath(u8"C:/")で、どこのフォルダを起点に監視するか決めている。  
  ※監視起点を決めるだけで表示には関係ない、セパレータが"/"なのが要注意。文字コードはutf8だぞ
- QTreeView  
  実際にフォルダツリーを描画してくれる人。  
  setModel(&model)で表示すべきデータモデルを設定
  show()で表示
- app.exec()するとメッセージループが回って実際にQtが動き始めます。

とりあえずこれでフォルダツリーができたのですが、余計な表示がいくつかあるので消していきます。  

卓駆っぽいフォルダツリーにはファイルサイズ、種別、変更日時は不要です。  
TreeViewの中身の表示はテーブル表示みたいな構造になっており、下図のオレンジでかこった部分に
TreeViewの子ViewとしてHeaderViewが登録されています。
列方向の情報の表示制御はHeaderViewに対する表示制御で実現できます。

![ヘッダー](https://qiita-image-store.s3.amazonaws.com/0/181706/4420e567-1985-5af6-775e-4fc1c3665e74.png)

必要な表示は左端のフォルダ名だけですので、0番以降はすべて非表示に設定します。

```cpp
for(int i=1; i<tree->header()->count(); i++){
    tree->header()->setSectionHidden(i, true);
}
```

ついでにHeaderViewそのものも表示不要なので消します。

```cpp
tree->setHeaderHidden(true);
```

![ヘッダーレスView](https://qiita-image-store.s3.amazonaws.com/0/181706/8afadac9-752e-f435-ef79-4728df40596a.png)

見栄えはかなり卓駆っぽくなってきました。
あとは表示するデータです。  
この状態では、実はフォルダだけじゃなく、ファイルや別のドライブの情報も表示されており、
卓駆っぽいドライブ毎のツリー構造にはなっていません。

ファイルの表示しないようにするのは簡単で以下の通りにフォルダ表示(.と..は表示しない)を
FileSytemModelに指示すれば上手くいく。

```cpp
model.setFilter(QDir::Dirs|QDir::NoDotAndDotDot);
```

まだまだ、全ドライブが表示されてしまったりしていますが、それなりに卓駆っぽくなってきたので
フォルダツリーはこれでよいこととしましょう♪

## その2 ファイルリストを表示しよう

卓駆の2大ビューのうち、フォルダツリーがそれっぽくなったところで、今度はファイルリストです。
やることは簡単でいままでTreeViewを利用していたところをListViewに変えるだけです。

```cpp
#include <QtWidgets/QApplication>
#include <QFileSystemModel>
#include <QListView>

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

    QFileSystemModel model;

	list->setModel(&model);
	list->setRootIndex(model.setRootPath(u8"C:/Media/Picture/100MSDCF"));
    list->show();

    return app.exec();
}
```

できた。ちょ〜簡単♪

![ファイルリスト](https://qiita-image-store.s3.amazonaws.com/0/181706/0d68d132-957e-e1d8-a702-0fb922a0ce72.png)

だがちょっと待て、要求スペックはこうだ。

- ファイル一覧表示でウィンドウ内に収まらない場合の展開方向は列方向にする。 

ウィンドウの下で折り返せていない！  
でも安心ウィンドウの下で折り返すには以下で対応できる。

```cpp
list->setWrapping(true);
```

ついでに表示形式の明示的な宣言

```cpp
list->setViewMode(QListView::ListMode);
```

ウィンドウ枠に合わせて自動リサイズ

```cpp
list->setResizeMode(QListView::Adjust);
```

![ファイルリストWrap](https://qiita-image-store.s3.amazonaws.com/0/181706/1ac21ed5-e0a8-6309-b4cb-b76d1fb9dc6c.png)

まだいろいろ表示が足りていないが、標準機能で実施できるのはここまで。
なお、ラージアイコンが表示したければ、setViewMode(QListView::IconMode)で可能です。

## その3 ツリーとリストの合体

ツリーとリストが揃ったので最後に、両方のViewを合体させます。

合体させるにはSplitterを利用します。
addWidget()で分割枠の幅が調整可能な状態でViewが登録されていきます。  
ここでの注意点は、addWidgetで登録したViewの持ち主は登録先のWidgetとなる点です。  
簡単に言うと、今回はSplitterにTreeViewとListViewをaddWidgetしましたが、
これにより、TreeViewとListViewはSplitterのdelete時に一緒に削除されるようになります。  
Qtのこの挙動便利なのですが、幾つか注意点があります。

- addWidgetするためにはnewで作成したインスタンスでなければならない(auto変数はNG)  
  内部的に自動deleteされますので、auto変数のリファレンスを設定すると、Stackに対してdeleteを発行してしまい実行時に例外が発生してしまいます。
- 上記の制約により、一つのViewを複数のWidgetに登録することはできない。
- ModelはsetModelしても持ち主権限は変らない。  
  Viewは自動でdeleteしてくれるからModelもと思いがちですが、ModelはsetModelしても証券は変りません。
  同一Modelに複数のViewを設定して見たくなることへの対応ですね(たとえばExcelの表とグラフの関係など)

上記のQtの挙動があるので、サンプルのWidgetは他Widgetに登録した際にコードを書き換えなくてよいように
newで定義して、リーク無視な書き方をしているのですね。
実際、他のWidgetに登録されないViewはMainWindowかDialogくらいなので、この2つさえちゃんと消していれば
概ねdeleteし忘れるようなことはありません。  

```cpp
#include <QtWidgets/QApplication>
#include <QFileSystemModel>
#include <QTreeView>
int main(int argc, char *argv[])
{
    QApplication app(argc,argv);
	QFileSystemModel modelTree;
	modelTree.setRootPath("C:/");
    modelTree.setFilter(QDir::Dirs|QDir::NoDotAndDotDot);

	QFileSystemModel modelist;
	modelist.setRootPath("C:/");

	QListView *list = new QListView();
	list->setModel(&modelist);
	list->setRootIndex(modelist.index("C:/"));
	list->setViewMode(QListView::ListMode);
	list->setWrapping(true);
	list->setResizeMode(QListView::Adjust);

	QTreeView *tree = new QTreeView();
	tree->setModel(&modelTree);
	for (int i = 1; i<tree->header()->count(); i++) {
		tree->header()->setSectionHidden(i, true);
	}
	tree->setHeaderHidden(true);
	tree->setCurrentIndex(modelTree.index("C:/"));

	QSplitter *split = new QSplitter();
	split->addWidget(tree);
	split->addWidget(list);

    split->show();
    return app.exec();
}
```

以下が実行結果です。短いプログラムですが、ずいぶんと卓駆っぽくなってきたのではないでしょうか。

![Splitter](https://qiita-image-store.s3.amazonaws.com/0/181706/bf1d47e0-6329-da85-4450-f8abd0bc85a0.png)

これだけだと別々のViewを並べた表示しただけなので、多少連携させでブラウズ機能を追加しましょう。  
それにはconnectを使って、TreeViewのフォルダ選択変更とListViewのフォルダダブルクリックのイベントを
ハンドルして、各々にフォルダ変更を伝えてあげれば可能です。

```cpp
void currentChanged(const QModelIndex &index, QTreeView* tree, QListView* list)
{
	QFileSystemModel* modelTree = qobject_cast<QFileSystemModel*>(tree->model());
	QFileSystemModel* modelList = qobject_cast<QFileSystemModel*>(list->model());

	const QFileSystemModel* model = qobject_cast<const QFileSystemModel*>(index.model());

	if (model->isDir(index)) {
		QString str = model->filePath(index);

		tree->setCurrentIndex(modelTree->index(str));
		list->setRootIndex(modelList->setRootPath(str));
	}
}

int main(int argc, char *argv[])
{
	QApplication app(argc, argv);
	QFileSystemModel modelTree;
	modelTree.setRootPath("C:/");
	modelTree.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);

	QFileSystemModel modelist;
	modelist.setRootPath("C:/");

	QListView *list = new QListView();
	list->setModel(&modelist);
	list->setRootIndex(modelist.index("C:/"));
	list->setViewMode(QListView::ListMode);
	list->setWrapping(true);
	list->setResizeMode(QListView::Adjust);

	QTreeView *tree = new QTreeView();
	tree->setModel(&modelTree);
	for (int i = 1; i<tree->header()->count(); i++) {
		tree->header()->setSectionHidden(i, true);
	}
	tree->setHeaderHidden(true);
	tree->setCurrentIndex(modelTree.index("C:/"));

	QObject::connect(tree->selectionModel(), &QItemSelectionModel::currentChanged,
		[tree,list](const QModelIndex &current, const QModelIndex &previous)
	{	if (current != previous) currentChanged(current,tree,list); });

	QObject::connect(list, &QListView::doubleClicked,
		[tree,list](const QModelIndex &index) {currentChanged(index,tree,list); });

	QSplitter *split = new QSplitter();
	split->addWidget(tree);
	split->addWidget(list);

	split->show();
	return app.exec();
}
```

C++11のLambda式を使っているので、そこそこ新しいコンパイラとQtの5以上(私はQt 5.8を利用)が
必要になりますが、Qtも5になってメッセージディスパッチもずいぶんと楽に書けるようになりましたね。