LoginSignup
5
5

More than 5 years have passed since last update.

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

Posted at

本業が忙しくなかなか進められてないですorz

要求スペックの再確認

まずは卓駆っぽさチェック! がんばったのに卓駆っぽさが向上していないだと!

前回は、がんばってGUIの強化をしたのだが、よく見ると卓駆っぽさの要素にGUI要素が
ほとんど記載されていない。求める機能がCUIなのだから当然か...

それでもGUI的には卓駆っぽさは格段に向上しているはずということで、気を取り直して
今回はコマンド対応です。これできっと卓駆っぽさは大幅に向上するはず!

※前回追加された卓駆っぽい要求仕様も記載してあるぞ。

卓駆っぽさ

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

追加で欲しい機能

  • OK ファイルコンテキストメニューは64bit Windows Shellのものを利用
  • NG ファイルコピーもWindows Shellを利用予定 (最近のエクスプローラのファイルコピーがお気に入り♪)
  • OK タブ対応したい
  • OK ファイルリストの2ペイン表示
  • OK ダブルクリックでファイルを開く or 実行

その1 アーキテクチャやデザインパターン

今迄勢いでプログラムを書いてきたが、コマンド対応する前に設計を見直そう。
さすがに、そろそろコード行数が爆発して、手に負えなくなるからね。
というか、ここまで書いてまだ破綻の兆しすら見えないQtはやっぱりよいツールキットだと思う。

アーキテクチャ

MVC

採用するアーキテクチャは当然MVCだ。QtがMVCを採用しているのだから当然だね。

ちなみに今迄のところ、ほとんどViewの改造だけだったので、フォルダ変更のところを除いてControlは登場
してきてないから、MVアーキテクチャ。SelectionModelとかProxyModelを使えるのでMVPか?
ともあれ、今回の実装でMVCにバージョンアップするのだ!

ここで注意しないといけないのが、ViewとControlの責務分担。
ModelとViewはわりとはっきり区別が付くので、適当にやってもそれっぽいところに落ち着くのだけど、
ViewとControlは最初にルールを決めておかないと、Viewがやたらと肥え太る残念な結果になっちゃいます。

あまりピンとこないかもしませんが、たとえば前回までに実装した、マウスの右クリックや、ダブルクリックの
挙動は、Modelであるところのファイルシステムに影響を与えるにも関わらず、Viewとして実装している。
また、ファイル選択の実装もSelectionModelを操作しているのでコントールの仕事じゃね?とかViewとControlの
境界はじつに曖昧です。

卓駆っぽいファイラーでは、ViewとControlの区別は以下とルールとしよう。

  • マウスおよび、移動系キー操作およびスペースキーはViewとして実装。
  • その他のキーはキーそのものをアクション扱い
  • マウス操作はアクションが確定した時点でControlへ移譲(ContextMenu/クリックによる実行など)
  • SelectionModelへの操作はView実装可
  • メニューコマンド / ツールバーコマンドはQAction経由でControlへ

だいたいこんな感じだろうか。
要約するとSelectionModel/カーソル操作等のView内のみで完結するアクション
以外は、UIアクションイベントとしてSignal飛ばして、Control経由で処理を実行するというポリシーです。

前回マウスの挙動に直接処理を実装していることを指摘されましたが、上記のルールに従えば
とりあえずは大丈夫でしょう。

デザインパターン

私的にはデザインパターンは利用するものというより、自分の設計を説明する時の共通言語の一つとして
使うものという認識(設計を分解していくと、いろいろなデザインパターンが表われてくるものです)なので、
あまり、"デザインパターンのどれを利用して設計する"とかいう使い方はしないのだけど、あえて言わせてもらおう
ControlにはCommandパターンを利用すると!

Command

Controlなんて、関数で書きゃええやんとか思うかもしれないけど、わりとクラスメンバに作業変数が
ほしくなって、使っているうちにクラスがどんどん汚染されていくというのはよくある話です。
まぁちゃんと設計すればそんなことはないのだけど、人間楽な方向に流れたくなるのは世の常。
そう仕方ないんです!便利なんだもん。

そんな、ずぶらなあなたを救ってくれるのがCommandパターン。Command一つごとにクラスを作成する面倒さを
許容すれば、メンバ変数ハッピーなプログラムを書いてもクラスレベルでは(外部モジュールを触らなければ)reentrantが保証される素敵なパターン。C++だとヘッダーとソースで1コマンド2ファイルになっちゃうのが
最大の弱点だけど、まぁ数千行の巨大ファイルができるリスクよりはましでしょう。

