NOTES :
The alternative demo project and touch projects above were submitted to me by Jonathan Hodges.
Alternative demo : fixes a layout issue and also provides support for MouseWheel events to scroll. Thanks Jonathan, nice edits.
Touch demo : works with the touch events for working with touch screens.
Better new improved Panorama mouse handlers : Marc Jacobi
Introduction
The attached code doesn't really do much so it doesn't need much jibber jabber describing what it does.
What it does is pretty simple and can quite easily be summarized by the following few points:
- Create a Metro style control. I say Style, as it may not be 100% in line with what the Win8 Metro tile interface does, or how it functions, but to
be honest, it fitted my needs well enough and I figured it may be OK for others. So be warned, it
is not like the best control ever, it is in fact pretty simple.
- Allow it to be used in direct content mode/or use MVVM to drive the creation of UI elements, via DataBinding.
- Allow users to customise the primary/complementary colors that should be used when generating tiles.
- Allow single tile group snap back animations (you know when you have not dragged past 1/2 way ala iPhone UI experience, it snaps back to
the previous tile group).
- Works with WPF.
I wrote this control because I could not find one that seemed to work, I am aware of one in the Windows7 contrib/source code, but I wanted one for WPF, so I had to write this one.
So that is basically what it does in a nutshell, I guess it would be a nice thing to do at this point to show a screenshot or
two, so let's do that.
Screenshots of Moving Parts
So here is how it all fits (you can click this image to get a bigger image):
The important thing to note here is that you are able to express how your tiles should look, that is entirely up to you. You have complete
control over how your tiles are shown, this is typically done using a DataTemplate
for your type of tile. We will see more on this later.
Deep Dive Into How It Works
There are really only a few controls that we need to worry about, which are described in full below:
PanoramaGroup
You can think of ParoramaGroup
as being a logical group of tiles. Where the ParoramaGroup
objects you supply need to look like this:
public class PanoramaGroup
{
public PanoramaGroup(string header, ICollectionView tiles)
{
this.Header = header;
this.Tiles = tiles;
}
public string Header { get; private set; }
public ICollectionView Tiles { get; private set; }
}
Panorama
This is obviously where all the real action happens. So what does this control provide? It provides the following:
- Templating ability via DataTemplates (in your own code...bonus)
- It is a lookless control, as such template/style it as you see fit
- It is easy to use
- Two forms of scrolling, either snapback or using friction to current mouse co-ordinates
So those are the features.. what does the code look like? Well, here it is:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Threading;
using System.ComponentModel;
using System.Windows.Input;
using System.Windows.Media;
namespace PanoramaControl
{
[TemplatePart(Name = "PART_ScrollViewer", Type = typeof(ScrollViewer))]
public class Panorama : ItemsControl
{
#region Data
private ScrollViewer sv;
private Point scrollTarget;
private Point scrollStartPoint;
private Point scrollStartOffset;
private Point previousPoint;
private Vector velocity;
private double friction;
private DispatcherTimer animationTimer = new DispatcherTimer(DispatcherPriority.DataBind);
private static int PixelsToMoveToBeConsideredScroll = 5;
private static int PixelsToMoveToBeConsideredClick = 2;
private Random rand = new Random(DateTime.Now.Millisecond);
private bool _mouseDownFlag;
private Cursor _savedCursor;
#endregion
#region Ctor
public Panorama()
{
friction = 0.85;
animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
animationTimer.Tick += new EventHandler(HandleWorldTimerTick);
animationTimer.Start();
TileColors = new Brush[] {
new SolidColorBrush(Color.FromRgb((byte)111,(byte)189,(byte)69)),
new SolidColorBrush(Color.FromRgb((byte)75,(byte)179,(byte)221)),
new SolidColorBrush(Color.FromRgb((byte)65,(byte)100,(byte)165)),
new SolidColorBrush(Color.FromRgb((byte)225,(byte)32,(byte)38)),
new SolidColorBrush(Color.FromRgb((byte)128,(byte)0,(byte)128)),
new SolidColorBrush(Color.FromRgb((byte)0,(byte)128,(byte)64)),
new SolidColorBrush(Color.FromRgb((byte)0,(byte)148,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)0,(byte)199)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)135,(byte)15)),
new SolidColorBrush(Color.FromRgb((byte)45,(byte)255,(byte)87)),
new SolidColorBrush(Color.FromRgb((byte)127,(byte)0,(byte)55))
};
ComplimentaryTileColors = new Brush[] {
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255))
};
}
static Panorama()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Panorama), new FrameworkPropertyMetadata(typeof(Panorama)));
}
#endregion
#region Properties
public double Friction
{
get { return 1.0 - friction; }
set { friction = Math.Min(Math.Max(1.0 - value, 0), 1.0); }
}
public List<Brush> TileColorPair
{
get
{
int idx = rand.Next(TileColors.Length);
return new List<Brush>() { TileColors[idx], ComplimentaryTileColors[idx] };
}
}
#region DPs
#region ItemBox
public static readonly DependencyProperty ItemBoxProperty =
DependencyProperty.Register("ItemHeight", typeof(double), typeof(Panorama),
new FrameworkPropertyMetadata((double)120.0));
public double ItemBox
{
get { return (double)GetValue(ItemBoxProperty); }
set { SetValue(ItemBoxProperty, value); }
}
#endregion
#region GroupHeight
public static readonly DependencyProperty GroupHeightProperty =
DependencyProperty.Register("GroupHeight", typeof(double), typeof(Panorama),
new FrameworkPropertyMetadata((double)640.0));
public double GroupHeight
{
get { return (double)GetValue(GroupHeightProperty); }
set { SetValue(GroupHeightProperty, value); }
}
#endregion
#region HeaderFontSize
public static readonly DependencyProperty HeaderFontSizeProperty =
DependencyProperty.Register("HeaderFontSize", typeof(double), typeof(Panorama),
new FrameworkPropertyMetadata((double)30.0));
public double HeaderFontSize
{
get { return (double)GetValue(HeaderFontSizeProperty); }
set { SetValue(HeaderFontSizeProperty, value); }
}
#endregion
#region HeaderFontColor
public static readonly DependencyProperty HeaderFontColorProperty =
DependencyProperty.Register("HeaderFontColor", typeof(Brush), typeof(Panorama),
new FrameworkPropertyMetadata((Brush)Brushes.White));
public Brush HeaderFontColor
{
get { return (Brush)GetValue(HeaderFontColorProperty); }
set { SetValue(HeaderFontColorProperty, value); }
}
#endregion
#region HeaderFontFamily
public static readonly DependencyProperty HeaderFontFamilyProperty =
DependencyProperty.Register("HeaderFontFamily", typeof(FontFamily), typeof(Panorama),
new FrameworkPropertyMetadata((FontFamily)new FontFamily("Segoe UI")));
public FontFamily HeaderFontFamily
{
get { return (FontFamily)GetValue(HeaderFontFamilyProperty); }
set { SetValue(HeaderFontFamilyProperty, value); }
}
#endregion
#region TileColors
public static readonly DependencyProperty TileColorsProperty =
DependencyProperty.Register("TileColors", typeof(Brush[]), typeof(Panorama),
new FrameworkPropertyMetadata((Brush[])null));
public Brush[] TileColors
{
get { return (Brush[])GetValue(TileColorsProperty); }
set { SetValue(TileColorsProperty, value); }
}
#endregion
#region ComplimentaryTileColors
public static readonly DependencyProperty ComplimentaryTileColorsProperty =
DependencyProperty.Register("ComplimentaryTileColors", typeof(Brush[]), typeof(Panorama),
new FrameworkPropertyMetadata((Brush[])null));
public Brush[] ComplimentaryTileColors
{
get { return (Brush[])GetValue(ComplimentaryTileColorsProperty); }
set { SetValue(ComplimentaryTileColorsProperty, value); }
}
#endregion
#region UseSnapBackScrolling
public static readonly DependencyProperty UseSnapBackScrollingProperty =
DependencyProperty.Register("UseSnapBackScrolling", typeof(bool), typeof(Panorama),
new FrameworkPropertyMetadata((bool)true));
public bool UseSnapBackScrolling
{
get { return (bool)GetValue(UseSnapBackScrollingProperty); }
set { SetValue(UseSnapBackScrollingProperty, value); }
}
#endregion
#endregion
#endregion
#region Private Methods
private void DoStandardScrolling()
{
sv.ScrollToHorizontalOffset(scrollTarget.X);
sv.ScrollToVerticalOffset(scrollTarget.Y);
scrollTarget.X += velocity.X;
scrollTarget.Y += velocity.Y;
velocity *= friction;
}
private void HandleWorldTimerTick(object sender, EventArgs e)
{
var prop = DesignerProperties.IsInDesignModeProperty;
bool isInDesignMode = (bool)DependencyPropertyDescriptor.FromProperty(prop,
typeof(FrameworkElement)).Metadata.DefaultValue;
if (isInDesignMode)
return;
if (IsMouseCaptured)
{
Point currentPoint = Mouse.GetPosition(this);
velocity = previousPoint - currentPoint;
previousPoint = currentPoint;
}
else
{
if (velocity.Length > 1)
{
DoStandardScrolling();
}
else
{
if (UseSnapBackScrolling)
{
int mx = (int)sv.HorizontalOffset % (int)ActualWidth;
if (mx == 0)
return;
int ix = (int)sv.HorizontalOffset / (int)ActualWidth;
double snapBackX = mx > ActualWidth / 2 ? (ix + 1) * ActualWidth : ix * ActualWidth;
sv.ScrollToHorizontalOffset(sv.HorizontalOffset + (snapBackX - sv.HorizontalOffset) / 4.0);
}
else
{
DoStandardScrolling();
}
}
}
}
#endregion
#region Overrides
public override void OnApplyTemplate()
{
sv = (ScrollViewer)Template.FindName("PART_ScrollViewer", this);
base.OnApplyTemplate();
}
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (sv.IsMouseOver)
{
_mouseDownFlag = true;
scrollStartPoint = e.GetPosition(this);
scrollStartOffset.X = sv.HorizontalOffset;
scrollStartOffset.Y = sv.VerticalOffset;
}
base.OnPreviewMouseLeftButtonDown(e);
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (_mouseDownFlag)
{
Point currentPoint = e.GetPosition(this);
Point delta = new Point(scrollStartPoint.X - currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
if (Math.Abs(delta.X) > PixelsToMoveToBeConsideredScroll ||
Math.Abs(delta.Y) > PixelsToMoveToBeConsideredScroll)
{
scrollTarget.X = scrollStartOffset.X + delta.X;
scrollTarget.Y = scrollStartOffset.Y + delta.Y;
sv.ScrollToHorizontalOffset(scrollTarget.X);
sv.ScrollToVerticalOffset(scrollTarget.Y);
if (!this.IsMouseCaptured)
{
if ((sv.ExtentWidth > sv.ViewportWidth) ||
(sv.ExtentHeight > sv.ViewportHeight))
{
_savedCursor = this.Cursor;
this.Cursor = Cursors.ScrollWE;
}
this.CaptureMouse();
}
}
}
base.OnPreviewMouseMove(e);
}
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
bool mouseDownFlag = _mouseDownFlag;
_mouseDownFlag = false;
if (this.IsMouseCaptured)
{
this.Cursor = _savedCursor;
this.ReleaseMouseCapture();
}
else if (mouseDownFlag)
{
}
_savedCursor = null;
base.OnPreviewMouseLeftButtonUp(e);
}
#endregion
}
}
Default Styles Applied
And as this is a lookless control. Just for completeness, here are the default Style
s that get applied to the Panorama
control
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PanoramaControl">
<local:PanoramaGroupWidthConverter x:Key="conv" />
<Style x:Key="headerLabelStyle" TargetType="Label">
<Setter Property="FontSize"
Value="{Binding RelativeSource={RelativeSource
AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontSize}" />
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource
AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontColor}" />
<Setter Property="FontFamily"
Value="{Binding RelativeSource={RelativeSource
AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontFamily}" />
<Setter Property="FontWeight"
Value="Normal" />
<Setter Property="HorizontalAlignment"
Value="Left" />
<Setter Property="HorizontalContentAlignment"
Value="Left" />
<Setter Property="VerticalAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="Margin"
Value="10,0,0,20" />
</Style>
<DataTemplate DataType="{x:Type local:PanoramaGroup}">
<DataTemplate.Resources>
<Style x:Key="transparentListBoxItemStyle"
TargetType="{x:Type ListBoxItem}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
Color="Transparent" />
</Style.Resources>
<Setter Property="Padding"
Value="0" />
<Setter Property="Margin"
Value="0" />
</Style>
</DataTemplate.Resources>
<DockPanel LastChildFill="True" Background="Transparent">
<Label Style="{StaticResource headerLabelStyle}"
Content="{Binding Header}"
DockPanel.Dock="Top" />
<ListBox ItemsSource="{Binding Tiles}"
SelectionMode="Single"
BorderThickness="0"
BorderBrush="Transparent"
Background="Transparent"
IsSynchronizedWithCurrentItem="True"
ItemContainerStyle="{StaticResource transparentListBoxItemStyle}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Height="{Binding
RelativeSource={RelativeSource
AncestorType={x:Type local:Panorama},
Mode=FindAncestor},
Path=GroupHeight}">
<WrapPanel.Width>
<MultiBinding Converter="{StaticResource conv}">
<Binding Path="ItemBox"
RelativeSource="{RelativeSource
AncestorType={x:Type local:Panorama},
Mode=FindAncestor}" />
<Binding Path="GroupHeight"
RelativeSource="{RelativeSource
AncestorType={x:Type local:Panorama},
Mode=FindAncestor}" />
<Binding RelativeSource="{RelativeSource
AncestorType={x:Type ListBox},
Mode=FindAncestor}" />
</MultiBinding>
</WrapPanel.Width>
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ListBox>
</DockPanel>
</DataTemplate>
<Style TargetType="{x:Type local:Panorama}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<ControlTemplate.Resources>
<Style TargetType="{x:Type ScrollViewer}">
<Setter Property="HorizontalScrollBarVisibility"
Value="Hidden" />
<Setter Property="VerticalScrollBarVisibility"
Value="Hidden" />
</Style>
</ControlTemplate.Resources>
<ScrollViewer x:Name="PART_ScrollViewer"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ItemsPresenter Margin="0"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
It really isn't that difficult actually. Essentially the Panorama
control is a specialized ItemsControl
that hosts its items (PanoramaGroup
)
inside a ScrollViewer
, and each PanoramaGroup
also hosts
a number of custom tiles (which you would declare as ViewModels say, and implement a DataTemplate
for) inside a ListBox
which is part
of the PanoramaGroup
default style. The look and feel of the ViewModel for the tile is down to you and can be applied via your own DataTemplate
.
As for the scrolling, that is all handled inside of the Panorama
control and it
is all mouse manipulation code. The only point worth mentioning is that you may
pick your scrolling type by setting the UseSnapBackScrolling
which you can set on the Panorama
control.
How To Use It In Your Own Applications
There are really only a couple of things you need to do to use it in your own applications, these are outlined below. There is also a demo app attached which
shows how to create the Panorama
as shown in this article.
1. Create A Custom ViewModel
This is really easy and you have pretty much full control over what your implementing class looks like.
Here is a really simple tile example:
public class PanoramaTileViewModel : INPCBase
{
private IMessageBoxService messageBoxService;
private Timer liveUpdateTileTimer = new Timer();
public PanoramaTileViewModel(IMessageBoxService messageBoxService, string text, string imageUrl, bool isDoubleWidth)
{
if (isDoubleWidth)
{
liveUpdateTileTimer.Interval = 1000;
liveUpdateTileTimer.Elapsed += LiveUpdateTileTimer_Elapsed;
liveUpdateTileTimer.Start();
}
this.messageBoxService = messageBoxService;
this.Text = text;
this.ImageUrl = imageUrl;
this.IsDoubleWidth = isDoubleWidth;
this.TileClickedCommand = new SimpleCommand<object, object>(ExecuteTileClickedCommand);
}
void LiveUpdateTileTimer_Elapsed(object sender, ElapsedEventArgs e)
{
if (Counter < 10)
Counter++;
else
Counter = 0;
NotifyPropertyChanged("Counter");
}
public int Counter { get; set; }
public string Text { get; private set; }
public string ImageUrl { get; private set; }
public bool IsDoubleWidth { get; private set; }
public ICommand TileClickedCommand { get; private set; }
public void ExecuteTileClickedCommand(object parameter)
{
messageBoxService.ShowMessage(string.Format("you clicked {0}", this.Text));
}
}
This simple demo ViewModel class provides the ability to:
- Show an image
- Stretch to being double width tile
- Show some text (if you want to, this demo shows this text as a
ToolTip
)
- Respond to being clicked
- Allow live updates to tiles via
INotifyPropertyChanged
bindings (the way I am doing it in the demo app is not that typical, but this is not how you would
normally do things
anyway, you would be hitting web services / databases / WCF services etc., not using a Timer
, it is just to demonstrate that you can live update
the tiles anyway you want via INotifyPropertyChanged
bindings)
You will note that my tile example here is fairly dumb for brevity, but you could have it animating/streaming data, whatever you like really, and the Panorama
control would just show that data. Basically it is all standard XAML, so anything XAML does should work.
2. Define a DataTemplate To Suit Your ViewModel Class
The next thing you need to do is create your own DataTemplate
. This must be what you want to see,
I can't help you there, but as you have just created your own ViewModel class, it's really just a question
of designing the look and feel of what you want to see for a given tile based on its current data value.
Here is a DataTemplate
for the demo app's PanoramaTileViewModel
class:
<DataTemplate DataType="{x:Type local:PanoramaTileViewModel}">
<Border x:Name="bord"
BorderThickness="2"
BorderBrush="{Binding RelativeSource={RelativeSource Mode=Self},
Path=Background}"
Background="{Binding RelativeSource={RelativeSource
AncestorType={x:Type pan:Panorama},
Mode=FindAncestor},
Path=TileColorPair[0]}"
Width="120" Height="120" Margin="0">
<StackPanel Orientation="Horizontal">
<Button Command="{Binding TileClickedCommand}">
<Button.Template>
<ControlTemplate>
<Image x:Name="img"
Source="{Binding ImageUrl}"
Width="100"
Height="100"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ToolTip="{Binding Text}" >
</Image>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsDoubleWidth}" Value="True">
<Setter TargetName="img"
Property="HorizontalAlignment"
Value="Left" />
<Setter TargetName="img"
Property="Margin"
Value="10,0,0,0" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
<Grid Margin="30,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<Ellipse Stroke="White"
StrokeThickness="2"
Width="50"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Height="50" Fill="Transparent"/>
<Label x:Name="liveUpdate"
Content="{Binding Counter}"
Visibility="Collapsed"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Foreground="White"
FontFamily="Segoe UI"
FontSize="30"
FontWeight="DemiBold"/>
</Grid>
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource
AncestorType={x:Type ListBoxItem}, Mode=FindAncestor},
Path=IsSelected}"
Value="True">
<Setter TargetName="bord"
Property="BorderBrush"
Value="{Binding RelativeSource={RelativeSource
AncestorType={x:Type pan:Panorama}, Mode=FindAncestor},
Path=TileColorPair[1]}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsDoubleWidth}"
Value="True">
<Setter TargetName="bord"
Property="Width"
Value="240" />
<Setter TargetName="liveUpdate"
Property="Visibility"
Value="Visible" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
3. Create an IEnumerable<PanoramaGroup> And Use That With the Panorama Control
Believe it or not, that is almost enough to get you there, all you need to do now is create either some manual UI elements (boo no one wants that, we want MVVM to do that for us), or create a ViewModel to supply items to an instance of a Panorama
control. Let's see a dead simple demo ViewModel which provides the
Items for a Panorama
control.
public class MainWindowViewModel : INPCBase
{
private Random rand = new Random(DateTime.Now.Millisecond);
private List<DummyTileData> dummyData = new List<DummyTileData>();
private IMessageBoxService messageBoxService;
public MainWindowViewModel(IMessageBoxService messageBoxService)
{
this.messageBoxService = messageBoxService;
dummyData.Add(new DummyTileData("Add", @"Images/Add.png"));
dummyData.Add(new DummyTileData("Adobe", @"Images/Adobe.png"));
dummyData.Add(new DummyTileData("Android", @"Images/Android.png"));
dummyData.Add(new DummyTileData("Author", @"Images/Author.png"));
dummyData.Add(new DummyTileData("Blogger", @"Images/Blogger.png"));
dummyData.Add(new DummyTileData("Copy", @"Images/Copy.png"));
dummyData.Add(new DummyTileData("Delete", @"Images/Delete.png"));
dummyData.Add(new DummyTileData("Digg", @"Images/Digg.png"));
dummyData.Add(new DummyTileData("Edit", @"Images/Edit.png"));
dummyData.Add(new DummyTileData("Facebook", @"Images/Facebook.png"));
dummyData.Add(new DummyTileData("GMail", @"Images/GMail.png"));
dummyData.Add(new DummyTileData("RSS", @"Images/RSS.png"));
dummyData.Add(new DummyTileData("Save", @"Images/Save.png"));
dummyData.Add(new DummyTileData("Search", @"Images/Search.png"));
dummyData.Add(new DummyTileData("Trash", @"Images/Trash.png"));
dummyData.Add(new DummyTileData("Twitter", @"Images/Twitter.png"));
dummyData.Add(new DummyTileData("VisualStudio", @"Images/VisualStudio.png"));
dummyData.Add(new DummyTileData("Wordpress", @"Images/Wordpress.png"));
dummyData.Add(new DummyTileData("Yahoo", @"Images/Yahoo.png"));
dummyData.Add(new DummyTileData("YouTube", @"Images/YouTube.png"));
List<PanoramaGroup> data = new List<PanoramaGroup>();
List<PanoramaTileViewModel> tiles = new List<PanoramaTileViewModel>();
for (int i = 0; i < 4; i++)
{
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(true));
tiles.Add(CreateTile(true));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
tiles.Add(CreateTile(false));
}
data.Add(new PanoramaGroup("Settings",
CollectionViewSource.GetDefaultView(tiles)));
PanoramaItems = data;
}
private PanoramaTileViewModel CreateTile(bool isDoubleWidth)
{
DummyTileData dummyTileData = dummyData[rand.Next(dummyData.Count)];
return new PanoramaTileViewModel(messageBoxService,
dummyTileData.Text, dummyTileData.ImageUrl, isDoubleWidth);
}
private IEnumerable<PanoramaGroup> panoramaItems;
public IEnumerable<PanoramaGroup> PanoramaItems
{
get { return this.panoramaItems; }
set
{
if (value != this.panoramaItems)
{
this.panoramaItems = value;
NotifyPropertyChanged("CompanyName");
}
}
}
}
public class DummyTileData
{
public string Text { get; private set; }
public string ImageUrl { get; private set; }
public DummyTileData(string text, string imageUrl)
{
this.Text = text;
this.ImageUrl = imageUrl;
}
}
And here is an example of it being used in XAML using this demo ViewModel:
<Controls:MetroWindow
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pan="clr-namespace:PanoramaControl;assembly=PanoramaControl"
Width="960" MinWidth="960" Height="540">
<pan:Panorama Grid.Row="1" x:Name="pan"
UseSnapBackScrolling="{Binding ElementName=chkUseSNapBackScrolling,
Path=IsChecked, Mode=OneWay}"
ItemsSource="{Binding PanoramaItems}"
ItemBox="120"
GroupHeight="360"
Background="Transparent" />
</Controls:MetroWindow>
You can see that the Panorama
control requires very little setup at all.
One thing that you may need to do when using it is to set your own Brush
arrays,
to specify your primary and complimentary tile colors (of course, you may disregard this altogether if you want to as well, at the end of the day
the tile generation/color/look and feel is up to you).
But if you want to work with what this control offers, there are two DependencyProperty
values that can be used to set an array of Brush
objects
which can be used for the tile and complimentary color brushes. These are available using the
following Panorama
control DPs, where
these are expected to be of type Brush[]
:
TileColors
ComplimentaryTileColors
(this can be used inside your DataTemplate
to color borders etc., should you wish to use them,
which as I say is up to you)
Credit Where Credit Is Due
For the overall Window
Style I am using some code found here: http://mahapps.com/MahApps.Metro/
which strangely enough also offers a Panorama control, which looks great, but did not seem to work very well in terms of scrolling when I tried it.
Perhaps I was just doing it wrong, but I couldn't get how it did its scrolling at all.
The initial code had code that captured the mouse and as such did not allow input controls such as Button to be part of the tiles DataTemplate
, but thanks to some better mouse handlers supplied by Marc Jacobi, Buttons inside Tile are no issues at all, as can be seen from the new demo code DataTemplate