LoginSignup
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-05-21

要求スペックの再確認

前回でとりあえず見た目は卓駆っぽいファイラーに近づいてきましたが、さすがにまだまだ要求スペックには
程遠いですね。そこで今回はMainWindowの追加(メユーやステータスバーですね)と、ビュー内の挙動を調整していきましょう。

卓駆っぽさ

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

追加で欲しい機能

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

その1 MainWindowの追加

通常ならここでQtDesignerなりQtCreatorの登場してメニューやツールバーを作っていくのですが、面倒なので直接記載しちゃいましょう。

今回はいきなし最終形から。見た目の確認なので、タブと2ペイン表示も実現してみました。
スコープ解決が面倒なので、今回はQMainWindow派生クラスで実装です。

MainWindow.png

QtFileListTest::QtFileListTest(QWidget *parent)
    : QMainWindow(parent)
{
    QMenu *menu = menuBar()->addMenu(tr("&File"));
    menu->addAction(new QAction(tr("&New")));

    QFileIconProvider provider;
    QToolBar *bar = addToolBar(tr("DriveBar"));
    bar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    bar->addAction(new QAction(provider.icon(QFileIconProvider::Drive), "D"));
    bar->addAction(new QAction(provider.icon(QFileIconProvider::Drive), "F"));
    bar->setIconSize(QSize(14, 14));

    QLabel *labelR = new QLabel(u8"Right Text");
    statusBar()->addPermanentWidget(labelR);
    QLabel *labelL = new QLabel(u8"Left Text");
    statusBar()->addWidget(labelL);

    QFileSystemModel *model = new QFileSystemModel();
    model->setRootPath("C:/");
    model->setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
    QTreeView *tree = new QTreeView();
    tree->setModel(model);
    for (int i = 1; i<tree->header()->count(); i++) {
        tree->header()->setSectionHidden(i, true);
    }
    tree->setHeaderHidden(true);
    tree->setCurrentIndex(model->index("C:/Media/Picture/100MSDCF"));

    QFileSystemModel *models[4];
    QListView *lists[4];

    for(int i=0; i<4; i++){
        models[i] = new QFileSystemModel();

        lists[i] = new QListView();
        lists[i]->setModel(models[i]);
        lists[i]->setRootIndex(models[i]->setRootPath("C:/Media/Picture/100MSDCF"));
        lists[i]->setViewMode(QListView::ListMode);
        lists[i]->setWrapping(true);
        lists[i]->setResizeMode(QListView::Adjust);
    }

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

    QTabWidget *tab = new QTabWidget();
    tab->setMovable(true);
    tab->addTab(lists[0], "Tab1");
    tab->addTab(lists[1], "Tab2");
    tab->setTabBarAutoHide(true);

    QTabWidget *tab2 = new QTabWidget();
    tab2->addTab(lists[2], "Tab3");
    tab2->addTab(lists[3], "Tab4");

    QHBoxLayout *hbox = new QHBoxLayout();
    hbox->setMargin(0);
    hbox->addWidget(tab);
    hbox->addWidget(tab2);


    QWidget *widget = new QWidget();
    widget->setLayout(hbox);

    split->addWidget(tree);
    split->addWidget(widget);

    this->setCentralWidget(split);
}

ひとつずつ説明していくと、

メニューバー

MainWindowMenu.png

赤く囲ってあるのがQMenuで、黄色で囲ってあるのがQAction。

"&"を付けると次の文字がアクセサキーに登録されます。下記の登録方法だと"ALT+F"でFileメニューが開き、
続いて"N"を押すとNewが実行されるという動きになります。

ちなみに文字列を"tr()"で囲っておくと、あとからQt Linguistを使って文字列の置き換えをすることができるようになります。国際化対応ですね。直接日本語で書きたい場合はu8"ファイル(&F)"みたいに表記してください。
これでUTF-8で設定できます。※VisualStudioでu8修飾しない文字列はShift-jisに変換されちゃいます。

QMenu *menu = menuBar()->addMenu(tr("&File"));
menu->addAction(new QAction(tr("&New")));

