【C++/WinRT】ファイルの追加・削除を検知する

WinUI3を使用したデスクトップアプリで、指定したフォルダ内のファイルの追加、削除を検知します。

今回は以下の2通りの方法を紹介します。

  1. フォルダ内のファイルを列挙し、ファイル名のリストを保持しておく。
  2. ファイルの追加・削除・変更をトリガーにしたイベントを作成する。

プロジェクトの作成

VisualStudioの新しいプロジェクトの作成から空のアプリ、パッケージ化(デスクトップのWinUI3)のプロジェクトを作成します。

WinUI3のプロジェクトテンプレートを使用する

そのままビルドすると、ボタンのみの画面が表示されます。

初期状態

ここに機能を追加していきます。

1. フォルダ内のファイルを列挙し、ファイル名のリストを保持しておく。

継続的にフォルダ内の状態を取得し、前回の状態を変わっていれば必要な処理を行うということを考えます。まずはフォルダ内のファイルの一覧を取得する機能を追加します。

まずXamlファイルにファイル名を表示するためのTextBlockを追加します。サイズは適当です。

<StackPanel Orientation="Vertical" 
            HorizontalAlignment="Center" 
            VerticalAlignment="Center">
    <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    <!-- TextBlockを追加 -->
    <TextBlock x:Name="myTextBlock" Width="700" Height="400" />
</StackPanel>

ヘッダーとソースも変更します。 非同期処理を含むので、myButton_Clickの返り値をvoidからIAsyncActionに変更しています。

// MainWindow.xaml.h
Windows::Foundation::IAsyncAction myButton_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args);
// MainWindow.xaml.cpp
// 追加
#include "winrt/Windows.Storage.h"
#include "winrt/Windows.Storage.Search.h"

// 省略

Windows::Foundation::IAsyncAction MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
{
    //myButton().Content(box_value(L"Clicked"));

    // テキストファイルのみを対象にする
    auto file_type{ single_threaded_vector<hstring>({L".txt"}) };
    Windows::Storage::Search::QueryOptions options = Windows::Storage::Search::QueryOptions{ Windows::Storage::Search::CommonFileQuery::DefaultQuery, file_type };
    Windows::Storage::StorageFolder target_folder{ co_await Windows::Storage::StorageFolder::GetFolderFromPathAsync(L"C:\\Users\\username\\Documents\\target_dir") };
    Windows::Storage::Search::StorageFileQueryResult query = target_folder.CreateFileQueryWithOptions(options);
    auto files{ co_await query.GetFilesAsync() };

    hstring result{ L"" };
    for (auto file : files) {
        result = result + L"; " + file.Name();
    }

    myTextBlock().Text(result);
}

ボタンがクリックされるたびに、以下の処理を行っています。 - 検索対象の拡張子のリストを作成 - クエリオプションを作成 - 検索対象のフォルダオブジェクトを取得 - フォルダに対してクエリを実行し、結果オブジェクトを取得 - 結果オブジェクトからファイルオブジェクトのリストを取得

今回は単に取得したファイルのファイル名を画面に表示していますが、このリストを保持しておき、前回の結果と比較することでファイルの作成・削除を検知することが可能になります。

2. ファイルの追加・削除・変更をトリガーにしたイベントを作成する。

別の方法としてイベントを使用する方法があります。

上記のコードの中に出てきたStorageFileQueryResultクラスにはContentsChangedイベントが用意されています。

公式ドキュメント > StorageFileQueryResult クラス

このイベントはクエリ対象フォルダのファイル追加・削除・変更を検知することができます。使ってみましょう。 新しくプロジェクトを作成し、追記していきます。

xamlは先ほどと同様に、表示用のTextBlockを追加します。 また、イベントを設定するためのボタンも追加します。

<!-- 再掲です -->
<StackPanel Orientation="Vertical" 
            HorizontalAlignment="Center" 
            VerticalAlignment="Center">
    <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    <Button x:Name="myButton2" Click="myButton2_Click">Set event</Button>
    <!-- TextBlockを追加 -->
    <TextBlock x:Name="myTextBlock" Width="700" Height="400" />
</StackPanel>

ヘッダーとソースを編集します。 先ほどの例と異なるのは、イベントを登録するための関数と、イベントを検知したときに行う処理(デリゲート)を作成する点、そしてイベントを登録するStorageFileQueryResultインスタンスはアプリの起動中は生存している必要があるため、MainWindowクラスのメンバ変数に持たせておきます。

// MainWindow.xaml.h
struct MainWindow : MainWindowT<MainWindow>
{
    void myButton_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args);
    void myButton2_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args);
    
    Windows::Foundation::IAsyncAction SetFileEvent();
    void QueryContentsChanged(Windows::Storage::Search::IStorageQueryResultBase const& sender, IInspectable const& args);

    Windows::Storage::Search::StorageFileQueryResult query_ = { nullptr };
    std::string state_ = { "default" };
};
// 追加
#include "winrt/Windows.Storage.h"
#include "winrt/Windows.Storage.Search.h"

// 中略
Windows::Foundation::IAsyncAction MainWindow::SetFileEvent() {
    auto file_type{ single_threaded_vector<hstring>({L".txt"}) };
    auto options = Windows::Storage::Search::QueryOptions{ Windows::Storage::Search::CommonFileQuery::DefaultQuery, file_type };
    auto target_folder{ co_await Windows::Storage::StorageFolder::GetFolderFromPathAsync(L"C:\\Users\\username\\Documents\\target_dir") };

    query_ = target_folder.CreateFileQueryWithOptions(options);
    // イベントの発生前にGetFilesAsyncを少なくとも1回は呼び出す必要がある
    auto files{ co_await query_.GetFilesAsync() };
    // delegateを渡す
    query_.ContentsChanged({ this,&MainWindow::QueryContentsChanged });
}

void MainWindow::myButton2_Click(IInspectable const&, RoutedEventArgs const&){
    SetFileEvent();
}

void MainWindow::QueryContentsChanged(Windows::Storage::Search::IStorageQueryResultBase const& sender, IInspectable const& args) {
    state_ = "changed";
}

SetFileEvent関数内で、ContentsChangedイベントにQueryContentsChangedデリゲートを渡しています。 開発者自身でデリゲートを作成する際は、ドキュメントを確認し、登録先のイベントに合った引数を持つデリゲートを作成する必要があります。

ContentsChangedが受け取るデリゲートの型はTypedEventHandler<IStorageQueryResultBase,IInspectable>なので、それに一致させるようにMainWindow::QueryContentsChangedを定義しています。

このアプリの実行時の挙動を説明します。 アプリの起動後、Set eventと書かれたボタンを押すことでMainWindow::QueryContentsChangedデリゲートがContentsChangedイベントにセットされます。 この状態でClick Meのボタンを押すと、state_変数の初期値であるdefaultの文字列が表示されます。

対象のフォルダに新たに.txtの拡張子を持つファイルを作成すると、内部でContentsChangedイベントが発火し、state_変数の値が更新されます。 よって、再びClick Meのボタンを押すと、今度はchangedと表示されます。