Showing posts with label Shell. Show all posts
Showing posts with label Shell. Show all posts

Wednesday, January 23, 2013

C# WPF TreeView File Explorer

How to explore file system using WPF TreeView control?

In C#, one of the most exciting topic is Shell. As what we've discuss in Displaying System Icon in C#, shelling is a technique of manipulating file system objects in the computer's operating system using the system APIs provided. In the previous version of Microsoft programming language like VB6.0, the common and popular name for this is FSO (or FileSystemObject). With the advent of the technology inventions, doing Shell is very easy compare to what we're doing with the older version of programming languages.

In this blog, we preferred to use the WPF technology since we already engage in some project that already using the WPF. In short, we already have our environment ready for it.

See below the image as what are our target output.


In the above Image, you can see a WPF TreeView control that already filled with the File System information. We just simply get the root drives and get the child directories and files of the drives in a lazy load technique. See below our actual code implementation and explanation.

TreeView Explorer Implementation

First and foremost, we created a new Window and place a TreeView control inside it. See the codes below.

<Window x:Class="CodesDirectory.WIN_TreeView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WIN TreeView" Height="300" Width="300">
    <TreeView Name="treeView" Margin="5"></TreeView>
</Window>

In order for us to display the list of the available
Drive in the file system, we need to use the System.IO.DriveInfo class to get the list of drives. The method GetDrives method will give us the information of the active system drives. See the code below our implementation.

public void LoadDirectories()
{
    var drives = DriveInfo.GetDrives();
    foreach (var drive in drives)
    {
        this.treeView.Items.Add(this.GetItem(drive));
    }
}

In our code above, we created the method LoadDirectories so it can be called during the construction of the Window. You will also notice that there is a method named GetItem passing the drive information. This method will actually return the actual TreeViewItem that binds the DriveInfo object into the TreeViewItem's DataContext and Tag property. See below our implementation.

private TreeViewItem GetItem(DriveInfo drive)
{
    var item = new TreeViewItem
    {
        Header = drive.Name,
        DataContext = drive,
        Tag = drive
    };
    this.AddDummy(item);
    item.Expanded += new RoutedEventHandler(item_Expanded);
    return item;
}

Again, the AddDummy method and the item_Expanded method is needed in this implementation. The AddDummy method is used to create a temporary dummy item inside each TreeViewItem (those bound to Directory and Drive). The purpose of this is to let the TreeViewItem be collapsible (you will notice there is an arrow before the item) WITHOUT loading the child directories and files. In the other side, the item_Expanded method is just an event handler to the Expanded event of the TreeViewItem. Later we'll explain how did we used this event when loading the child directories and files.

Additionally, in order for us to determine whether the item is dummy or not we are forced to create a new class that inherits the
TreeViewItem. We called it DummyTreeViewItem. See the code below our implementation.

public class DummyTreeViewItem : TreeViewItem
{
    public DummyTreeViewItem()
        : base()
    {
        base.Header = "Dummy";
        base.Tag = "Dummy";
    }
}

Now, since we are exploring the File System Objects, we are not just limited for a drive. We have to address also the other object like File and Directory. In this case, we need to create an overloaded method of GetItem that returns the actual TreeViewItem that binds the corresponding object. See below our implementation.

private TreeViewItem GetItem(DirectoryInfo directory)
{
    var item = new TreeViewItem
    {
        Header = directory.Name,
        DataContext = directory,
        Tag = directory
    };
    this.AddDummy(item);
    item.Expanded += new RoutedEventHandler(item_Expanded);
    return item;
}

private TreeViewItem GetItem(FileInfo file)
{
    var item = new TreeViewItem
    {
        Header = file.Name,
        DataContext = file,
        Tag = file
    };
    return item;
}

What you can see above is just an overloaded method of what like the GetItem method of the DriveInfo. The only difference is just we used the DirectoryInfo and FileInfo object instead. Notice that in the FileInfo GetItem method there is no AddDummy and Expanded event listener. This is because the File object is not expandable and doesn't contain any children.

Looks like very surprising, till now we're not yet written the
AddDummy implementation. We just only would like to be in the proper order before doing so. The code is below is our implementation for the AddDummy method.

private void AddDummy(TreeViewItem item)
{
    item.Items.Add(new DummyTreeViewItem());
}

private bool HasDummy(TreeViewItem item)
{
    return item.HasItems && (item.Items.OfType<TreeViewItem>().ToList().FindAll(tvi => tvi is DummyTreeViewItem).Count > 0);
}

private void RemoveDummy(TreeViewItem item)
{
    var dummies = item.Items.OfType<TreeViewItem>().ToList().FindAll(tvi => tvi is DummyTreeViewItem);
    foreach (var dummy in dummies)
    {
        item.Items.Remove(dummy);
    }
}

Yes, that's it. Very simple! We only added a new DummyTreeViewItem (the class we created above). The other methods named HasDummy and RemoveDummy, they are usable once the item Expanded will be triggered. For the explanation, the HasDummy is used to check whether the TreeViewItem has child DummyTreeViewItem, and the RemoveDummy is to simply remove the DummyTreeViewItem from the parent's TreeViewItem children.

