PowerShell WPF TreeView with Dynamic Images

149 Views Asked by At

I am working on a project where I have a PowerShell driven UI that has a Treeview list with one selection. I have that code working just fine following this blog PowerShell WPF - Customize TreeView Icon...great article BTW! I would like to customize the tree list with different images for different files base on its item tag. I can get this to work using png image source and DataTriggers, however I am trying to embed all images as SVG within a canvas template. The only way I know how to get this to work is using the VisualBrush element but I cannot seem to get this to work within the Treeview.resource framework element. Here is what I have so far:

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TreeView"
        Height="600" Width="800"
        ResizeMode="NoResize"
        WindowStartupLocation="CenterScreen"
        ShowInTaskbar="False" Topmost="True">  
    <Window.Resources>
        
        <ResourceDictionary>

            <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="Resources\Icons.xaml" />
                    <ResourceDictionary Source="Resources\TabControl_LeftSideStyle.xaml" />
                    <ResourceDictionary Source="Resources\TreeViewItem_StandardStyle.xaml" />

                </ResourceDictionary.MergedDictionaries>
            
            
            <Style TargetType="{x:Type Window}">
                <Setter Property="FontFamily" Value="Segoe UI" />
                <Setter Property="FontWeight" Value="Light" />
                <Setter Property="BorderBrush" Value="#004275" />
                <Setter Property="BorderThickness" Value="0.5" />
            </Style>

            <HierarchicalDataTemplate x:Key="CheckBoxItemTemplate" ItemsSource="{Binding Children, Mode=OneTime}">
                <StackPanel Orientation="Horizontal">
                    <CheckBox Focusable="False" IsChecked="{Binding IsChecked}" VerticalAlignment="Center" />
                    <ContentPresenter Content="{Binding Name, Mode=OneTime}" Margin="2,0" />
                </StackPanel>
            </HierarchicalDataTemplate>
            </ResourceDictionary>
        </Window.Resources>
    
    <Grid>
        
        <Rectangle Fill="#004275" HorizontalAlignment="Left" Height="600" VerticalAlignment="Center" Width="150"/>
        <Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Right" Height="600" VerticalAlignment="Center" Width="156"/>
        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10">
            <Image x:Name="_wizMainLogo" Height="48" Width="132" Stretch="Uniform" />
        </StackPanel>

        <!-- START TAB MENU -->
        <TabControl x:Name="_wizTabControl" Style="{DynamicResource TabControlLeftSide}" Width="800" Height="550" HorizontalAlignment="Center" VerticalAlignment="Top">
            <TabItem x:Name="_wizid" Header="Tree view" Style="{DynamicResource TabItemsWhite}" Width="150" Margin="0" IsEnabled="False">
                <Grid x:Name="TabLayout" Margin="0" Grid.ColumnSpan="2" FocusManager.FocusedElement="{Binding ElementName=tsTree}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="490"></ColumnDefinition>
                        <ColumnDefinition Width="150"></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <Label x:Name="TabMainTitle" Grid.Column="0" HorizontalAlignment="Left" Margin="10,20,0,0" VerticalAlignment="Top" FontSize="22" Content="Tree view list with icons"/>
                    <Rectangle Grid.Column="0" Fill="HotPink" HorizontalAlignment="Right" Height="40" Width="40" Margin="0,20,13.5,0" VerticalAlignment="Top">
                        <Rectangle.OpacityMask>
                            <VisualBrush Stretch="Fill" Visual="{DynamicResource icons_listoutline}"/>
                        </Rectangle.OpacityMask>
                    </Rectangle>


                    <Label x:Name="TabSubTitle" FontSize="14" HorizontalAlignment="Left" Margin="10,73,0,0" VerticalAlignment="Top" Content="Search for item in list"/>
                    <TextBox x:Name="TabSearch" HorizontalAlignment="Left" Height="31" Margin="10,103,0,0" TextWrapping="Wrap" Text="Search..." VerticalAlignment="Top" Width="331" Foreground='Gray' VerticalContentAlignment="Center" FontSize="18"/>
                    <Button x:Name="TabSearchClear" Content="Clear" Height="31" Width="63" HorizontalAlignment="Left" VerticalAlignment="Bottom" FontSize="10" Padding="2" Margin="414,0,0,416" />
                    <Button x:Name="TabSearchEnter" Content="Search" Height="31" Width="63" HorizontalAlignment="Left" VerticalAlignment="Bottom" FontSize="10" Padding="2" Margin="346,0,0,416" />

                    <Button x:Name="TabExpand" Content="Expand All" Height="25" Width="113" HorizontalAlignment="Left" VerticalAlignment="Bottom" FontSize="10" Padding="2" Margin="10,0,0,386" />
                    <Button x:Name="TabCollapse" Content="Collapse All" Height="25" Width="113" HorizontalAlignment="Left" VerticalAlignment="Bottom" FontSize="10" Padding="2" Margin="128,0,0,386" />

                    <TreeView x:Name="TabTree" HorizontalAlignment="Left" Height="371" Margin="10,169,0,0" VerticalAlignment="Top" Width="467" FontSize="14"  
                                ItemContainerStyle="{StaticResource TreeViewItemStandard}" 
                                ItemTemplate="{StaticResource CheckBoxItemTemplate}" >
                        <TreeView.Resources>
                            <Style TargetType="{x:Type TreeViewItem}">
                                <Setter Property="HeaderTemplate">
                                    <Setter.Value>
                            
                                        <DataTemplate>
                                            <StackPanel Orientation="Horizontal">
                                              <Image Name="TreeNodeIMG" Width="20" Height="20" Stretch="Fill">
                                                <Image.Style>
                                                  <Style TargetType="{x:Type Image}">
                                                    <Style.Triggers>
                                                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=Tag[0]}" Value="folder">
                                                            <Setter Property="Source" Value="images\folder.png"/>
                                                        </DataTrigger>
                                                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=Tag[0]}" Value="file">
                                                            <Setter Property="Source" Value="images\file.png"/>
                                                        </DataTrigger>
      
                                                    </Style.Triggers>
                                                  </Style>
                                                </Image.Style>
                                              </Image>
                                              <TextBlock VerticalAlignment="Center" Text="{Binding}" Margin="5,0" />
                                            </StackPanel>
                                          </DataTemplate>

                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </TreeView.Resources> 

                    </TreeView>

                    <Label Content="More Info" Grid.Column="1" FontSize="14" HorizontalAlignment="Left" Margin="10,31,0,0" VerticalAlignment="Top" Foreground="LightSlateGray" />
                    <TextBlock x:Name="TabMoreInfo" Grid.Column="1" HorizontalAlignment="Left" Margin="10,89,0,0" Width="136" TextWrapping="Wrap" VerticalAlignment="Top" Height="422">
                        <Run Text="orem ipsum dolor sit amet, consectetur adipiscing elit. Morbi risus mi, consequat in ultricies eu, euismod nec eros. Pellentesque vel augue sed enim euismod sodales varius a ipsum. "/>
                    </TextBlock>
                </Grid>
            </TabItem>
        </TabControl>
        
    </Grid>
    
