本サイトでは、アフィリエイト広告およびGoogleアドセンスを利用しています。

WPF TreeView with SelectedPath binding

Windows エクスプローラーが備えているフォルダツリーのようなものを、 WPF を利用している自作アプリケーションで使用したいことがあります。
調べてみるといくつかの実装が見つかりますが、フォルダツリーの実現のためにビューモデルとビューが分離されている本気度の高いものが多く、もう少し手軽なものがないかなと思った次第です。

以前に以下のアドレスで公開されていた方式が割と気に入っていたのですが、現時点においてアクセスが出来なくなってしまいました。
http://khmylov.com/blog/2010/11/wpf-explorer-treeview-with-selectedpath-binding/

ちょっとしたときにはこれで十分だったりしてお気に入り実装だったので、元の記事を自分なりに再解釈してみたいと思います。

はじめに

Windows エクスプローラーが提供しているようなファイルシステムのフォルダツリーを表現するコントロールを作成することを考えます。基本的には TreeView を継承したカスタムの TreeView を作成し、ツリー上で選択されているフォルダのパスを取得・設定が出来る SelectedPath プロパティを持たせたいと思います。

ユーザーがフォルダツリーから、フォルダのノードを選択すると、上部のアドレスバー(のような箇所)のテキストが更新されます。ここでユーザーがアドレスバーに直接アドレスを入力して Enter を押下すると、フォルダツリーの選択も更新されます。

まずは TreeView を継承したクラスを作成します。

public class ExplorerTreeView : TreeView
{
  public string SelectedPath
  {
    get { return (string)GetValue(SelectedPathProperty); }
    set { SetValue(SelectedPathProperty, value); }
  }

  public static readonly DependencyProperty SelectedPathProperty =
    DependencyProperty.Register(SelectedPathPropertyName, typeof(string), typeof(ExplorerTreeView));
}

ここでは SelectedPath プロパティでバインディングを出来るようにしておくための準備も行いました。

実装:フォルダ項目の表示

コントロールがロードされたときに、ユーザーの PC 上のドライブのみが表示されるようにしたいと思います。以下のようにして、最初のドライブノードを作成します。

public void InitExplorer()
{
  Items.Clear();
  foreach( var drive in DriveInfo.GetDrives())
  {
    Items.Add(GenerateDriveNode(drive));
  }
}

private static TreeViewItem GenerateDriveNode(DriveInfo drive)
{
  var item = new TreeViewItem
  {
    Tag = drive,
    Header = drive.ToString()
  };
  item.Items.Add("*");
  return item;
}

GenerateDriveNode メソッドは DriveInfo を引数に取り、 TreeViewItem を生成します。ここでダミーのこの-度を追加していることがポイントです。このようにダミーを追加しておかないと、フォルダの開閉に必要なボタン(昔でいう +/-, 最近では三角形のアイコン)が表示されません。

この InitExplorer メソッドをコンストラクタで呼び出すようにすれば、コントロールが生成されたときにドライブ一覧を表示した状態で表示されます。

次に、 TreeViewItem.ExpandedEvent イベントを処理する実装を記述します。フォルダノードが展開されるときに、サブフォルダの情報をセットします。このイベントハンドラはコンストラクタで追加します。

AddHandler(TreeViewItem.ExpandedEvent, new RoutedEventHandler(OnItemExpanded));

そして OnItemExpanded メソッドは以下のように実装します。

private void OnItemExpanded(object sender, RoutedEventArgs e)
{
  var item = (TreeViewItem)e.OriginalSource;
  item.Items.Clear();
  DirectoryInfo dir;
  if( item.Tag is DriveInfo )
  {
    var drive = (DriveInfo)item.Tag;
    dir = drive.RootDirectory;
  }
  else
  {
    dir = (DirectoryInfo)item.Tag;
  }
  foreach(var sub in dir.GetDirectories())
  {
    if (sub.Attributes.HasFlag(FileAttributes.Hidden))
      continue;
    if (sub.Attributes.HasFlag(FileAttributes.System))
      continue;
    item.Items.Add(GenerateDirectoryNode(sub));
  }
}

ここでシステム属性や隠し属性の付いたものを除外していますが、ユーザーに見えて良いのであればこのチェックを外してください。

GenerateDirectoryNode メソッドは以下のようになります。

private static TreeViewItem GenerateDirectoryNode(DirectoryInfo directory)
{
  var item = new TreeViewItem
  {
    Tag = directory,
    Header = directory.Name,
  };
  item.Items.Add("*");
  return item;
}

ここまでは簡単な手順でした。しかしここまでの内容でドライブの表示、フォルダの開閉イベントにあわせて情報取得・表示まで動作しています。

実装: SelectedPath 同期

次に ツリービューの選択状態と SelectedPath プロパティの同期を実装します。現在のツリービューのアイテム選択にアクセス&変更をするために、以下のような2つのヘルパーメソッドを作成します。