継続的に増えていく部分はスケールアウトが容易な方が、コードが読み辛くならないと思うのですよね。
作業空間を分けられるので、プラグインみたいな構造や、Undo/Redoに対応しやすくなるのも利点ですし。
対応しないけど...

アプリケーションの管理構造

上記を踏まえて、管理構造は以下のようにしようと思う。UMLの線の種類は適当だよw

TableEngineClass

  1. ViewからのSignalは一旦TeViewStoreで一時受けして、TeDispatcherに処理を移譲。
  2. TeDispatcherTeCommandMapを参照して、アクションとコマンドの関連付けを取得。
  3. コマンドが確定したらTeCommandFactoryでコマンドを作成&実行。
  4. 各コマンドは、必要に応じてTeViewStoreから対応Viewを取得しアクションを実行する。

ってな感じに処理してみようと思う。レイアウトの変更自由度はTeViewStoreで抽象化して、
オプションによるコマンド割り付けはTeCommandMapを弄ればなんとかなるだろうとう算段ですね。

Modelはあまり積極的に管理する予定はないので、各Viewから直接取得する方向で管理すれば大丈夫でしょう。
わりと性善説にのっとったノーガードな管理だけど、問題点は運用でカバーしよう←失敗を招く危険な思想(^^;)

その2 コマンド対応

ディスパッチの方針も決ったところで、卓駆っぽさが上らない元凶コマンド対応を開始しよう。

まずは、ファイル管理ソフトになくてはならないコマンドということで、コピー・移動・削除を対応して
いきましょう。

ダイアログ

卓駆のファイル操作ではコピー先を決めるためにまず以下のようなダイアログが出てくるので。
ダイアログからちゃっちゃと作ってしまいましょう。

CopyToDialog

ダイアログ作成にはQDialogクラスから派生クラスを作成します。
べつにQWidgetでもたいして作業の手間は変わらにのだけど、あるものは有効活用していきましょう。
なお、今回はダイアログを使いまわしたいので、派生クラスを作成していますが、
これくらいのダイアログであれば、コンストラクタで記載している内容をインスタンス作成時に、
実行しても作れるのがQtのよいところですね。

TeFilePathDialog::TeFilePathDialog(QWidget *parent)
    : QDialog(parent)
{
    //Helpボタン削除
    Qt::WindowFlags flags = windowFlags();
    setWindowFlags(flags & ~Qt::WindowContextHelpButtonHint);

    QVBoxLayout *layout = new QVBoxLayout();

    //FilePath指定用 CommboBox
    mp_combo = new QComboBox();
    mp_combo->setEditable(true);
    mp_combo->setMinimumWidth(300);
    mp_combo->installEventFilter(this);
    layout->addWidget(mp_combo);

    //OK Cancelボタン登録
    QHBoxLayout* hlayout = new QHBoxLayout();

    hlayout->addStretch(10);
    QPushButton* button = new QPushButton(tr("OK"));
    button->setDefault(true);
    connect(button, SIGNAL(clicked()), this, SLOT(accept()));

    hlayout->addWidget(button);

    button = new QPushButton(tr("Cancel"));
    connect(button, SIGNAL(clicked()), this, SLOT(reject()));
    hlayout->addWidget(button);
    hlayout->addStretch(10);

    layout->addLayout(hlayout);

    setLayout(layout);
}

上から見ていくと、まずはダイアログボックスからhelpボタンを削除。
通常ダイアログのリサイズは禁止にするのですが、長いパス名のときリサイズできると便利なので、
リサイズ機能についてはそのまま放置の方向です。

//Helpボタン削除
Qt::WindowFlags flags = windowFlags();
setWindowFlags(flags & ~Qt::WindowContextHelpButtonHint);

パス選択用のComboBox作成。
ここでは、setEditable()を用いて、テキスト入力可能にし、setMinimumWidth()で最小幅を指定して
addWidget()で登録ですね。

//FilePath指定用 CommboBox
mp_combo = new QComboBox();
mp_combo->setEditable(true);
mp_combo->setMinimumWidth(300);
layout->addWidget(mp_combo);