上記のコードはmenuBar()によって、MainWindowのメニューバーを取得。addMenu()で"File"メニューを追加。
addMenu()の返り値から取得QMenuポインタを通じて、addAction()することで、メニュー項目"New"を追加しています。今回は見栄えを確認したいだけなので、Actionになにも登録していませんが、connectを用いて処理を
登録すれば、ちゃんと反応してくれます。

メニューの中にもう一階層深いメニューを入れたい場合はaddMenu(tr("2ndMenu"))とすれば新しいメニュー階層
を作成することができます。

ツールバー

MainWindowTool.png

ツールバーの設定は割と簡単で、addToolBar()で好きなだけツールバーを追加することができます。
この時点でフローティングバーにも対応していますので、マウスで自分の好きな場所に再配置することも可能です。

QFileIconProvider provider;
QToolBar *bar = addToolBar(tr("DriveBar"));
bar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
bar->addAction(new QAction(provider.icon(QFileIconProvider::Drive), "D"));
bar->addAction(new QAction(provider.icon(QFileIconProvider::Drive), "F"));
bar->setIconSize(QSize(14, 14));

ツールバーにはアイコンを設定する必要があります。これにはQFileIconProviderを利用することができます。
下記のコードでは、直接アイコンの種別を指定していますが、QFileInfoを引数とすることで、
ファイル種別に対応したアイコンを設定することもできます。

QFileIconProvider provider;
provider.icon(QFileIconProvider::Drive), "D");

実際にドライブボタンを追加しているが以下のコードです。

bar->addAction(new QAction(provider.icon(QFileIconProvider::Drive), "D"));

デフォルトではアイコンのみ表示になっているのですが、卓駆っぽいドライブバーはドライブレターも
表示するので、setToolButtonStyle(Qt::ToolButtonTextBesideIcon)を利用してテキストも表示するように
ツールバーの設定を変更しています。またアイコンのサイズも卓駆っぽさを増すためにsetIconSize(QSize(14, 14))で小さめに表示させています。

ステータスバー

MainWindowStatus.png

ステータスバー自体はMainWindowに最初から表示されています。これに情報を追加するには例によってViewを
追加すればいいのですが、追加方法が2通りあります。

1つはステータスバーに右詰めで表示する方法で以下の通り

QLabel *labelR = new QLabel(u8"Right Text");
statusBar()->addPermanentWidget(labelR);

2つめはステータスバーに左詰めで表示する方法で何時ものaddWidget()を利用する以下の方式

QLabel *labelL = new QLabel(u8"Left Text");
statusBar()->addWidget(labelL);

ちなみに、statusBar()->showMessage("Hello")とするとステータスバーに簡単に文字(この場合は"Hello")を
表示できるのですが、addWidget()で追加されたViewだとメッセージ表示中はメッセージに隠されてしまうが、
addPermanentWidget()を利用したViewは消えないという違いもあります。
表示したメッセージはstatusBar()->clearMessage()で消すことができます。

タブ

MainWindowTab.png

卓駆っぽさとは関係ないのですが、使えたら便利そうってことでタブも追加してみました。
マウスのドラッグ&ドロップ操作でのコピー時にはファイルリストを並べて表示したくなる時もあるので、
2分割表示にも対応です。制御が面倒になるので2分割以上は禁止の方向で。

まずは、タブへのListViewの登録。利用する関数がaddWidget()からaddTab()に変るだけで
通常のView登録と同じです。登録する際にタブに表示する文字列を一緒に設定します。
setTabBarAutoHide(true)と設定すると、タブに登録されているViewが1つになった時点でタブバー部分が
非表示になり、通常のViewと同じ表示になってくれます。

QTabWidget *tab = new QTabWidget();
tab->setMovable(true);
tab->addTab(lists[0], "Tab1");
tab->addTab(lists[1], "Tab2");
tab->setTabBarAutoHide(true);

お次は、2分割ビューの実現部分。今回はSplitterではなく、HBoxLayoutを利用しています。
こちらは、Splitterのように分割サイズをマウスで変更することはできませんが、簡単に等分割表示ができます。

