Edited at

WPF TreeViewを使ってみた(2)

More than 1 year has passed since last update.

前回はTreeViewの簡単な実装だけやってみた

今回は要素の取得まで実装してみたが、

取得方法を調べたり考えたりしてたら前回の実装方法と大分変わってしまった


作るもの

前回と同じような感じだが、エクスプローラー的なものにしてみる

2017-04-28_23h25_10.png


作ってみる

前回同様MVVM的に作る

※ReactivePropertyを使用

(参考:かずきのBlog@hatena/ReactiveProperty オーバービュー)


考えてみる

まずTreeViewのSelectedItemとかにプロパティをBindすればいいじゃん、と思ったら読み取り専用でBindできないことが判明

それじゃどうするか

TreeViewItemを継承させてSelectedイベントに選択したものを取得する処理を追加したらいいのでは?

ってことでやってみる


Model

まずはModel

といっても今回はこれがほぼすべて

public class Model_TreeViewItem:TreeViewItem

{
public DirectoryInfo _Directory { get; set; }
private bool _Expanded { get; set; } = false;
public ReactiveProperty<Model_TreeViewItem> _SelectionItem { get; set; } = new ReactiveProperty<Model_TreeViewItem>();

public Model_TreeViewItem(string path)
{
this._Directory = new DirectoryInfo(path);
if (_Directory.GetDirectories().Count() > 0)
{
this.Items.Add(new TreeViewItem());
this.Expanded += Model_TreeViewItem_Expanded;
}
this.Header = CreateHeader();
this.Selected += Model_TreeViewItem_Selected;
}

private void Model_TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
if (!_Expanded)
{
this.Items.Clear();
foreach (DirectoryInfo dir in _Directory.GetDirectories())
{
if (dir.Attributes == FileAttributes.Directory)
{
this.Items.Add(new Model_TreeViewItem(dir.FullName));
}
}
_Expanded = true;
}
}

private StackPanel CreateHeader()
{
StackPanel sp = new StackPanel() { Orientation = Orientation.Horizontal };
sp.Children.Add(new Image()
{
Source = new BitmapImage(new Uri(@"Resources\Folder.ico", UriKind.Relative)),
Width = 15,
Height = 18,
});
sp.Children.Add(new TextBlock() { Text = _Directory.Name });
return sp;
}

private void Model_TreeViewItem_Selected(object sender, RoutedEventArgs e)
{
_SelectionItem.Value = (this.IsSelected) ? this : (Model_TreeViewItem)e.Source ;
}
}

いきなり長めのコードになってしまったので細かく解説する

前述したようにTreeViewItemを継承している

プロパティは以下の通り


_Directory

自身のフォルダ



_Expanded

一度でも展開したかどうか



_SelectionItem

選択しているオブジェクト



まず、コンストラクタで渡されたパスをもとにDirectoryInfoを作って、そのフォルダにサブフォルダがあるかを調べている

一度にすべてのフォルダを走査して子要素を追加すると処理に時間がかかってしまうのでここでは1階層下のもののみ走査している

子要素があるフォルダになにも追加しないと展開する▷マークが表示されないので、サブフォルダがあるものはItemsにダミーのTreeItemを追加している

そんでExpandedイベントでサブフォルダを取得してそれぞれをnewした自身のクラスをItemに追加している

今回はエクスプローラー風にフォルダ名の左にアイコンをつけたかったのでHeaderプロパティにStackPanelをセットしている

詳細は省く

あと、Selectedイベント

こいつが曲者でハマってしまった

こいつは子要素側で発生しても親要素に伝播して親要素でも同じイベントが発生する

なので単純に(Model_TreeViewItem)e.Sourceとやっただけでは取得できない

考えた結果、この伝播する動作を利用してルートの親要素にイベント発生源のオブジェクトを運ぶことにした

_SelectionItem.Value = (this.IsSelected) ? this : (Model_TreeViewItem)e.Source ;

では、_SelectionItemプロパティに、自身が選択状態であれば自身を、そうでなければイベント発生源をセットしている

これでルートの_SelectionItemプロパティにBindすれば選択しているオブジェクトが取得できるようになった


ViewModel

シンプル

ただのViewModel

Modelのコンストラクタにパスを渡してるだけ

    class ViewModel

{
public List<Model_TreeViewItem> VM { get; }

public ViewModel()
{
VM = new List<Model_TreeViewItem>()
{
new Model_TreeViewItem(@"C:\Users\yoshiki\Desktop\てすと")
};
}
}


View

Modelのおかげでこんだけで済む

        <TreeView x:Name="treeView" ItemsSource="{Binding VM}">

選択中のDirectoryはこんな感じでBindできる

<TextBlock Text="{Binding VM/_SelectionItem.Value._Directory.Name}" />


おまけ

ListViewにファイル一覧をアイコン付きで表示してみる場合

Converterを使えばよさげ

_SelectionItem.Value._DirectoryをBindしてConvertメソッドで受け取る

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)

{
DirectoryInfo dir = (DirectoryInfo)value;
List<StackPanel> list = new List<StackPanel>();
foreach(var file in dir.GetFiles())
{
StackPanel sp = new StackPanel() { Orientation = Orientation.Horizontal };
System.Drawing.Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(file.FullName);
sp.Children.Add(new Image()
{
Source= Imaging.CreateBitmapSourceFromHIcon(icon.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()),
Width = 15,
Height = 18,
});
sp.Children.Add(new TextBlock()
{
Text = file.Name
});
list.Add(sp);
icon.Dispose();
}
return list;
}

ざっくり解説すると、受け取ったDirectoryInfo内のファイルを取得し、それぞれの関連付けられたアイコンとファイル名をStackPanelに詰め込んでそれをリストにして返している


まとめ

自分なりに考えて実装してみたら少しややこしくなった

思い付きでやってみたため間違っている可能性があるかも

この実装方法が正しいかはわからないが、Bindしたりするのはかなり楽になったと思う