せっかくなので、ちょっとスケベ心を出してパス名補完機能をつけるには以下のようにします。
QFileSystemModelをフォルダのみモードで生成して、ルートパスをsetRootPath("")初期化 (※デフォルトではカレントフォルダがルートになるようです)。QCompleterにモデルを設定して、Comboboxに登録すれば
intellisenceみたいな補完ができるようになります。QFileSystemModelの遅延ロードの影響かWindowsとQtの
相性の問題なのか、補完がうまく発動しないことがしばしばありますが、ないよりいいでしょう。
ここはQtの今後に期待です。(^^;)

QFileSystemModel* model = new QFileSystemModel();
model->setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
model->setRootPath("");
QCompleter* completer = new QCompleter(model);
completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel);
completer->setCaseSensitivity(Qt::CaseInsensitive);
mp_combo->setCompleter(completer);

こんな感じにドロップダウンで候補が出てきます。(ComboBoxの表示じゃないよ)

Completer.png

最後にOKとCancelボタンを登録して完了。
これはQDialogButtonBoxを使うとすっきりします。
acceptrejectに接続しているのは、ボタンにOK、Cancelの動作をさせるものです。
acceptだとexec()を実行した時にtureが返り、rejectだとfalseが返ることになります。
acceptrejectどちらでもも呼び出されるとダイアログは閉じられます。

//OK Cancelボタン登録
QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttonBox->setCenterButtons(true);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);

layout->addWidget(buttonBox);

おっと、折角設定したパスを取得できないと意味がありませんね。
ついでに設定関数も入れておきましょう。
相対パスも利用できるようにカレントパス設定ができるようにしてあります。

void TeFilePathDialog::setCurrentPath(const QString & path)
{
    m_currentPath = path;
}

QString TeFilePathDialog::currentPath()
{
    return m_currentPath;
}

void TeFilePathDialog::setTargetPath(const QString & path)
{
    mp_combo->setCurrentText(QDir::toNativeSeparators(path));
}

QString TeFilePathDialog::targetPath()
{
    if (m_currentPath.isEmpty()) {
        return QDir::fromNativeSeparators(mp_combo->currentText());
    }
    else {
        QDir dir(m_currentPath);
        QString path = QDir::fromNativeSeparators(mp_combo->currentText());
        return dir.absoluteFilePath(path);
    }
}

作ったダイアログは以下のようにして利用することができます。

TeFilePathDialog dlg;
if(dlg.exec()){
  //設定されたパスを取得
  QString path = dlg.targetPath();
}

これでファイルパス入力用のダイアログは完成なのですが、卓駆にはShift+Enterもしくは、参照ボタンを押すことで
ツリーからファイルパスを選択することができる機能があります。
参照ボタンは見栄えが悪いし私は使わないので、とりあえずなかったことにして、Shift+Enterの対応です。

ComboBoxにフォーカスがあたっている場合のみ反応してほしいので、eventFilterという仕組みを利用します。
これを利用すると別クラスからComboBox等のWidgetのイベントディスパッチに割り込めるので便利ですね。

今回はShift+Enterの対応なので、QEvent::KeyPressを受けてShift+Enterが押されたら
処理を実行するようにしましょう。

bool TeFilePathDialog::eventFilter(QObject * obj, QEvent * event)
{
    if (obj == mp_combo) {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
            //Shift+Enterによるフォルダ選択ツリー表示
            if ((keyEvent->modifiers() == Qt::ShiftModifier) && (keyEvent->key() == Qt::Key_Return)) {
                TeSelectPathDialog dlg(this);
                dlg.setWindowTitle(tr(u8"Select Path"));
                dlg.setTargetPath(QDir::toNativeSeparators(targetPath()));
                if (dlg.exec()) {
                    setTargetPath(QDir::fromNativeSeparators(dlg.targetPath()));
                }
                return true;
            }
        }
    }
    return false;
}

作成したFilterは以下の命令で有効にすることができます。
cpp
mp_combo->installEventFilter(this);

お次は先程利用した、ツリーからファイルパスを選択するためのダイアログです。

SelectPathDialog.png

基本は同じなので、説明はよいでしょう。パス補完はsetCompletionMode(QCompleter::InlineCompletion)
として今回は先頭候補を選択状態で自動補完されるようにしています。あいかわらず補完が発動したりしなかったり
していますが、おおくは気にしない方向にします。
一旦Backspaceで消して戻ると表示されたりするのですが原因不明なのですよね。一見するとQtのソースコード上は
QFileSystemModelの遅延読み出しにも対応しているようなのですが...やっぱりWindowsとの相性なのでしょうか。