Layoutに登録しただけでは表示できないので、最後に表示先を指定するためにQWidgetを作成して、
そこにsetLayout()でLayoutに登録されたViewを表示させています。

QHBoxLayout *hbox = new QHBoxLayout();
hbox->setMargin(0);
hbox->addWidget(tab);
hbox->addWidget(tab2);

QWidget *widget = new QWidget();
widget->setLayout(hbox);

その2 右クリック対応

卓駆っぽいファイラーを目指すには2種類のコンテキストメニューに対応する必要があります。

  1. 卓駆独自のコンテキストメニュー(ファイル/フォルダが存在しないところをクリックすると出る)
  2. Windowsが提供するコンテキストメニュー(ファイル/フォルダをクリックすると出る)

卓駆独自コンテキストを実現するには、Qtの機能を利用すればメニューと同じ感覚で作ることができますので、
さくっと作ってしまいましょう。
右クリックのイベントを取得したいので、TreeView/ListViewで派生クラスを作成します。
右クリックに必要な実装は両方のViewの派生元であるAbstractItemViewにあり、どちらも同じ実装に
なりますので、今回はListViewだけ書いてみます。

基本はメニューを作成したときと同じで、自分で作成したQMenuにActionやMenuを登録して最後にexecで
表示させるところのみが差分となります。

私も最初嵌りましたが、関数内で同期実行させたい時はpopupじゃなくてexecを利用します。

class TeListView : public QListView
{
public:
    TeListView();
    virtual ~TeListView();
protected:
    virtual void mouseReleaseEvent(QMouseEvent *event);

};

TeListView::TeListView() {}
TeListView::~TeListView(){}

void TeListView::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::RightButton) {
        QMenu cmenu;
        cmenu.addAction(new QAction(tr("Context1")));
        cmenu.addAction(new QAction(tr("Context1")));
        cmenu.addAction(new QAction(tr("Context3")));
        cmenu.exec(event->globalPos());
    }
    else {
        QListView::mouseReleaseEvent(event);
    }
}

MainWindowPopup.png

クラスの派生を行っているので多少長くなってしまいましたが、重要なのはmouseReleaseEventの中身だけです。
自分が扱わない部分は親クラスに任せましょう。

ここまでは簡単なのですが、次のWindowsが提供するコンテキストメニューはやや厄介です。
というのも、こちらはWindows Shellを呼びだすのですが、Windowメッセージを処理する必要があるため、
Qtとは別枠で、Win32API製のWindowを作ってあげる必要があります。

コンテキストメニューを出すだけのコードを以下に示しますが、いきなり今迄書いたコードより長いですw

COMを利用しているのため、コードの適当な所であらかじめCoInitializeしておく必要があります。
とりあえず、MainWindowのコンストラクタとデストラクタに該当コードを記載しておきましょう。

TeListView::TeListView() { CoInitialize(NULL); }
TeListView::~TeListView() { CoUninitialize(); }

コンテキストメニューを出すコードはExternal Windowsさんからパクッてきた(^^; コードをQtから呼び出せるように改変して以下のようになりました。

#include <Windows.h>
#include <commctrl.h>
#include <Shlobj.h>

QMap<HWND,IContextMenu2*> g_context;

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (uMsg == WM_INITMENUPOPUP) {
        IContextMenu2* pContextMenu2 = g_context.find(hwnd).value();
        if (pContextMenu2 != NULL) {
            pContextMenu2->HandleMenuMsg(uMsg, wParam, lParam);
            return 0;
        }
    }
    return DefWindowProc(hwnd,uMsg,wParam,lParam);
}