</Window>

And the PowerShell script sample I am testing with is this:

#$ErrorActionPreference='Stop'
##*=============================================
##* VARIABLES
##*=============================================
[string]$ResourceRoot = ($PWD.ProviderPath, $PSScriptRoot)[[bool]$PSScriptRoot]


##*=============================================
##* LOAD UI
##*=============================================
[System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | out-null

function LoadXaml ($filename){
    $XamlLoader=(New-Object System.Xml.XmlDocument)
    $XamlLoader.Load($filename)
    return $XamlLoader
}
$XamlMainWindow=LoadXaml($ResourceRoot+"\MainWindow.xaml")
$reader = (New-Object System.Xml.XmlNodeReader $XamlMainWindow)
$Form = [Windows.Markup.XamlReader]::Load($reader)


$Global:FolderTree = $Form.FindName("TabTree")

##*=============================================
##* MAIN
##*=============================================

$dummyNode = $null
$preselect = 'rootFile1.txt'


$AllFiles  = [IO.Directory]::GetFiles("$ResourceRoot\Test")
$AllDirectory = [IO.Directory]::GetDirectories("$ResourceRoot\Test")

# ================== Handle Folders ===========================
#$folder = $AllDirectory[0]
foreach ($folder in $AllDirectory){

    $treeViewItem = [Windows.Controls.TreeViewItem]::new()
    $treeViewItem.Header = $folder.Substring($folder.LastIndexOf("\") + 1)
    $treeViewItem.Tag = @("folder",$folder)
    $treeViewItem.Items.Add($dummyNode) | Out-Null

    
    $treeViewItem.Add_Expanded({
        Write-Host $_.OriginalSource.Header  " is expanded"
        TreeExpanded($_.OriginalSource)
    })
    $FolderTree.Items.Add($treeViewItem)| Out-Null

}

# ================== Handle Files ===========================
foreach ($file in $AllFiles){

    $treeViewItem = [Windows.Controls.TreeViewItem]::new()
    $treeViewItem.Header = $file.Substring($file.LastIndexOf("\") + 1)
    $treeViewItem.Tag = @("file",$file) 
    $FolderTree.Items.Add($treeViewItem)| Out-Null
    If($treeViewItem.Header -eq $preselect){
        $treeViewItem.IsSelected = $true
        Write-Host ("pre selected Item: {0}" -f $treeViewItem.Header)
    }Else{
        Write-Host ("Added Item: {0}" -f $treeViewItem.Header)
    }

    $treeViewItem.Add_PreviewMouseLeftButtonDown({
        [System.Windows.Controls.TreeViewItem]$sender = $args[0]
        [System.Windows.RoutedEventArgs]$e = $args[1]
        Write-Host "Left Click: $($sender.Tag)"
    })

    $treeViewItem.Add_PreviewMouseRightButtonDown({
        [System.Windows.Controls.TreeViewItem]$sender = $args[0]
        [System.Windows.RoutedEventArgs]$e = $args[1]
        Write-Host "Right Click: $($sender.Tag)"
    })

}


Function TreeExpanded($sender){
    
    $global:item = [Windows.Controls.TreeViewItem]$sender
    
    If ($item.Items.Count -eq 1 -and $item.Items[0] -eq $dummyNode)
    {
        $item.Items.Clear();
        Try
        {
            
            foreach ($string in [IO.Directory]::GetDirectories($item.Tag[1].ToString()))
            {
                $subitem = [Windows.Controls.TreeViewItem]::new();
                $subitem.Header = $string.Substring($string.LastIndexOf("\") + 1)
                $subitem.Tag = @("folder",$string)
                $subitem.Items.Add($dummyNode)
                
                $subitem.Add_Expanded({
                    TreeExpanded($_.OriginalSource)
                })
                $item.Items.Add($subitem) | Out-Null
            }

            foreach ($file in [IO.Directory]::GetFiles($item.Tag[1].ToString())){

                $subitem = [Windows.Controls.TreeViewItem]::new()
                $subitem.Header = $file.Substring($file.LastIndexOf("\") + 1)
                $subitem.Tag = @("file",$file) 
                $item.Items.Add($subitem)| Out-Null

                If($subitem.Header -eq $preselect){
                    $subitem.IsSelected = $true
                    Write-Host ("pre-selected Item: {0}" -f $subitem.Header)
                }Else{
                    Write-Host ("Added Item: {0}" -f $subitem.Header)
                }


                $subitem.Add_PreviewMouseLeftButtonDown({
                    [System.Windows.Controls.TreeViewItem]$sender = $args[0]
                    [System.Windows.RoutedEventArgs]$e = $args[1]
                    Write-Host "Left Click: $($sender.Tag)"
                })
            }
        }   
        Catch [Exception] { }
    }
     
}

##*=============================================
##* SHOW UI
##*=============================================

$Form.ShowDialog() | Out-Null

When I run the code it looks like this:

Treeview WPF

This is an issue with the images not showing for the root level folder but not too big of a deal unless someone has an answer for that too...

The canvas resource dictionary file has this in it:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Canvas x:Key="icon_file" Width="24" Height="24">
        <Path Stretch="Fill" Fill="White" Data="M3 5V19H20V5H3M7 7V9H5V7H7M5 13V11H7V13H5M5 15H7V17H5V15M18 17H9V15H18V17M18 13H9V11H18V13M18 9H9V7H18V9Z" />
    </Canvas>

    <Canvas x:Key="icon_folder" Width="24" Height="24">
        <Path Width="20" Height="20" Fill="White" Stretch="Uniform" Data="M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z"/>
    </Canvas>

</ResourceDictionary>

The only way I got it to sort of work is by using this code within the xaml

<TreeView.Resources>
    <Style TargetType="{x:Type TreeViewItem}">
        <Setter Property="HeaderTemplate">
            <Setter.Value>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">

                        <Rectangle Fill="Green" HorizontalAlignment="Right" Height="20" Width="20" VerticalAlignment="Top">
                            <Rectangle.OpacityMask>
                                <VisualBrush Stretch="Fill" Visual="{DynamicResource icons_folder}"/>
                            </Rectangle.OpacityMask>
                        </Rectangle>

                        <TextBlock Text="{Binding}" Margin="5,0" />
                    </StackPanel>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</TreeView.Resources>

Even though this fills the entire image to the fill color, it does show but its the same for both folders and files. Now If I try to use the bindings for the canvas images and the datatriggers, I get a data type error. I have found nothing online that talks about how to use svg data bindings with treeview and data triggers. There is allot out there for C# coding but looking for a PowerShell and .Net solution.

Even though I assumed it would be easier to accomplish this in the XAML code, I did try to programmatically change images within PowerShell...but that too failed miserably. Any ideas?

I need some of your expert skills! THANKS!!!

1

There are 1 best solutions below

0
mm8 On

You could replace the Image in the HeaderTemplate with a ContentControl that displays one of your Canvas resources:

<Style TargetType="{x:Type TreeViewItem}">
    <Setter Property="HeaderTemplate">
        <Setter.Value>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <ContentControl>
                        <ContentControl.Style>
                            <Style TargetType="ContentControl">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=Tag[0]}" Value="folder">
                                        <Setter Property="Content" Value="{DynamicResource icon_folder}" />
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=Tag[0]}" Value="file">
                                        <Setter Property="Content" Value="{DynamicResource icon_file}" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </ContentControl.Style>
                    </ContentControl>
                    <TextBlock VerticalAlignment="Center" Text="{Binding}" Margin="5,0" />
                </StackPanel>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>