C#
WPF
VisualStudio

WPF TreeViewを使ってみた(3)

More than 1 year has passed since last update.

前回、エクスプローラー風の画面で選択している要素の取得までできるものを実装してみた

しかし、このままだと初回起動時にしかルートフォルダを指定できない

現実的には「開く」とかのメニューでルートフォルダを変更することが多々あると思う

そこで今回は動的にルートが変更できるようにしてみた


作るもの

こんな感じでメニューの「開く」ボタンからフォルダを選択して、それをルートにTreeViewを変更する

Sample.gif


作ってみる

前回同様MVVM的に作る

※ReactivePropertyを使用

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

※Microsoft.WindowsAPICodePack.Dialogsを使用

(参考:中の下の上の真ん中あたり )


考えてみる

前回作ったModelを流用したい

でも前回のModelにルートフォルダの変更を通知させる機能を持たせるのは無理っぽい、というか持たせたらおかしい

それならルートフォルダのプロパティ持つ新しいModelを作って、プロパティの変更時(PropertyChanged)に前回のModelを変更すればいけそう!

あとルートフォルダのパスは設定値に保持させたら次回起動時に楽そう

ってことでやってみる


Model

まずは前回作ったModel

少し変更


  • コンストラクタで渡された引数が空白の場合と、そのパスにフォルダがない場合と引数がない場合の動作を追加

  • 子要素展開時にアイコンを変更する動作を追加(需要なさそうだから詳細は省く)

public class M_ProjectTree :TreeViewItem

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

public M_ProjectTree(string path)
{
if (!string.IsNullOrWhiteSpace(path))
{
this._Directory = new DirectoryInfo(path);
this.Selected += Model_TreeViewItem_Selected;
if (this._Directory.Exists)
{
this.Header = CreateHeader();
if (_Directory.GetDirectories().Count() > 0)
{
this.Items.Add(new TreeViewItem());
this.Expanded += Model_TreeViewItem_Expanded;
this.Collapsed += M_ProjectTree_Collapsed;
}
}
else
{
this.Header = "フォルダが見つかりません";
}
}
}

public M_ProjectTree()
{
}

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 M_ProjectTree(dir.FullName));
}
}
_Expanded = true;
}
IconChange();
}

private void M_ProjectTree_Collapsed(object sender, RoutedEventArgs e)
{
IconChange();
}

private void IconChange()
{
StackPanel sp = (StackPanel)this.Header;
Image image = (Image)sp.Children[0];
if (this.IsExpanded)
{
image.Source = new BitmapImage(new Uri(@"Images\FolderOpen.ico", UriKind.Relative));
}
else
{
image.Source = new BitmapImage(new Uri(@"Images\Folder.ico", UriKind.Relative));
}
}

private StackPanel CreateHeader()
{
StackPanel sp = new StackPanel() { Orientation = Orientation.Horizontal };
sp.Children.Add(new Image()
{
Source = new BitmapImage(new Uri(@"Images\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 : (M_ProjectTree)e.Source;
}
}

そんで今回新しく作るModel

public class M_Common

{
public ReactiveProperty<string> _WorkingFolderPath { get; set; } = new ReactiveProperty<string>();
public ReactiveCollection<M_ProjectTree> _ProjectTree { get; } = new ReactiveCollection<M_ProjectTree>();
public ReactiveCommand C_ShowFolderSelectDialog { get; } = new ReactiveCommand();

public M_Common()
{
_WorkingFolderPath.PropertyChanged += _WorkingFolderPath_PropertyChanged;
_WorkingFolderPath.Value = Properties.Settings.Default.Working_Directory;
C_ShowFolderSelectDialog.Subscribe(_ => ShowFolderSelectDialog());
}

private void _WorkingFolderPath_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
TreeUpdate();
}

private void TreeUpdate()
{
var prop = Properties.Settings.Default;
prop.Working_Directory = _WorkingFolderPath.Value;
prop.Save();
_ProjectTree.Clear();
_ProjectTree.Add(new M_ProjectTree(_WorkingFolderPath.Value));
}

private void ShowFolderSelectDialog()
{
var folderSelect = new CommonOpenFileDialog() {IsFolderPicker = true };
string appLocation = folderSelect.InitialDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);

if (!string.IsNullOrWhiteSpace(_WorkingFolderPath.Value))
{
DirectoryInfo dir = new DirectoryInfo(_WorkingFolderPath.Value);
folderSelect.InitialDirectory = (dir.Exists) ? dir.Parent.FullName : appLocation;
}
else
{
folderSelect.InitialDirectory = appLocation;
}
var dialogResult = folderSelect.ShowDialog(Application.Current.Windows.OfType<Window>().SingleOrDefault(w => w.IsActive));
if (dialogResult == CommonFileDialogResult.Ok)
{
if (!folderSelect.FileName.Equals(_WorkingFolderPath.Value))
{
_WorkingFolderPath.Value = folderSelect.FileName;
}
}
}
}

まずプロパティは以下の通り


_WorkingFolderPath

ルートにするフォルダのパス



_ProjectTree

前回作ったModel



C_ShowFolderSelectDialog

フォルダを選択するダイアログを出すコマンド



んで、コンストラクタでは


  • _WorkingFolderPathのプロパティ変更時(PropertyChanged)の動作定義

  • _WorkingFolderPathにアプリケーションの設定値からパスを読み込む

  • C_ShowFolderSelectDialog(コマンド)の動作定義

をやっている

_WorkingFolderPathのプロパティ変更時には


  • _WorkingFolderPathの値を設定値に保存

  • _ProjectTreeを新たな_WorkingFolderPathの値で更新

をしている

これで_WorkingFolderPathの値を変更するだけで_ProjectTreeが更新されるようになった

※注意 PropertyChangedは同じ値が入るときには呼ばれない

例えば、設定値から読み込んだパスのフォルダが削除されていて、同じパスで新たにフォルダを作りそのパスを_WorkingFolderPathに入れた時、読み込んだ時の値と同じなのでPropertyChangedが呼ばれずTreeが更新されない現象が発生する

その場合は、新たに値を入れる前に一度空白とかをダミーで入れておくと回避できる

コマンドについては、Microsoft.WindowsAPICodePack.Dialogsを使ってフォルダを選択するダイアログを出している

詳細は省く


ViewModel

なんの変哲もないただのViewModel

初期化でパスを渡す必要もなくなったのでさらにシンプルになった

    class ViewModel

{
public M_Common _Common { get; } = new M_Common();
}


View

TreeViewとMenuItemにバインドするだけ

(バインド部以外省略)

<MenuItem Command="{Binding _Common.C_ShowFolderSelectDialog}"/>

<TreeView ItemsSource="{Binding _Common._ProjectTree}">


まとめ

またまた思いつきでやってみたら案外うまくいった

Modelを持ったModelってどうなの?それはもうViewModelじゃないの?とか思ったけどModelと思い込むことにする

コマンドを増やしていけば「新規作成」とかも簡単に実装できそう