void showWindowsContext( const QString path )
{
    QString rpath = path;
    rpath.replace(u8"/", u8"\\");
    PIDLIST_ABSOLUTE pidlAbsolute = ILCreateFromPath(reinterpret_cast<LPCWSTR>(rpath.utf16()));

    static QString cname;
    if (cname.isEmpty()) {
        cname = u8"TableEngineWindowsContext";
        WNDCLASSEX wc;
        wc.cbSize = sizeof(WNDCLASSEX);
        wc.style = 0;
        wc.lpfnWndProc = WindowProc;
        wc.cbClsExtra = 0;
        wc.cbWndExtra = 0;
        wc.hInstance = static_cast<HINSTANCE>(GetModuleHandle(0));
        wc.hCursor = 0;
        wc.hbrBackground = 0;
        wc.hIcon = 0;
        wc.hIconSm = 0;
        wc.lpszMenuName = 0;
        wc.lpszClassName = reinterpret_cast<LPCWSTR>(cname.utf16());
        RegisterClassEx(&wc);
    }

    HWND hwnd = CreateWindowEx(0, reinterpret_cast<LPCWSTR>(cname.utf16()),
        L"ContextWindow", WS_OVERLAPPED,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        HWND_MESSAGE, NULL, static_cast<HINSTANCE>(GetModuleHandle(0)), NULL);


    int                 nId;
    HRESULT             hr;
    POINT               pt;
    HMENU               hmenuPopup;
    IContextMenu        *pContextMenu = NULL;
    IShellFolder        *pShellFolder = NULL;
    PITEMID_CHILD       pidlChild;

    SHBindToParent(pidlAbsolute, IID_PPV_ARGS(&pShellFolder), NULL);
    pidlChild = ILFindLastID(pidlAbsolute);

    hr = pShellFolder->GetUIObjectOf(NULL, 1, (LPCITEMIDLIST *)&pidlChild, IID_IContextMenu, NULL, (void **)&pContextMenu);
    IContextMenu2 *pContextMenu2 = NULL;
    if (hr == S_OK) {
        hr = pContextMenu->QueryInterface(IID_PPV_ARGS(&pContextMenu2));
        pContextMenu->Release();
        g_context.insert(hwnd, pContextMenu2);
    }

    if (hr == S_OK) {
        hmenuPopup = CreatePopupMenu();
        pContextMenu2->QueryContextMenu(hmenuPopup, 0, 1, 0x7fff, CMF_NORMAL);

        GetCursorPos(&pt);
        nId = TrackPopupMenu(hmenuPopup, TPM_RETURNCMD, pt.x, pt.y, 0, hwnd, NULL);
    }

    if (nId != 0) {
    CMINVOKECOMMANDINFO ici;
        ici.cbSize = sizeof(CMINVOKECOMMANDINFO);
        ici.fMask = 0;
        ici.hwnd = hwnd;
        ici.lpVerb = (LPCSTR)MAKEINTRESOURCE(nId - 1);
        ici.lpParameters = NULL;
        ici.lpDirectory = NULL;
        ici.nShow = SW_SHOW;

        hr = pContextMenu2->InvokeCommand(&ici);
    }

    if (pContextMenu2 != NULL) {
        g_context.erase(g_context.find(hwnd));
        pContextMenu2->Release();
        pContextMenu2 = NULL;
    }
    if(pShellFolder!=NULL)pShellFolder->Release();

    DestroyWindow(hwnd);
    ILFree(pidlAbsolute);
}

MainWindowPopupWin.png

長いですが、ずっと使えるのでご安心ください。

一つずつ説明していきましょう。
最初はWindowsコンテキストメニューを表示するためのメッセージプロシージャ関数です。
たぶん、"送る"などの、子Popupメニューを表示するのに使っていると思われる(謎)。
これを処理するためにWin32APIのウィンドウが必要になり、ウィンドウを作成するためにコードが長くなり...
という負のスパイラルに落ちた結果前述の長いコードが生れています。

g_contextをわざわざグローバルに宣言しているのは、WindowProcから読んでメッセージを
処理させないといけないからなんですね。一応複数表示できるようにHWNDをキーにして
対応するIContextMenu2を引いていますが、たぶんここまでがんばらなくても通常は大丈夫なはず。

QMap<HWND,IContextMenu2*> g_context;

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (uMsg == WM_INITMENUPOPUP) {
        IContextMenu2* pContextMenu2 = g_context.find(hwnd).value();
        if (pContextMenu2 != NULL) {
            pContextMenu2->HandleMenuMsg(uMsg, wParam, lParam);
            return 0;
        }
    }
    return DefWindowProc(hwnd,uMsg,wParam,lParam);
}

