XAMLを使わないWPF入門16「バインドしないTreeView」

コンピュータ

TreeViewをデータソースとバインドしないWinFormsみたいなプログラミングをしてみたいと思います。

実行イメージ
image

ファイル名:NoXAML16TreeView.csproj


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
    <StartUP>NoXAML16TreeView.App</StartUP>
  </PropertyGroup>

</Project>

ファイル名:App.xaml.cs


using System.Windows;

namespace NoXAML16TreeView;

public partial class App : Application
{
    [STAThread]
    public static void Main()
    {
        var app = new App();
        var window = new MainWindow();
        app.Run(window);
    }
}

ファイル名:AssemblyInfo.cs


using System.Windows;

[assembly:ThemeInfo(
    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
                                                //(used if a resource is not found in the page,
                                                // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
                                                //(used if a resource is not found in the page,
                                                // app, or any theme specific resource dictionaries)
)]

ファイル名:DirTreeItem.cs


using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;

namespace NoXAML16TreeView;

public class DirTreeItem : TreeViewItem
{
    public DirTreeItem(string name, string fullName)
    {
        Header = UIHelpers.CreateHeader(name);
        Tag = fullName;
    }
    protected override void OnSelected(RoutedEventArgs e)
    {
        //Debug.Print("Drive Selected");

        // 強制的展開
        this.IsExpanded = true;

        if (this.Items.Count > 0) return;

        string current = (string)this.Tag;

        // サブディレクトリの一覧
        IEnumerable<string> subDirs = Directory.EnumerateDirectories(
            current, "*", SearchOption.TopDirectoryOnly);
        
        foreach(var subDir in subDirs)
        {
            if (FileUtil.IsHiddenOrSystem(subDir)) continue;


            string name = Path.GetFileName(subDir);
            string fullName = subDir;

            var dirTreeItem = new DirTreeItem(name, fullName);

            this.Items.Add(dirTreeItem);
        }
    }
}
/*
構成図: ツリービュー構築のクラス構造

+-----------------------------+
|        MainWindow          |
|  - DirTreeView を配置      |
+-------------+--------------+
              |
              v
+-----------------------------+
|        DirTreeView         |  : TreeView
|  - _root: RootTreeItem     |
|  - CurrentDirectory         |
|  - GetPathHierarchy()       |
|  - FindByTag()              |
+-------------+--------------+
              |
              v
+-----------------------------+
|        RootTreeItem        |  : TreeViewItem
|  - ドライブ一覧を追加       |
+-------------+--------------+
              |
              v
+-----------------------------+
|        DirTreeItem         |  : TreeViewItem
|  - フォルダ一覧を追加       |
+-------------+--------------+

補助クラス:

+-----------------------------+
|         UIHelpers          |
|  - CreateHeader(string)    | ← フォルダ名+アイコン表示
+-----------------------------+

+-----------------------------+
|         FileUtil           |
|  - IsHiddenOrSystem(path)  |
+-----------------------------+
*/


ファイル名:DirTreeView.cs


using System.Windows.Controls;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Windows.Media;
using System.Windows;

namespace NoXAML16TreeView;

public class DirTreeView : TreeView
{
    RootTreeItem _root;
    string _currentDirectory = "";


    public static string[] GetPathHierarchy(string fullPath)
    {
        var list = new List<string>();

        try
        {
            string? current = Path.GetFullPath(fullPath.TrimEnd(Path.DirectorySeparatorChar));
            while (!string.IsNullOrEmpty(current))
            {
                list.Insert(0, current); // 先頭に追加(親→子の順)

                string? parent = Path.GetDirectoryName(current);
                if (parent == null || string.Equals(parent, current, StringComparison.OrdinalIgnoreCase))
                    break;

                current = parent;
            }
        }
        catch
        {
            // 無効なパスなどの処理(必要に応じて)
        }
        list.Insert(0, "__Root__");

        return list.ToArray();
    }
    public static TreeViewItem? FindByTag(TreeView tree, object targetTag)
    {
        foreach (TreeViewItem item in tree.Items)
        {
            var result = FindByTagRecursive(item, targetTag);
            if (result != null) return result;
        }
        return null;
    }

    private static TreeViewItem? FindByTagRecursive(TreeViewItem item, object targetTag)
    {
        if (Equals(item.Tag, targetTag))
            return item;

        foreach (var child in item.Items.OfType<TreeViewItem>())
        {
            var result = FindByTagRecursive(child, targetTag);
            if (result != null)
                return result;
        }

        return null;
    }    

    public string CurrentDirectory
    {
        get
        {
            return _currentDirectory;
        }
        set
        {
            if (_currentDirectory == value) return;

            string[] paths = GetPathHierarchy(value);

            foreach (var path in paths)
            {
                var result = FindByTag(this, path);
                if (result is not null)
                {
                    result.IsExpanded = true;
                    result.IsSelected = true;
                }
            }            


            _currentDirectory = value;
        }
    }

