🐡

AvaloniaUI TreeViewの使い方(発展編)

2024/02/26に公開

下記の続き
https://zenn.dev/ajkfds/articles/3fdeb4e93d00e9

やりたいこと

winformsのTreeViewを代替する機能を作りたい。
Nodeにアイコンを追加し、Node Object側からアイコンを変更できるようにしたい。
各種イベント/プロパティの処理をNode Objectに追加したい。

使用環境

Windows 11 : Home 22H2 + Visual Studio Community 2022(64bit)
Linuxテスト環境 : 上記Windows上のWSL2 Ubuntsu 22.04.2 LTS
Visual Studio2022, AvaloniaUI 11.0.2

TreeNode実装

TreeNodeクラスは下記のような実装にする
ImageアイコンとText文字列を持ち、Select/Expand/Collapse/Click/DoubleClickなどの動作に応じて各methodが呼ばれるようにする。双方向Bindingによって、Propertyの変更がViewに反映されるようにする。

TreeNode.cs
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace AvaloniaExample.Models
{
    internal class TreeNode : INotifyPropertyChanged
    {
        public TreeNode(string text)
        {
            Text = text;
        }

        // 初期アイコン (Assets内のpngファイル)
        static Bitmap defaultBitmap = new Bitmap(AssetLoader.Open(new Uri("avares://AvaloniaExample/Assets/paper.png")));

        // アイコン画像
        private IImage bitmap = defaultBitmap;
        public IImage Image
        {
            get
            {
                return bitmap;
            }
            set
            {
                bitmap = value;
            }
        }

        // ノード展開状態
        private bool _IsExpanded = false;
        public bool IsExpanded
        {
            get { return _IsExpanded; }
            set
            {
                bool prev = _IsExpanded;
                _IsExpanded = value;
                NotifyPropertyChanged();
                if (!prev & _IsExpanded)
                {
                    OnExpand();
                }
                if (prev & !_IsExpanded)
                {
                    OnCollapse();
                }
            }
        }

        // ノード展開時に呼ばれる
        public virtual void OnExpand() 
        {
            Text = "Expanded";
        }

        // ノードを閉じたときに呼ばれる
        public virtual void OnCollapse() 
        {
            Text = "Collapsed";
        }

        // ノードが選択されたときに呼ばれる
        public virtual void OnSelected() 
        {
            Text = "Selected";
        }

        // ノードがクリックされたときに呼ばれる
        public virtual void OnClicked()
        {
            Text = "Clicked";
        }

        // ノードがダブルクリックされたときに呼ばれる
        public virtual void OnDoubleClicked()
        {
            Text = "DoubleClicked";
        }

        // ノードテキスト
        private string _Text = "";
        public string Text
        {
            get { return _Text; }
            set { _Text = value; NotifyPropertyChanged(); }
        }

        // 子ノード
        private ObservableCollection<TreeNode> nodes = new ObservableCollection<TreeNode>();
        public ObservableCollection<TreeNode> Nodes
        {
            get { return nodes; }
            set { nodes = value; NotifyPropertyChanged(); }
        }

        // 双方向BIndingのためのViewModelへのProperty変更通知
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

ViewModel実装

ここではRoot Nodesの実装と、ノードダミーデータの初期化のみ。

TreeViewModel.cs
using AvaloniaExample.Models;
using System.Collections.ObjectModel;

namespace AvaloniaExample.ViewModels
{
    internal class TreeViewModel : ViewModelBase
    {
        // TreeViewのRoot Nodes
        public ObservableCollection<TreeNode> Nodes { get; } = new ObservableCollection<TreeNode>();

        public TreeViewModel()
        {
            // Nodeの追加
            Models.TreeNode node = new TreeNode("AAA");
            Nodes.Add(node);

            Models.TreeNode node2 = new TreeNode("BBB");
            node.Nodes.Add(node2);

            Models.TreeNode node3 = new TreeNode("CCC");
            node.Nodes.Add(node3);
        }
    }
}

axaml実装

Tapped,DoubleTapped,SelectionChangedのイベント実装とIsExpandedプロパティのBininding実装。TreeViewのItemはStackedPanelでImage(アイコン)とTextBlockを左右に並べたものにする。

TreeControl.axaml
<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:AvaloniaExample.ViewModels;assembly=AvaloniaExample"
             xmlns:views="clr-namespace:AvaloniaExample.Views;assembly=AvaloniaExample"
             xmlns:models="clr-namespace:AvaloniaExample.Models;assembly=AvaloniaExample"
             mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="450"
             x:Class="AvaloniaExample.Views.TreeControl"
			 >
	<Design.DataContext>
		<vm:MainViewModel />
	</Design.DataContext>
	<TreeView
		Name="Tree"
		x:DataType="vm:TreeViewModel"
		ItemsSource="{Binding Nodes}"
		
		Tapped="TreeView_Tapped"
		DoubleTapped="TreeView_DoubleTapped"
		SelectionChanged="TreeView_SelectionChanged"
		>
		
		<!-- Tap,DoubleTap,SelectionChangedイベントの割り当て -->

		<TreeView.ItemTemplate>
			<TreeDataTemplate
				x:DataType="models:TreeNode"
				ItemsSource="{Binding Nodes}"
					>
				<!-- ItemはImageとTextBlockを横に並べた構造にする -->
				<StackPanel Orientation="Horizontal">
					<Image Source="{Binding Image}" Width="16" Height="16"/>
					<TextBlock Text="{Binding Text}" Margin="8 0 0 0" />
				</StackPanel>
			</TreeDataTemplate>
		</TreeView.ItemTemplate>

		<!-- IsExpanded Propertyを更新する -->
		<TreeView.Styles>
			<Style Selector="TreeViewItem">
				<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded,Mode=TwoWay}"/>
			</Style>
		</TreeView.Styles>
	</TreeView>
</UserControl>

※ Solution作成時にCompiled Binindingをtrueにしていると、現状のAvaloniaの問題でIsExpand PropertyのStyle設定部でエラーが発生する。(TreeNodeではなくTreeViewModelにBindしようとする。) 現状ではCompiled Bindingは外しておくべきなよう。

TreeContolのコード

ハンドラで取得できるオブジェクトからgetTreeNodeでTreeNodeを取得している。
オブジェクトの親オブジェクトを探索してTreeViewItemを探し、そのDataContextからTreeNodeを取得している。

TreeControl.axaml.cs
using Avalonia.Controls;
using AvaloniaExample.Models;

namespace AvaloniaExample.Views
{
    public partial class TreeControl : UserControl
    {
        public TreeControl()
        {
            InitializeComponent();
            // Binding先の設定
            Tree.DataContext = new ViewModels.TreeControlModel();
        }

        // ノードクリック時の処理
        private void TreeView_Tapped(object? sender, Avalonia.Input.TappedEventArgs e)
        {
            TreeNode node = getTreeNode(e.Source);
            if (node == null) return;
            node.OnClicked();
        }

        // ノード選択時の処理
        private void TreeView_SelectionChanged(object? sender, Avalonia.Controls.SelectionChangedEventArgs e)
        {
            TreeNode node = getTreeNode(Tree.SelectedItem);
            if (node == null) return;
            node.OnSelected();
        }

        // ノードダブルクリック時の処理
        private void TreeView_DoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
        {
            TreeNode node = getTreeNode(e.Source);
            if (node == null) return;
            node.OnDoubleClicked();
        }

        // 取得したオブジェクトからTreeNodeを探しにいく
        // TextBlock/StackPanelのParentからTreeViewItemを探し、
        // TreeViewItemのDataContextからTreeNodeを取得する
        private TreeNode getTreeNode(object? target)
        {
            if (target == null) return null;
            if (target is TreeNode)
            {
                return target as TreeNode;
            }
            if (target is TextBlock)
            {
                TextBlock textBlock = target as TextBlock;
                return getTreeNode(textBlock.Parent);
            }
            else if (target is StackPanel)
            {
                StackPanel stackPanel = target as StackPanel;
                return getTreeNode(stackPanel.Parent);
            }
            else if (target is TreeViewItem)
            {
                TreeViewItem treeViewItem = target as TreeViewItem;
                return treeViewItem.DataContext as TreeNode;
            }
            else
            {
                return null;
            }
        }
    }
}

さらに以下に続く
https://zenn.dev/ajkfds/articles/c21c02ba197fa7

参考

WPFでのtreeVieewのBind方法
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/data/binding-sources-overview?view=netdesktop-7.0
https://araramistudio.jimdo.com/2016/10/24/wpfのtreeviewへデータをバインドする/

プロパティ変更通知の方法
https://www.d-itlab.co.jp/blog/2013/11/21/340.html

Discussion