user316117
user316117

Reputation: 8281

Displaying a grid of thumbnails with C#/WPF

In my C# / WPF application I want to display a grid of image thumbnails from a directory tree of image files (currently .bmp's, but eventually other formats). In the future I'll probably want to allow the user to either click on a thumbnail to see a bigger version, or mouse over it to see some technical details, but right now all I want to do is display the thumbnails.

The number of images is not predictable and my instructions are to enable scrolling if there are more images than I can fit on a screen (rather than, say, shrinking the thumbnails).

I have a recursive routine to walk the tree and identify files to display . . .

   private bool WalkTree(String sRoot)
    {
        string sDirectoryName;
        string sFileName;
        int iDirectoryCount = 0;
        DirectoryInfo DirInfo;

        DirInfo = new DirectoryInfo(sRoot);
        // Get a list of all the files in this directory.
        foreach (FileInfo fi in DirInfo.GetFiles("*.bmp"))
        {
            sFileName = fi.Name;
            // DO SOMETHING WITH FILE FOUND HERE
        } 

        // Now get a list of all the subfolders in this directory.
        foreach (DirectoryInfo di in DirInfo.GetDirectories())
        {
            sDirectoryName = di.Name;
            WalkTree(sRoot + "\\" + sDirectoryName);  //recurse!!
            iDirectoryCount++;
        }
        return true;
    }  // End WalkTree 

So what's a good way to do this? What XAML control should I use to put all these in? A Grid? Can I add rows to that and make it scroll as I recurse and discover more files? Or should I walk the tree twice - once to get a count and configure the rows and columns in my page and the second time to actually display the thumbnails? Or am I thinking about this all wrong?

I feel like this problem must have been solved enough times in the past that there must be a canonical design pattern for it but I couldn't find one.

Upvotes: 3

Views: 6434

Answers (1)

Marc
Marc

Reputation: 13194

The comments describe how to do the layout part. It is relatively easy and what solution to use depends on the actual layout. UniformGrid is fine. It would be sufficient to use an ItemsControl here:

<ItemsControl ItemsSource="{Binding Images}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid>
                <ScrollViewer>
                    <UniformGrid />
                </ScrollViewer>
            </Grid>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Image Source="{Binding}" Width="100" Height="100" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl> 

Question is: What is Images, the property you bind the ItemsSource to? What you have is a list of strings with the filenames, what you need is a collcetion of ImageSources. There is no way around loading the images to memory. Still you need to do it efficiently, if you don't want your app to consume huge amounts of memory when the images are numerous and large. An option is to scale the images to thumbnails. Secondly, as you are scaling the images, this is a good opportunity to crop them as well to get fixed aspect ratios and make the grid look nice.

Images needs to be a collection of BitmapImage here in order to be bindable to the ImageSource property of the Image control:

public ObservableCollection<BitmapImage> Images { get; set; }

And this is basically what goes to // DO SOMETHING WITH FILE FOUND HERE:

var image = CreateBitmap(path);
var width = image.PixelWidth;
var height = image.PixelHeight;

// Crop image (cut the side which is too long)
var expectedHeightAtCurrentWidth = width*4.0/3.0;
var expectedWidthAtCurrentHeight = height*3.0/4.0;
var newWidth = Math.Min(expectedWidthAtCurrentHeight, width);
var newHeight = Math.Min(expectedHeightAtCurrentWidth, height);            
var croppedImage = CropImage(image, (int)newWidth, (int)newHeight);

// Scale to with of 100px
var ratio= 100.0 / newWidth;
var scaledImage = ScaleImage(croppedImage, ratio);
Images.Add(scaledImage);

With the following functions to create, scale and crop the image:

private static BitmapImage CreateBitmap(string path)
{
    var bi = new BitmapImage();
    bi.BeginInit();
    bi.UriSource = new Uri(path);
    bi.EndInit();
    return bi;
}

private BitmapImage ScaleImage(BitmapImage original, double scale)
{
    var scaledBitmapSource = new TransformedBitmap();
    scaledBitmapSource.BeginInit();
    scaledBitmapSource.Source = original;
    scaledBitmapSource.Transform = new ScaleTransform(scale, scale);
    scaledBitmapSource.EndInit();
    return BitmapSourceToBitmap(scaledBitmapSource);
}

private BitmapImage CropImage(BitmapImage original, int width, int height)
{
    var deltaWidth = original.PixelWidth - width;
    var deltaHeight = original.PixelHeight - height;
    var marginX = deltaWidth/2;
    var marginY = deltaHeight/2;
    var rectangle = new Int32Rect(marginX, marginY, width, height);
    var croppedBitmap = new CroppedBitmap(original, rectangle);
    return BitmapSourceToBitmap(croppedBitmap);
}

private BitmapImage BitmapSourceToBitmap(BitmapSource source)
{
    var encoder = new PngBitmapEncoder();
    var memoryStream = new MemoryStream();
    var image = new BitmapImage();

    encoder.Frames.Add(BitmapFrame.Create(source));
    encoder.Save(memoryStream);

    image.BeginInit();
    image.StreamSource = new MemoryStream(memoryStream.ToArray());
    image.EndInit();
    memoryStream.Close();
    return image;
}

Upvotes: 5

Related Questions