I just did some recent exploration in creating a tag cloud with WPF. In particular, I wanted to be able to create a tag cloud of the lastnames for a collection of people. I came up with a solution that I'm happy with and would like to share.
You can see a screenshot of the prototype below. The left Listbox is bounded to the collection of people and the right list is the tag cloud. The size of the tags is based on the number of people with that lastname. Names can be added manually or updated in the top section. There is a Churn button to auto populate the people collection.
Code
Surprisingly I was able to build the tag cloud with very little code. Instead of generating the tag cloud on the fly each time or cached like most web-based solution, I wanted the tag cloud to be dynamic as new items are added and changed. I also didn't want to roll my own class that keep tracks of the lastnames. So I relied heavily on the built-in functionality and power of WPF and the .NET 3.0 Framework to accomplish this.
Tags and ListCollectionView
To get the list of tags, I created a ListCollectionView on the people collection and grouped it by added a GroupDescription on the "Lastname" property. The PeopleTagCloud ListBox is bounded to the groups for that ListCollectionView.
// ListCollectionView is used for sorting and grouping
lcv = new
ListCollectionView(people);
...
// Group the list by lastname, the tag cloud is based on the group Name and ItemCount
lcv.GroupDescriptions.Add(new
PropertyGroupDescription("Lastname"));
PeopleTagCloud.ItemsSource = lcv.Groups;
Displaying the Tags
The PeopleTagCloud is just a Listbox that has a style to make it layout the items like a tag cloud. The trick is to replace the ItemsHost which originally is a StackPanel with a WrapPanel. Lee Brimelow wrote about this awhile back.
<Style
x:Key="TagsListBox"
TargetType="{x:Type ListBox}">
…
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type ListBox}">
<Grid>
<Border
x:Name="Border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"/>
<ScrollViewer
Margin="1"
Style="{DynamicResource SimpleScrollViewer}"
Focusable="false"
Background="{TemplateBinding Background}">
<!-- Replaced the default StackPanel ItemsHost with a WrapPanel to get the TagCloud layout-->
<WrapPanel
Margin="2"
IsItemsHost="true"/>
</ScrollViewer>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Recall that in the previous section the PeopleTagCloud is bounded to the groups of lastnames. With that binding, I can simply create a DataTemplate to display the individual tags.
<DataTemplate
x:Key="TagCloudTemplate">
<TextBlock
Padding="0,0,10,0"
FontSize="{Binding Path=ItemCount, Converter={StaticResource CountToFontSizeConverter}}"
Text="{Binding Path=Name, Mode=Default}"/>
</DataTemplate>
The text is bounded to the Name property and the FontSize is bounded to the ItemCount.
CountToFontSizeConverter
To get the ItemCount to be a FontSize, I needed to create a value converter. The Convert method itself isn't that great but works. I'm sure someone can devise a better algorithm for the sizes.
class
CountToFontSizeConverter : IValueConverter
{
#region IValueConverter Members
public
object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
const
int minFontSize = 6;
const
int maxFontSize = 38;
const
int increment = 3;
int count = (int)value;
return ((minFontSize + count + increment) < maxFontSize) ? (minFontSize + count + increment) : maxFontSize;
}
…
ObservableCollection and INotifyPropertyChanged
How does the PeopleTagCloud know that it's being updated? That's the beauty of inheriting from the ObservableCollection class. No extra code necessary in the presentation layer.
class
People : ObservableCollection<Person> { }
To get the PeopleTagCloud to update when the lastname is modified is a bit trickier. I presumed that I can simply implement INotifyPropertyChanged.
class
Person : INotifyPropertyChanged
This typically just works as you can see the effect on the PeopleListBox when the names are changed. However, the ListViewCollection groups and ItemCount were not updated. I needed to programmatically call the Refresh() method when the lastname is changed.
private
void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
// Tell the ListCollectionView to update itself so that the groups and itemcount are updated
if (lcv != null)
lcv.Refresh();
}
Because of this, there is slight performance on updating the tag cloud when the names are modified. A possible solution might be to refresh after losing focus instead of TextChanged to lessen the cost of refreshing the groups. Let me know if I'm missing something here.
Auto-Populating the Data
I just re-used the churning code by Kevin Moore in his bag of tricks. It uses a background thread to add the people. The names are from the list of common names on names.mongabay.com.
Source
You can download the source here.