お次は、パス名のセパレータをQt標準の"/"から、Windows標準の"\"に変更しています。
決め打ちで書いていますが、気にしないことにしましょうw

ILCreateFromPathでWindows Shellが内部管理で使っているアイテムリスト形式にパス名を変換するのも
忘れてはいけません。Windows Shellでは、デスクトップやらライブラリやらの仮想フォルダ構造を
利用しているためか、ファイルパスを直接使うことはないようです。

QString rpath = path;
rpath.replace(u8"/", u8"\\");
PIDLIST_ABSOLUTE pidlAbsolute = ILCreateFromPath(reinterpret_cast<LPCWSTR>(rpath.utf16()));

コードを無駄に長くしている元凶その2のウィンドウ作成部分です。ここで作成しているウィンドウは
コンテキストメニューを出すためのメッセージディスパッチを利用するためのみに利用するため、
不可視属性になっています。よって、コンテキストメニュー表示時に作成したウィンドウが表示されることは
ありません。

一般的なウィンドウ作成コードなので、要点だけ説明すると、最初のif分の中で、ウィンドウ作成用の
ウィンドウクラスをWindowsに登録しています。登録したウィンドウクラスは名前で管理されるので、
cnameで登録したウィンドウクラス名を覚えておきます。staticで定義されているので最初の一回目で
登録してあとは使いまわす仕組みになっています。わざわざウィンドウクラスを作成しているのは、
wc.lpfnWndProc = WindowProcで自前のウィンドウプロシージャを使えるようにするためです。

ウィンドウクラスが登録できたら、CreateWindowExでウィンドウを作成すれば完了です。
WindowStyleにWS_OVERLAPPEDしか設定していないため、このウィンドウは表示されません。
HWND_MESSAGEとしておくと、メッセージ専用ウィンドウとなるっぽいです。
ちなみにここの部分はQtのWindows用plugin実装のソースコード部分から必要なところをパクッてきていますw

static QString cname;
if (cname.isEmpty()) {
    cname = u8"TableEngineWindowsContext";
    WNDCLASSEX wc;
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = 0;
    wc.lpfnWndProc = WindowProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = static_cast<HINSTANCE>(GetModuleHandle(0));
    wc.hCursor = 0;
    wc.hbrBackground = 0;
    wc.hIcon = 0;
    wc.hIconSm = 0;
    wc.lpszMenuName = 0;
    wc.lpszClassName = reinterpret_cast<LPCWSTR>(cname.utf16());
    RegisterClassEx(&wc);
}

HWND hwnd = CreateWindowEx(0, reinterpret_cast<LPCWSTR>(cname.utf16()),
    L"ContextWindow", WS_OVERLAPPED,
    CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT,
    HWND_MESSAGE, NULL, static_cast<HINSTANCE>(GetModuleHandle(0)), NULL);

ここから先がいよいよEternalWindows様からのパクりコードです。
まずは、SHBindToParentを用いてIShellFolderインターフェイスを取得、
ILFindLastIDでコンテキストメニューを表示したいアイテムをアイテムリスト配列から取得して、
GetUIObjectOfをつかってゲットという流れになります。IID_IContextMenuの部分がコンテキストメニュー
ほしいぜ!という宣言です。

SHBindToParent(pidlAbsolute, IID_PPV_ARGS(&pShellFolder), NULL);
pidlChild = ILFindLastID(pidlAbsolute);
hr = pShellFolder->GetUIObjectOf(NULL, 1, (LPCITEMIDLIST *)&pidlChild, IID_IContextMenu, NULL, (void **)&pContextMenu);