private string GetSelectedPath()
{
  var item = (TreeViewItem)SelectedItem;
  if (item == null)
    return null;
  var driveInfo = item.Tag as DriveInfo;
  if( driveInfo != null )
  {
    return (driveInfo).RootDirectory.FullName;
  }
  var directoryInfo = item.Tag as DirectoryInfo;
  if( directoryInfo != null )
  {
    return directoryInfo.FullName;
  }
  return null;
}
private void SetSelectedPath(string value)
{
  InitExplorer();
  if (string.IsNullOrEmpty(value))
    return;

  var split = Path.GetFullPath(value).Split(
    new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
    StringSplitOptions.RemoveEmptyEntries);
  var drive = new DriveInfo(split[0]);
  foreach(TreeViewItem item in Items)
  {
    var name = ((DriveInfo)item.Tag).Name.ToLower();
    if(name == drive.Name.ToLower())
    {
      if( !Expand(item, 1, split) )
      {
          MessageBox.Show("指定されたディレクトリは存在しません。");
      }
      break;
    }
  }
}
private bool Expand(TreeViewItem item, int index, string[] pathParts )
{
  if (index > pathParts.Length)
    return false;
  if( index == pathParts.Length )
  {
    item.IsSelected = true;
    return true;
  }
  if(!item.IsExpanded)
  {
    item.IsExpanded = true;
  }
  var name = pathParts[index].ToLower();
  foreach(TreeViewItem folderItem in item.Items)
  {
    var directoryInfo = (DirectoryInfo)folderItem.Tag;
    if(directoryInfo.Name.ToLower() == name )
    {
        Expand(folderItem, index + 1, pathParts);
        return true;
    }
  }
  return false;
}

GetSelectedPath メソッドは、選択した項目から DriveInfo や DirectoryInfo を取得してフルパスの情報を返します。
SetSelectedPath メソッドは、引数に指定されたターゲットパスをフォルダの配列に分解し、順番にフォルダを開いていきます。
ツリービューの選択の仕組み上、いくつかの問題があり、正しく項目を選択できなかったため、 InitExplorer メソッドを最初に呼ぶようにしました。コードで SelectedPath を設定することは一般的なタスクではないこと、遅延ロードを使用することでうまく機能します。

SelectedItemChanged イベントについてハンドラを追記しておきます。

SelectedItemChanged += (s,e) => SelectedPath = GetSelectedPath();

SelectedPath の変更を処理して、 UI の更新を行うコードを追加します。

protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
  base.OnPropertyChanged(e);
  if( e.Property == SelectedPathProperty )
  {
    var newValue = (string)e.NewValue;
    if( IsSelectionUpdateRequired(newValue))
    {
      SetSelectedPath(newValue);
    }
  }
}
private bool IsSelectionUpdateRequired(string newPath)
{
  if (string.IsNullOrEmpty(newPath))
    return true;
  var selectedPath = GetSelectedPath();
  if (string.IsNullOrEmpty(selectedPath))
    return true;
  return !Path.GetFullPath(newPath).Equals(Path.GetFullPath(selectedPath));
}

ここで IsSelectionUpdateRequired メソッドについて説明しておきます。
これは無限ループ(ユーザーがノードを選択 ⇒ SelectedPath が変更 ⇒ TreeView.SelectedItem が変更 ⇒ SelectedPath 変更 ⇒ … )というループを避けるためのものです。また、不要な計算を防止するために使用します。

サンプルコード

作成した ExplorerTreeView を使用するコードを作成します。以下のような xaml を作成します。


  
    
    
  
  
  

テキストボックスで Enter を入力したときに反映させるために、以下のようなコードを実装します。

private void TxtPath_KeyUp(object sender, KeyEventArgs e)
{
  if( e.Key == Key.Enter )
  {
    var binding = txtPath.GetBindingExpression(TextBox.TextProperty);
    binding?.UpdateSource();
  }
}

このメソッドをテキストボックスのイベントハンドラとして呼び出されるようにセット “txtPath.KeyUp += TxtPath_KeyUp;” しておきます。
MainWindowViewModel 等で用意した SelectedFilePath プロパティとバインディングしておいて動作を確認します。

まとめ

割と簡単な手順で SelectedPath プロパティを備えたフォルダ用のツリービューが作成できることを示しました。
SetSelectedPath で InitExplorer() メソッドが呼び出されていたりと、気になる点はありますが簡単なものにはこれで十分でしょう。テキストボックスでパスを入力したら、先ほどまで開いていた別のツリー構造は閉じてしまうという点が個人的に気になりました。

フォルダの状態が変わらない限りは、1度 InitExplorer を呼びだしておけば、毎回 SetSelectedPath で呼び出す必要はありません。また、この SetSelectedPath はフォルダをマウスで選択した時には呼び出されません。テキストボックスでの値入力のときに呼び出されます。これは IsSelectionUpdateRequired メソッドによって更新の必要性が判定されているために、このような動きとなります。

プログラミング
すらりんをフォローする
すらりん日記

コメント

タイトルとURLをコピーしました