TeSelectPathDialog::TeSelectPathDialog(QWidget *parent)
    : QDialog(parent)
{
    //Helpボタン削除
    Qt::WindowFlags flags = windowFlags();
    setWindowFlags(flags & ~Qt::WindowContextHelpButtonHint);

    QFileSystemModel* model = new QFileSystemModel();
    model->setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
    model->setRootPath("");

    //パス名記載用EditBox
    QVBoxLayout *layout = new QVBoxLayout();
    mp_edit = new QLineEdit();
    //パス名入力とツリーを連動させる。
    connect(mp_edit, &QLineEdit::textChanged, [this](const QString& text) 
       {setTargetPath(QDir::fromNativeSeparators(text)); });
    layout->addWidget(mp_edit);

    //LineEditに文字補完機能
    QCompleter* completer = new QCompleter(model);
    completer->setCompletionMode(QCompleter::InlineCompletion);
    completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel);
    completer->setCaseSensitivity(Qt::CaseInsensitive);
    mp_edit->setCompleter(completer);


    //フォルダツリー
    model = new QFileSystemModel();
    model->setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
    mp_tree = new QTreeView();
    mp_tree->setModel(model);
    mp_tree->setRootIndex(model->setRootPath(u8""));
    for (int i = 1; i<mp_tree->header()->count(); i++) {
        mp_tree->header()->setSectionHidden(i, true);
    }
    mp_tree->setHeaderHidden(true);
    mp_tree->installEventFilter(this);
    mp_tree->setAutoScroll(true);

    //フォルダを選択するとlineEditにも反映
    connect(mp_tree->selectionModel(), &QItemSelectionModel::currentChanged,
      [this,model](const QModelIndex &current, const QModelIndex &previous)
        { setTargetPath(model->filePath(current)); });

    layout->addWidget(mp_tree);

    //OK Cancelボタン登録
    QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
    buttonBox->setCenterButtons(true);
    connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
    connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);

layout->addWidget(buttonBox);
    setLayout(layout);
}

パス名の初期設定 & 取得インターフェイスは以下のようにします。
指定されたパスが存在するなら、ツリー上の選択も同時に変更します。

本来なら、ダイアログ表示前に設定しておくと、表示時に選択されたフォルダが表示されるよう
自動スクロールするはずなのですが、うまく動かないようです。
どうやらShowイベントで表示物が確定していないからっぽいのですが、Qtのバグっぽいですし、
スマートな解決方法がなかったので、これもとりあえず放置。(放置多いですね...)

void TeSelectPathDialog::setTargetPath(const QString & path)
{
  QFileSystemModel* model = qobject_cast<QFileSystemModel*>(mp_tree->model());
  QModelIndex index = model->index(path);
  if (index.isValid()) {
    mp_tree->setCurrentIndex(index);
    mp_tree->scrollTo(mp_tree->currentIndex(),QAbstractItemView::PositionAtCenter);
  }
  mp_edit->setText(QDir::toNativeSeparators(path));
}

QString TeSelectPathDialog::targetPath()
{
  return QDir::fromNativeSeparators(mp_edit->text());
}

コピーコマンド

ずいぶんと寄りみちしましたが、ここからコマンド本体です。
前述した通り、Commandパターン & Factoryパターンで実装していきましょう。

コマンドのベースは以下の通りで、

  1. キーイベントをディスパッチすると、デフォルトコンストラクタによりインスタンスを作成。
  2. execute()により実処理を実行。

という順番で実行される。

class TeCmdCopyTo :
    public TeCommandBase
{
public:
    TeCmdCopyTo();
    virtual ~TeCmdCopyTo();

protected:
    virtual bool execute(TeViewStore* p_store);
};

おつぎはexecuteの中身です。基本的な流れは

  1. 選択ファイルを抽出するためのViewを確定。
  2. 選択ファイルをQSelectionModelから取得 & パス名変換。
  3. 先程作ったTeFilePathDialogでコピー先フォルダを選択。
  4. コピー先フォルダの存在有無を確認。
    フォルダが存在しない場合は新規作成 ( or リネームコピーに処理を変更 )
  5. IFileOperationを利用してファイルコピーを実施

って感じになります。思いのほか実装が必要なのですね...