無事コンテキストメニューが取得できたらと言って安心してはいけません。"送る"メニューに対応するために
QueryInterfaceを利用して、IContextMenu2にアップグレードしてあげる必要があります。
面倒ですね(^^;

アップグレードしたらpContextMenuはもう用無しなので、さっさとReleaseで消してしまいましょう。
取得したpContextMenu2はウィンドウプロシージャからも利用するため
g_context.insert(hwnd, pContextMenu2)でグローバル変数に登録しておきます。

IContextMenu2 *pContextMenu2 = NULL;
if (hr == S_OK) {
    hr = pContextMenu->QueryInterface(IID_PPV_ARGS(&pContextMenu2));
    pContextMenu->Release();
    g_context.insert(hwnd, pContextMenu2);
}

やっとのことで、コンテキストメニューを作ってくれる人をゲットできたので、
今度は実際にポップアップメニューへ登録する作業が発生します。(もうあとは表示するだけだと思ったあなた! あまいぞ!)

といってもここからは、さほど面倒ではなくて、CreatePopupMenuで作成されるポップアップメニューに
QueryContextMenuを利用して、アイテムを登録。GetCursorPosでマウスの位置を取得してTrackPopupMenu
で表示だ。TrackPopupMenuに渡すhwndを設定するために面倒なコードをがんばって書いていたとも言える。

QueryContextMenuでメニューを登録する場所は自由に選べるので、自分の作ったポップアップメユーの
適当な場所にコンテキストメニューを挿入することも可能なはずだ(やったことないけどw)。

ポップアップメニューで、なにか選択されら場合はTrackPopupMenuの返り値が0以上の値になるので覚えておく。

if (hr == S_OK) {
    hmenuPopup = CreatePopupMenu();
    pContextMenu2->QueryContextMenu(hmenuPopup, 0, 1, 0x7fff, CMF_NORMAL);

    GetCursorPos(&pt);
    nId = TrackPopupMenu(hmenuPopup, TPM_RETURNCMD, pt.x, pt.y, 0, hwnd, NULL);
}

nIdが0以外ならなんか選択されたことになるので、メニュー項目に対応する処理を実行しよう。
それにはCMINVOKECOMMANDINFOを作ってIContextMenu2に実行を依頼するわけだが、
ここで覚えておいてほしいのはici.lpVerb = (LPCSTR)MAKEINTRESOURCE(nId - 1)の部分
1を引いているのは、QueryContextMenuの第三引数で1を指定していたからで、別の値を
設定した人は対応する値を引き算する必要がある。

if (nId != 0) {
    CMINVOKECOMMANDINFO ici;
    ici.cbSize = sizeof(CMINVOKECOMMANDINFO);
    ici.fMask = 0;
    ici.hwnd = hwnd;
    ici.lpVerb = (LPCSTR)MAKEINTRESOURCE(nId - 1);
    ici.lpParameters = NULL;
    ici.lpDirectory = NULL;
    ici.nShow = SW_SHOW;

    hr = pContextMenu2->InvokeCommand(&ici);
}

上記までコンテキストメニューの処理は終了なので、最後に使ったものを片付けて完了です。
長かった....

if (pContextMenu2 != NULL) {
    g_context.erase(g_context.find(hwnd));
    pContextMenu2->Release();
    pContextMenu2 = NULL;
}
if(pShellFolder!=NULL)pShellFolder->Release();

DestroyWindow(hwnd);
ILFree(pidlAbsolute);

作った関数は、以下のようにして利用します。
indexAtで、ListViewやTreeView内のマウスが指しているアイテムのModelIndexを取得し、
存在していたら、FileSystemModelを利用してパス名を取得。
取得したパスを先程作成した関数showWindowsContextに渡せばコンテキストメニューが無事表示される。

index == currentIndex()でガードを入れているのは、マウスの右ボタンを押したまま、
マウスを移動させて、ListView内のアイテムの上でマウスの右ボタンを離すと離したところのアイテムの
コンテキストメニューが表示されて気持ちわるかったので、追加してあります。

void TeListView::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::RightButton) {
        QPersistentModelIndex index = indexAt(event->pos());
        if (index.isValid()) {
            if (index == currentIndex()) {
                QFileSystemModel *fmodel = qobject_cast<QFileSystemModel*>(model());
                showWindowsContext(fmodel->filePath(index));
            }
        }
        else{
            QMenu cmenu;
            cmenu.addAction(new QAction(tr("Context1")));
            cmenu.addAction(new QAction(tr("Context1")));
            cmenu.addAction(new QAction(tr("Context3")));
            cmenu.exec(event->globalPos());
        }
    }
    else {
        QListView::mouseReleaseEvent(event);
    }
}