    public DirTreeView()
    {
        _root = new RootTreeItem();

        this.Items.Add(_root);
    }
    protected override void OnSelectedItemChanged(System.Windows.RoutedPropertyChangedEventArgs<object> e)
    {
        if (e.NewValue is null) return;

        TreeViewItem item = (TreeViewItem)e.NewValue;
        string FullName = (string)item.Tag;
        _currentDirectory = FullName;
        Debug.Print(FullName);
    }
}

ファイル名:FileUtil.cs


using System.IO;

namespace NoXAML16TreeView;

public static class FileUtil
{
    public static bool IsHiddenOrSystem(string path)
    {
        try
        {
            var attributes = File.GetAttributes(path);
            return attributes.HasFlag(FileAttributes.Hidden) || attributes.HasFlag(FileAttributes.System);
        }
        catch
        {
            return true;
        }
    }
}

ファイル名:MainWindow.xaml.cs


using System.IO;
using System.Windows;

namespace NoXAML16TreeView;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        Title = "ツリービュー";
        Width = 640;
        Height = 400;

        var dirTreeView = new DirTreeView();
        dirTreeView.Loaded += (sender, e) =>
        {
            dirTreeView.CurrentDirectory = Directory.GetCurrentDirectory();
        };
        Content = dirTreeView;

        
    }
}

ファイル名:RootTreeItem.cs


using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;

namespace NoXAML16TreeView;

public class RootTreeItem : TreeViewItem
{
    public RootTreeItem()
    {
        Header =  UIHelpers.CreateHeader("Root");
        Tag = "__Root__";
    }
    protected override void OnSelected(RoutedEventArgs e)
    {
        //Debug.Print("Root Selected");

        // 強制的展開
        this.IsExpanded = true;

        if (this.Items.Count > 0) return;

        // ドライブレターの一覧
        System.IO.DriveInfo[] drives = System.IO.DriveInfo.GetDrives();

        foreach(var drive in drives)
        {
            if (drive.IsReady == false) continue;

            string name = drive.Name.TrimEnd('\\');
            string fullName = drive.Name;

            var driveTreeItem = new DirTreeItem(name, fullName);

            this.Items.Add(driveTreeItem);
        }
    }
}

ファイル名:UIHelpers.cs


using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace NoXAML16TreeView;
    
public static class UIHelpers
{
    public static StackPanel CreateHeader(string name)
    {
        var stack = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
        var iconText = new TextBlock
        {
            FontFamily = new FontFamily("Segoe MDL2 Assets"),
            Text = "\uE8B7", // Folder アイコンの Unicode
            FontSize = 16,
            Margin = new Thickness(4, 4, 4, 4),
            VerticalAlignment = VerticalAlignment.Center
        };
        var text = new TextBlock { Text = name, VerticalAlignment = VerticalAlignment.Center };
        stack.Children.Add(iconText);
        stack.Children.Add(text);
        return stack;
    }
}

TreeViewの解説。
TreeViewは全体を表すTreeViewクラスと項目に相当するTreeViewItemクラスで構成されています。
TreeViewは木構造を表すコントロールですので、まずTreeViewのオブジェクトにルートとなるTreeViewItemを登録します。

サンプルコードではTreeViewを継承したDirTreeViewのコンストラクタで、TreeViewItemを継承したRootTreeItemのインスタンスを生成し、DirTreeView(this)のItems.Add()メソッドで登録しています。

_root = new RootTreeItem();

this.Items.Add(_root);

サンプルアプリはディレクトリツリーですので、ルートのアイテムは一つだけで、ルートアイテムから木構造作ります。

後は、同様にルート⇒ドライブ一覧⇒フォルダ一覧と階層をたどりながら、DirTreeItem(TreeViewItem)を親アイテムに.Teims.Add()で追加していきます。

本来、最初の段階でディレクトリツリーの全てのノードを構築出来ると良いのですが、全てのディレクトリ構造を再帰的に走査する必要があり、非常に時間が掛かる応答性の悪いUIになってしまいます。その対策として、選択したアイテムがサブディレクトリが無い場合、サブディレクトリを追加する仕様となっています。
TreeViewItem.OnSelected() をオーバーライドし、選択時に Directory.EnumerateDirectories() を呼び出して子ノードを追加します。

使っているTreeViewItemのプロパティはHeaderとTagです。
HeaderはItemに表示する文字列を設定設定します。
WPFの場合Heaerは文字列だけではなく、コントロールをセットすることも出来るので、サンプルプログラムではStackPanelにアイコン画像とディレクトリ名(文字列)を表示するようにしてあります。
次にTagですが此方はobjectをセットすることが出来ます。
たしか全てのクラスの祖となるクラスであるobjectですので、あらゆるオブジェクトをセットすることが出来ます。
Itemに紐づく任意のオブジェクトを乗せることが出来るので、イベントなどでItemが扱える状態であれば、紐づくオブジェクトが利用できるのでプログラミングの幅が広がります。
サンプルプログラムではディレクトリのフルパス(文字列)セットしています。セットする場合はそのまま代入で良いですが、取り出して使う場合はstring(文字列)にキャストして使う必要があります。

Headerにコンテンツを乗せることが出来ること以外は、ほぼほぼWinFormsと同じようなプログラミングになっていると思います。

コメント