You might be wondering, what is he talking about? Was there a part 1 about this application? Yes there was and it wasn't actually called part one. It was an article about custom windows in WPF and how I made one myself for my photoviewer application. This part however is going to be about the collage view I created for the photoviewer application.
What is the collage view
The collage view is a view that provides a fun alternative to the standard thumbnails view in the photoviewer application. It's mainly there because I was bored last Saturday and wanted to see how far I could go modifying the listbox.
I think this is going pretty far, but I don't think I have reached the maximum of what is possible with the listbox control. Although I have to admit that the initial idea of displaying the listbox items in a 3D viewport is a bit too much for my liking.
Building the basic structure for a photo
The architecture of the photoviewer application is rather strict about the domainmodel of photos and albums. No presentation logic was implemented in the domainmodel classes. This gives me the freedom of reusing the domainmodel for different presentation techniques/frameworks (Yep, I am planning on doing so), but also requires me to do a bit more work to actually display photos.
For the collage view a photo needs to be displayed as a thumbnail with a location on screen (We need to throw it somewhere on screen) and a rotation to make it look random. I already have the PhotoViewModel that adds the thumbnail property and some custom logic to load photos in a background thread. Since I already have most things required to show a photo in the collage view I extended the PhotoViewModel class and added the properties required for throwing and rotating the picture on screen. Mind you, throwing here is just generating random coordinates within a limited area.
Templating the listbox
To actually get a collage view you will need to break the listbox apart. This means redefining the presentation of the listbox while leaving the behavior part intact. The can all be done with XAML, there is no need to modify any code.
To make it possible to place items randomly inside the listbox the following template is required:
<ListBox.Template>
<ControlTemplate TargetType="{x:Type ListBox}">
<Canvas IsItemsHost="True" Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"/>
</ControlTemplate>
</ListBox.Template>
What will happen is that all items will be placed at the top-left corner of the listbox and will be stacked one on top of the other. I will show you how to fix this problem in a minute.
Templating listbox items
The core of the modification of the listbox lies at the template of the listboxitems. This template not only solves the problem of having all items at the top-left corner of the canvas, but also shows the items slightly tilted.
The first piece of XAML is the datatemplate for the listbox items:
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CollagePhoto}">
<controls:PictureFrame x_Name="ItemContent"
Source="{Binding Thumbnail}" Width="120"/>
</DataTemplate>
</ListBox.ItemTemplate>
This doesn't do much, except for showing a box with a black border and white background, a drop-shadow and of course the thumbnail of the photo.
The magic happens in the ItemContainerStyle:
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<Border x_Name="ItemBorder">
<Border.LayoutTransform>
<RotateTransform Angle="{Binding Path=Angle,Mode=OneWay}"
CenterX="60" CenterY="45"/>
</Border.LayoutTransform>
<ContentPresenter/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="ItemBorder" Property="BorderThickness" Value="2"/>
<Setter TargetName="ItemBorder" Property="BorderBrush" Value="Blue"/>
<Setter Property="Canvas.ZIndex" Value="1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Canvas.Left" Value="{Binding Left}"/>
<Setter Property="Canvas.Top" Value="{Binding Top}"/>
</Style>
</ListBox.ItemContainerStyle>
Two setters are used to place each photo at the random generated position in the listbox. What this does is bind the Top and Left property of the CollagePhoto instance to the Canvas.Top and Canvas.Left attached properties thus placing the photo at the correct location.
The second interesting thing to note is the Layout transform of the border. I used this to rotate the whole listboxitem rather than just rotating the photo. The reason for this is the selection border around listbox items. If you only rotate the photo the border isn't drawn around the tilted photo, instead you will see an ugly square around the selected photo that doesn't line up at all. So I tilt the border and not the photos. This makes the application draw the border around the photo correctly.
Points of interest
The collage view has a few odds and ends that I still need to clean up. For example, when the photos fall outside the bounds of the canvas no scrollbars are shown. You may have noticed that a ScrollViewer is missing, this was done on purpose. Adding the ScrollViewer does nothing, because the canvas doesn't grow when the items are going outside the bounds of the canvas. This is by design and fixing it is somewhat difficult. However expect a fix for this soon, because it bothers me quite a lot.
Another feature I would like to introduce is moving the pictures around in the pile. I haven't really got a good idea on how to do this, but I know it can be done. It's just that I need another boring Saturday to get it fixed 😉
Canvas.ZIndex
In the first version of the collage view you could select photos that were at the bottom of the pile, but they didn't pop to the top of the pile making it kind of hard to see what you selected. At first this seemed unfixable, because I had no way of changing the ZIndex of the photo in the pile.
After a search on google it turned out Microsoft didn't document the full framework. It turns out that there is a ZIndex attached property on the canvas class that allows you to change the ZIndex of elements. Setting a higher number will cause these items to be drawn on top of items with a lower ZIndex.
Making photos pop to the top of the pile when selecting them turned out to be pretty easy to do. Just add <Setter Property="Canvas.ZIndex" Value="1"/> and you are done.
Where to get the goods
Since there isn't really a conclusion I'm left with writing down the locations where to get the goods to see the new collage view in action:
- http://www.codeplex.com/photoviewer/Release/ProjectReleases.aspx?ReleaseId=18556