気持を切り替えて、Viewの選択から始めていきます。

TeFolderView* p_folder = p_store->currentFolderView();

//コピー対象確定
QAbstractItemView* p_itemView = nullptr;
if (p_folder->tree()->hasFocus()) {
    p_itemView = p_folder->tree();
}
else {
    p_itemView = p_folder->list();
}

TeCommandBaseでは、イベントの発行元のViewとEventその物も利用できるようにしているのですが、
メニューバーから実行される場合もあるので、WindowのFocus状態から処理対象Viewを確定しています。
ちなみにp_store->currentFolderView();はタブ & 分割ウィンドウ対応を見こしていますが、
現状はまだ実装していませんw

Viewが確定したら、関連付けられているQSelectionModelselectionModel()を用いて
ひっこぬいてきてQFileSytemModel::filePath()でファイルパスに変換し、paths
登録していきます。

QFileSystemModel* model = qobject_cast<QFileSystemModel*>(p_itemView->model());
QStringList paths;

if (p_itemView->selectionModel()->hasSelection()) {
    //選択済コンテンツを対象とする。
    QModelIndexList indexList = p_itemView->selectionModel()->selectedIndexes();
    for each (QModelIndex index in indexList)
    {
        if (index.column() == 0) {
            paths.append(model->filePath(index));
        }
    }
}
else {
    //未選択時はカレントターゲットを対象とする。
    if (p_itemView->currentIndex().isValid()) {
        paths.append(model->filePath(p_itemView->currentIndex()));
    }
}

ここまでで、コピー対象ファイルのリストができましたので、TeFilePathDialogを用いて
コピー先のパスを取得します。TeFolderView::currenPath()を設定しているのは、
相対パスに対応するためのです。卓駆っぽいアプリでは内部制御の簡単化のため、
内部引数として利用するパスは、すべてクリーンな絶対パスとしているので、
相対パスを絶対パスに変換するために必要となります。

パスが設定されなかった場合の卓駆の動作はカレントフォルダへのリネームコピーですが、
私としてはあまり便利ではない機能のため、エラー扱いとしています。
ちなみに、エラー表示に用いているQMessageBoxですが、今回はアプリの雰囲気を揃えるために
アイコンを自前で用意したものからロードしていますが、setIcon()を利用することで、
システムのデフォルトのアイコンを用いることもできます。

if (!paths.isEmpty()) {
    //コピー先フォルダ確定
    TeFilePathDialog dlg(p_store->mainWindow());
    dlg.setCurrentPath(p_folder->currentPath());
    dlg.setWindowTitle(TeFilePathDialog::tr("Copy to"));
    if (dlg.exec()) {
        if (dlg.targetPath().isEmpty()) {
            QMessageBox msg(p_store->mainWindow());
            msg.setIconPixmap(QIcon(":TableEngine/warning.png").pixmap(32, 32));
            msg.setText(QObject::tr("Faild CopyTo Function.\nTarget path is not set."));
            msg.exec();
        }
        else {
            copyItems(p_store,paths, dlg.targetPath());
        }
    }
}

お次はやっとIFileOperatonかと思いきや、指定パスの存在有無によって、処理がわかれます。
やたらif分がネストしてしまってドライじゃないコードになっていますが、気にしないほうこうでw

TeAskCreationModeDialogは新規フォルダ生成するか、リネームコピーを実施するかを選択するダイアログです。
本当は1ファイルのみ選択されている場合専用なのですが、デザインが面倒だったので多数のダイアログがあっても
ユーザーが混乱するので、複数選択の場合も同じダイアログを使いまわしてます。