Explore Directories and Files

Exploring directories and files is very easy in C#. With the use of the DirectoryInfo object from System.IO namespace, we can get the list of child directories and files. Please visit Microsoft documentation for your reference.

private void ExploreDirectories(TreeViewItem item)
{
    var directoryInfo = (DirectoryInfo)null;
    if (item.Tag is DriveInfo)
    {
        directoryInfo = ((DriveInfo)item.Tag).RootDirectory;
    }
    else if (item.Tag is DirectoryInfo)
    {
        directoryInfo = (DirectoryInfo)item.Tag;
    }
    else if (item.Tag is FileInfo)
    {
        directoryInfo = ((FileInfo)item.Tag).Directory;
    }
    if (object.ReferenceEquals(directoryInfo, null)) return;
    foreach (var directory in directoryInfo.GetDirectories())
    {
        var isHidden = (directory.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
        var isSystem = (directory.Attributes & FileAttributes.System) == FileAttributes.System;
        if (!isHidden && !isSystem)
        {
            item.Items.Add(this.GetItem(directory));
        }
    }
}

private void ExploreFiles(TreeViewItem item)
{
    var directoryInfo = (DirectoryInfo)null;
    if (item.Tag is DriveInfo)
    {
        directoryInfo = ((DriveInfo)item.Tag).RootDirectory;
    }
    else if (item.Tag is DirectoryInfo)
    {
        directoryInfo = (DirectoryInfo)item.Tag;
    }
    else if (item.Tag is FileInfo)
    {
        directoryInfo = ((FileInfo)item.Tag).Directory;
    }
    if (object.ReferenceEquals(directoryInfo, null)) return;
    foreach (var file in directoryInfo.GetFiles())
    {
        var isHidden = (file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
        var isSystem = (file.Attributes & FileAttributes.System) == FileAttributes.System;
        if (!isHidden && !isSystem)
        {
            item.Items.Add(this.GetItem(file));
        }
    }
}

The method above named ExploreDirectories and ExploreFiles is used to explore the directory's child directories and files. It accepts one argument TreeViewItem (in our case the one that was returned by GetItem method bound by File, Directory or Drive), from there we start the exploration. You will also notice that in both method we used the GetFiles() and GetDirectories() method and iterate it. These two methods are very important to explore the child object of the directory.

Note: The GetFiles() and GetDirectories() method includes the System or Hidden objects in their result. So we must be aware of that before displaying it in the UI.

Also, you will notice that we used the
Bitwise comparison for the object Attributes. We first determined whether it is a System or a Hidden object, and if it is not we add it into the parent's TreeViewItem Items property.

Expanded Event

Now that we're almost done with our topic. One important thing we need to do is to manage how are we going to load the children of the file system object. We need to determine in what event and what our code is doing. In the TreeViewItem.Expanded event, as you remember, we added the item_Expanded event handler and there we did our job. We first checked whether the current item has a Dummy item and remove it. After that, we loaded the child directories and files with the methods we implemented above. See below our codes.

void item_Expanded(object sender, RoutedEventArgs e)
{
    var item = (TreeViewItem)sender;
    if (this.HasDummy(item))
    {
        this.Cursor = Cursors.Wait;
        this.RemoveDummy(item);
        this.ExploreDirectories(item);
        this.ExploreFiles(item);
        this.Cursor = Cursors.Arrow;
    }
}

You will also notice that we only call the RemoveDummy, ExploreDirectories and ExploreFiles method only if there is a Dummy node. This is because we used the Dummy object as a flag to the TreeViewItem state whether it is explored or not.

Lastly, we need to call the first method we created named LoadDirectories in the window's constructor so everything is initialized. See below the code.

public WIN_TreeView()
{
    InitializeComponent();
    this.LoadDirectories();
}

And that's it. We just finish the WPF TreeView file system explorer topic. Next topic is about the thing of loading the file system image into the TreeView.


Related topics:

Friday, January 18, 2013

Displaying System Icon in C#, WPF

How can we display the file system image in our Image object?

In C#, some system API is needed before we can manage to manipulate with the system objects. In some cases, we are doing the technique of Interoperability to do the inter-operation manipulation outside the application.

This blog will show you one technique how to use system APIs with the Shell functionality (file system manipulation) implementation.

Shelling is the process and technique of file system manipulation. Before we can do this, we need to import some basic functions from system API. Our target is to display the system icon base on the full path or extension of the file. Let's start with the system APIs.

Shell System API (functions and variables)

[DllImport("shell32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, out SHFileInfo psfi, uint cbFileInfo, uint uFlags);

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DestroyIcon(IntPtr hIcon);

public const uint SHGFI_ICON = 0x000000100;
public const uint SHGFI_USEFILEATTRIBUTES = 0x000000010;
public const uint SHGFI_OPENICON = 0x000000002;
public const uint SHGFI_SMALLICON = 0x000000001;
public const uint SHGFI_LARGEICON = 0x000000000;
public const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
public const uint FILE_ATTRIBUTE_FILE = 0x00000100;

The DllImport is an attribute used to import the system library into our application. For more information regarding DllImport, please visit Microsoft documentation. We used the SHGetFileInfo and DestroyIcon system function because the target of this blog is to only display the system icons (by file path and extension).

SHGetFileInfo: is the system function used to retrieve the information of file/folder from the file system (Windows operating system).

DestroyIcon: is the system function used to destroy the icon.

Note: For you to follow our implementation, please place the codes above in one static class named Interop.

After declaring the system functions/constants above, you need to create additional struct and named it SHFileInfo. Please see the code below.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct SHFileInfo
{
    public IntPtr hIcon;

    public int iIcon;

    public uint dwAttributes;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string szDisplayName;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
    public string szTypeName;
};

The struct above is the one who will be used by the SHGetFileInfo system function and will be the object who will handle all the information of the system file attribute.

After declaring the struct, we need to call the SHGetFileInfo system function passing the right attribute and the struct above so we can get the information of the system file. We have to create our own function to get the proper icon from the file system (with the the help of the system variables/functions above). Please see below our codes.

public class ShellManager
{
    public static Icon GetIcon(string path, ItemType type, IconSize size, ItemState state)
    {
        var flags = (uint)(Interop.SHGFI_ICON | Interop.SHGFI_USEFILEATTRIBUTES);
        var attribute = (uint)(object.Equals(type, ItemType.Folder) ? Interop.FILE_ATTRIBUTE_DIRECTORY : Interop.FILE_ATTRIBUTE_FILE);
        if (object.Equals(type, ItemType.Folder) && object.Equals(state, ItemState.Open))
        {
            flags += Interop.SHGFI_OPENICON;
        }
        if (object.Equals(size, IconSize.Small))
        {
            flags += Interop.SHGFI_SMALLICON;
        }
        else
        {
            flags += Interop.SHGFI_LARGEICON;
        }
        var shfi = new SHFileInfo();
        var res = Interop.SHGetFileInfo(path, attribute, out shfi, (uint)Marshal.SizeOf(shfi), flags);
        if (object.Equals(res, IntPtr.Zero)) throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
        try
        {
            Icon.FromHandle(shfi.hIcon);
            return (Icon)Icon.FromHandle(shfi.hIcon).Clone();
        }
        catch
        {
            throw;
        }
        finally
        {
            Interop.DestroyIcon(shfi.hIcon);
        }
    }
}

What the code is doing is. We created our own class named ShellManager, inside this class we created a method named GetIcon to return the Icon object based on the file path (or extension) by using the SHGetFileInfo system function and SHFileInfo defined struct. Once you had called the system function, the hIcon property of the struct will be the one who will handle the Pointer (Handle of IntPtr object) of the system icon. From there, we can load the actual icon using the FromHandle method of Icon class.

Note: It is an Icon class of System.Drawing namespace.

Additional supportive enumeration (personalize enumerations).

public enum IconSize : short
{
    Small,
    Large
}

public enum ItemState : short
{
    Undefined,
    Open,
    Close
}

Now, after placing everything in proper place. You can call the ShellManager GetIcon method to get the icon. Code below is the sample one.

var icon = ShellManager.GetIcon(Path.GetExtension(filename), ItemType.File, IconSize.Small, ItemState.Undefined);

Implementing FolderManager and FileManager class

Below are the codes we can use to manage the icon for both folder and icon.

public static class FolderManager
{
    public static ImageSource GetImageSource(string directory, ItemState folderType)
    {
        try
        {
            return FolderManager.GetImageSource(directory, new Size(16, 16), folderType);
        }
        catch
        {
            throw;
        }
    }

    public static ImageSource GetImageSource(string directory, Size size, ItemState folderType)
    {
        try
        {
            using (var icon = ShellManager.GetIcon(directory, ItemType.Folder, IconSize.Large, folderType))
            {
                return Imaging.CreateBitmapSourceFromHIcon(icon.Handle, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromWidthAndHeight((int)size.Width, (int)size.Height));
            }
        }
        catch
        {
            throw;
        }
    }
}

public static class FileManager
{
    public static ImageSource GetImageSource(string filename)
    {
        try
        {
            return FileManager.GetImageSource(filename, new Size(16, 16));
        }
        catch
        {
            throw;
        }
    }

    public static ImageSource GetImageSource(string filename, Size size)
    {
        try
        {
            using (var icon = ShellManager.GetIcon(Path.GetExtension(filename), ItemType.File, IconSize.Small, ItemState.Undefined))
            {
                return Imaging.CreateBitmapSourceFromHIcon(icon.Handle, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromWidthAndHeight((int)size.Width, (int)size.Height));
            }
        }
        catch
        {
            throw;
        }
    }
}

You'll notice that in the
GetImageSource method, we used the ShellManager.GetIcon method and create a new ImageSource object by mimicing the existing one and do the resize. This also give us an idea how resize an image.

The code above returned the ImageSource object which you can use to the value of the Source property of the Image control.

Congratulations! You just finish the tutorial how to display system icon in your Image object.