その3 ダブルクリックでファイルを開く

Windowsのコンテキストメニューに対応したので、右クリックしたらファイルを開くこともできるのだけど、
どうせならダブルクリックでも開いてほしい。
これは簡単で以下のように、選択されているアイテムからファイルパスを取得して、実行すればよい。
実行前にisDirを利用して、フォルダじゃないことを確認しておかないと、フォルダでのダブルクリックで
移動できなくなっちゃうぞ。

ちなみにつかっているmodelの実体は勿論FileSystemModelだ。

ShellExecute(NULL, L"open", 
  reinterpret_cast<LPCWSTR>(model->filePath(index).utf16()), NULL, NULL, SW_SHOWNORMAL);

その4 スペースキーで複数選択したいぞ

私が卓駆じゃないと生きられない一番の理由がこれ! スペースキーで選択!

一番必要なのに要求スペックから外れているのは、もう空気も同然 あってあたり前だからさ!
(忘れてた訳じゃないぞw)

Qtにおんぶにだっこ作戦の本企画としては、まずはQtの機能で複数選択からサポートしてみよう。

lists[i]->setSelectionMode(QAbstractItemView::ExtendedSelection);

はい完了。これで、いままで一個しか選択できなかったのが、Windowsのエクスプローラみたいに
マウスドラッグによる範囲選択や、Shiftキーによる範囲選択、Ctrlキーによる追加選択ができる。
マウスによる選択はとりあえず、これでよいだろう。

お次はキー操作。と、ここでいきなし問題発生。Qtの実装だとキー操作に連動して選択加除されちゃうんですね。
やりたいのは、方向キーでの移動では選択を変更せず、スペースキーで選択だ。

まずは選択解除されないようにしていきましょう。ListViewのselectionCommandをオーバーライドすることで
選択状態のコントロールができるようになります。Qtのデフォルトではカーソルの移動が発生すると
選択解除されるので、対応するキーコマンドを処理した際の選択方式フラグを変更します。

  • Clear : カーソル位置の選択解除
  • Select : カーソル位置を選択
  • NoUpdate: 選択状態維持
  • Toggle : カーソル位置の選択状態反転
  • ClearAndSelect : 全選択を解除して、カーソル位置を選択

※Currentというオプションもあるけど、これは範囲選択用なので気にしない方向で。

のオプションがあるが、今回は選択状態を変化させたくないので、NoUpdateを選ぶ。

他の動作は変えないので、該当イベント以外は全部ListViewのデフォルト実装に任せよう。
後でビックリしないように、元にしているExtendedSelectionでだけ有効となるようガードを入れてみた