void TeCmdCopyTo::copyItems(TeViewStore* p_store, const QStringList & list, const QString & path)
{
    //指定ディレクトリが存在しない場合作成するか問い合わせる。
    QDir dir;

    bool bSuccess = true;

    if (dir.exists(path)) {
        bSuccess = copyFiles(list, path);
    }
    else {
        QFileInfo info(path);
        if (list.count() == 1 && info.dir().exists()) {
            //Rename or CreateFolder
            TeAskCreationModeDialog dlg(p_store->mainWindow());
            dlg.setTargetPath(path);
            if (dlg.exec()) {
                if (dlg.createMode() == TeAskCreationModeDialog::MODE_RENAME_FILE) {
                    //リネームコピー実行
                    bSuccess = copyFile(list.at(0), path);
                }
                else {
                    //新規フォルダ作成してからコピー
                    bSuccess = dir.mkpath(path);
                    if (bSuccess) {
                        bSuccess = copyFiles(list, path);
                    }
                }
            }
        }
        else {
            TeAskCreationModeDialog dlg(p_store->mainWindow());
            dlg.setTargetPath(path);
            dlg.setModeEnabled(TeAskCreationModeDialog::MODE_RENAME_FILE, false);
            if (dlg.exec()) {
                //Create Folder
                bSuccess = dir.mkpath(path);
                if (bSuccess) {
                    bSuccess = copyFiles(list, path);
                }
            }
        }
    }

    if (!bSuccess) {
        QMessageBox msg(p_store->mainWindow());
        msg.setIconPixmap(QIcon(":TableEngine/warning.png").pixmap(32, 32));
        msg.setText(QObject::tr("Copy to following path failed.") + QString("\n") + path);
        msg.exec();
    }
}

さてやっと辿りついたコピー処理本体。Microsoftのページに記載されているのをそのまま書けば動きます。
かなりざっくりした説明ですが、IFileOperationはほんとパス名を列挙してつっこめば、あと動いちゃうんですね。
簡単ですね。

一応説明しておくと、

  1. CoCreateInstance()IFileOperationを取得。
  2. SHCreateItemFromParsingName()を用いて、フォルダ構造にパスを分割。
    どうやら、この時点でフォルダの有無がすでにチェックされているっぽい。 pathはセパレータとして/を用いているので、QDir::toNativeSeparators()\に変換しておく。 SHCreateItemFromParsingName()の引数LPCWSTRはUTF-16のポインタなので、path.utf16()で UTF-16型に変換した後キャストしている。
  3. 作成したパスをIFileOperation::CopyItem()でコピー対象として登録していく。
    フォルダとファイル名は別腹らしく、下記の例では、ファイル名としてNULLを設定(第3引数ね)しているが、 コピー時にファイル名を変更したい場合は第3引数に別途ファイル名を記載する必要があります。
  4. 最後にIFileOperation::PerformOperations()を実施すると見慣れたダイアログによるコピーが実施されます。

CoInitialize()はスレッド毎にかならず実行しておこう。
アプリメインスレッドだとなんか呼ばなくても動いていたけど説明書には呼べと書かれているので忘れるな!

HRESULT hr = S_OK;
IFileOperation *pfo;

// Create the IFileOperation interface 
hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pfo));
IShellItem *psiTo = NULL;
//コピー先パス設定
hr = SHCreateItemFromParsingName(
    (LPCWSTR)QDir::toNativeSeparators(path).utf16(), NULL, IID_PPV_ARGS(&psiTo));

for each(QString from in files) {
    IShellItem *psiFrom = NULL;
    //コピー元ファイルパス群設定
    hr = SHCreateItemFromParsingName(
        (LPCWSTR)QDir::toNativeSeparators(from).utf16(), NULL, IID_PPV_ARGS(&psiFrom));

    //コピー登録して、コピー元開放
    hr = pfo->CopyItem(psiFrom, psiTo, NULL, NULL);
    psiFrom->Release();

    if (FAILED(hr)) break;
}

//コピー先パス開放
psiTo->Release();

//コピー処理実施
hr = pfo->PerformOperations();

// IFileOperation開放
pfo->Release();

あまり面白くないので、記載しませんでしたが、QKeyEventから取得した、キー種別を
コマンドIDに変換して、CommandFactory経由で上記のインスタンスを作成するコードも別途つくってあるので

  • OK キーボードからのコマンド直接実行機能を持たせる ("a"で全選択とか、"e"で編集など)
  • OK ファイルの一括操作コマンドを装備
  • OK ファイルコピーもWindows Shellを利用予定 (最近のエクスプローラのファイルコピーがお気に入り♪)

となって、卓駆っぽさに残すところあと2つ。

  • NG 圧縮ファイル操作を装備
  • NG テキスト / 画像の簡易ビューアー装備

両方とも面倒ではありますが、だいたい枠組みとなる技術はそろってきたので、あとはがんばってコマンド拡張
していけばなんとかなりそうです。

次は卓駆っぽさとは関係ありませんが、設定ファイル対応とUnitテスト対応をしていきたいと思います。
完成はいつになるのだろうか(^^;)

5
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5