QItemSelectionModel::SelectionFlags TeListView::selectionCommand(const QModelIndex &index, const QEvent *event) const
{
    if (event) {
        switch (event->type()) {
        case QEvent::KeyPress: {
            switch (static_cast<const QKeyEvent*>(event)->key()) {
            case Qt::Key_Down:
            case Qt::Key_Up:
            case Qt::Key_Left:
            case Qt::Key_Right:
            case Qt::Key_Home:
            case Qt::Key_End:
            case Qt::Key_PageUp:
            case Qt::Key_PageDown:
                if(selectionMode() == QListView::ExtendedSelection )
                    return QItemSelectionModel::NoUpdate;
            }
            break;

            case Qt::Key_Space:
                if (selectionMode() == QListView::ExtendedSelection)
                    return QItemSelectionModel::Toggle;
        break;

        default:
            break;
        }
    }
    return QListView::selectionCommand(index, event);
}

これでカーソル位置を移動しても選択解除されなくなった。
ついでにスペースキーで選択トグルも実現している。
ただ残念なことに卓駆っぽいスペースキーの操作は選択&カーソル移動だ。
さすがに移動までやるとなるとkeyPressEventをオーバーライドするしかないので、
ここは素直にkeyPressEventをオーバーライドして動作を変えよう。
どうせあとでキーコマンド対応する時にオーバーライドするので、早いか遅いかの差で結果は同じだw

void TeListView::keyPressEvent(QKeyEvent *event)
{
    QListView::keyPressEvent(event);

    QPersistentModelIndex newCurrent;
    Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
    if (event) {
        switch (event->type()) {
        case QEvent::KeyPress: {
            modifiers = static_cast<const QKeyEvent*>(event)->modifiers();
            switch (static_cast<const QKeyEvent*>(event)->key()) {
            case Qt::Key_Space:
                newCurrent = moveCursor(MoveNext, event->modifiers());
                selectionModel()->setCurrentIndex(newCurrent, QItemSelectionModel::NoUpdate);
            }
            break;
        }
        default:
            break;
        }
    }
}

ListViewデフォルトの動作の後にmoveCursorで、カーソル位置を次に進める。
QtのAPI仕様上はこれでうまくいくはずなのだが、ListViewの折り返し表示モードを有効にした場合に
実装不具合があり、折り返し位置で、次の列にカーソルが移動する処理が実現できていないorz。

さすがにこれは悲しいので、自分で実装しておく。幸い次に進むという処理はModel的にはRowCountを
一つ進めるだけなので、currentIndexで取得したModelIndexを1行分下にずらすことで対応しよう。

void TeListView::keyPressEvent(QKeyEvent *event)
{
    QListView::keyPressEvent(event);

    QPersistentModelIndex newCurrent;
    Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
    if (event) {
        switch (event->type()) {
        case QEvent::KeyPress: {
            modifiers = static_cast<const QKeyEvent*>(event)->modifiers();
            switch (static_cast<const QKeyEvent*>(event)->key()) {
            case Qt::Key_Space:

                newCurrent = model()->index(currentIndex().row()+1,currentIndex().column(),rootIndex());
                selectionModel()->setCurrentIndex(newCurrent, QItemSelectionModel::NoUpdate);
            }
            break;
        }
        default:
            break;
        }
    }
}

これでOKかと思いきや、Qtのデフォルト実装だと、マウスクリックがシングル選択となっているため、
このままだとマウスクリック後の一発目のスペースキーで、マウスクリックによる選択を解除してしまって
使いにくい。ここは思いきってCtrlキーやShiftキーなしのマウスクリックは選択動作をさせないようにselectionCommand変更してしまおう。

MouseButtonReleaseにもマスクするのを忘れるな。ListViewはPressとRelease両方でマウスの
クリックを処理しているぞ。

QItemSelectionModel::SelectionFlags TeListView::selectionCommand(const QModelIndex &index, const QEvent *event) const
{
    Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
    if (event) {
        switch (event->type()) {
        case QEvent::MouseButtonRelease:
        case QEvent::MouseButtonPress: {
            modifiers = static_cast<const QMouseEvent*>(event)->modifiers();
            const bool shiftKeyPressed = modifiers & Qt::ShiftModifier;
            const bool controlKeyPressed = modifiers & Qt::ControlModifier;
            if (!shiftKeyPressed && !controlKeyPressed)
                return QItemSelectionModel::NoUpdate;
            break;
        }
        case QEvent::KeyPress: {
            switch (static_cast<const QKeyEvent*>(event)->key()) {
            case Qt::Key_Down:
            case Qt::Key_Up:
            case Qt::Key_Left:
            case Qt::Key_Right:
            case Qt::Key_Home:
            case Qt::Key_End:
            case Qt::Key_PageUp:
            case Qt::Key_PageDown:
                if(selectionMode() == QListView::ExtendedSelection )
                    return QItemSelectionModel::NoUpdate;
            case Qt::Key_Space:
                if (selectionMode() == QListView::ExtendedSelection)
                    return QItemSelectionModel::Toggle;
            }
            break;
        }
        default:
            break;
        }
    }
    return QListView::selectionCommand(index, event);
}

これでかなり卓駆っぽく選択できるようなったはずだ。
アイコンをクリックで選択とか、マウスでの範囲選択はここまでバリエーションゆたかじゃないとか
細かい違いはあるが、便利そうなのでよいことにしよう。

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
6