Monday, May 12, 2008

Scrolling, without using ScrollViewer!

The main objective when building SimpleViewer-SL was to keep the executable as small as possible and hence I didn't have the option of using ScrollViewer. I still needed scrolling functionality though and hence had to take some alternate approach. This post is a result of my findings. There is possibly no reason for anyone to take this approach, unless there is a compelling reason to not use ScrollViewer, but it might prove useful!

So how did I go about implementing scrolling? The trick is to contain the visible area and use TranslateTransform.

Let's look at the Xaml first
<UserControl x:Class="SimpleViewer_SL.Thumbnails"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid x:Name="LayoutThumbnailsViewport" HorizontalAlignment="Left">
<Grid x:Name="LayoutThumbnails" HorizontalAlignment="Left">
<Grid.RenderTransform>
<TranslateTransform x:Name="GridTranslate" />
</Grid.RenderTransform>
<Grid.Resources>
<Storyboard x:Name="GridScrollStoryboard">
<DoubleAnimation x:Name="GridScrollAnimation" Storyboard.TargetName="GridTranslate" Storyboard.TargetProperty="X" Duration="00:00:00.5"/>
</Storyboard>
</Grid.Resources>
</Grid>
</Grid>
<Canvas x:Name="RightNav" Height="25" Width="30" Background="Transparent" HorizontalAlignment="Right">
<Path Height="25" Width="30" Stretch="Fill"
Data="F1 M 0,50L 130,50L 80,0L 130,0L 200,70L 130,140L 80,140L 130,90L 0,90L 0,50 Z">
</Path>
</Canvas>
<Canvas x:Name="LeftNav" Height="25" Width="30" Background="Transparent" HorizontalAlignment="Left">
<Path Height="25" Width="30" Stretch="Fill"
Data="F1 M 200,90L 70,90L 120,140L 70,140L 0,70L 70,0L 120,0L 70,50L 200,50L 200,90 Z">
</Path>
</Canvas>
</Grid>
</UserControl>


What we are trying to do here is display the thumbnails in a grid, but, as you can see from the above Xaml, I have had to create 3 grids to implement scrolling.
  • LayoutThumbnails - This is the grid which will host the thumbnails
  • LayoutThumbnailsViewport - This is the grid which will host the translated thumbnails
  • LayoutRoot - This is the grid which will host the clipped thumbnails
  • GridTranslate - This exposes the translate transform of LayoutThumbnails
  • GridScrollStoryboard - This is the storyboard that animates GridTranslate to simulate (horizontal) scrolling
Now for some code
public partial class Thumbnails : UserControl
{
Page _parent = null;
int _borderWidth = 2;
int _selectedThumbnailIndex = -1;
int _rowCount = 0;
int _colCount = 0;
int _totalColCount = 0;
int _numPages = 0;
int _currPage = 0;
Size _thumbSize = new Size { Width = 65, Height = 65 };
int _borderPlusPaddingWidth = 0;

public double ControlWidth { get { return (_thumbSize.Width + (_borderPlusPaddingWidth * 2)) * _colCount; }}

public Thumbnails(Page parent)
{
_parent = parent;
_rowCount = _parent.Settings.ThumbnailRows;
_colCount = _parent.Settings.ThumbnailColumns;
_totalColCount = (int)(Math.Ceiling((double)_parent.Settings.ImagesSettings.Count / _rowCount));
_numPages = (int)(Math.Ceiling((double)_totalColCount / _colCount));
_borderPlusPaddingWidth = _parent.Settings.ThumbPadding + _borderWidth;

InitializeComponent();

LayoutRoot.Width = ControlWidth; // ****1****
LayoutThumbnailsViewport.Width = ControlWidth * _numPages; // ****2****

CreateLayout();
DrawThumbnails();
}

int CreateLayout() {
// Thumbnails
LayoutRoot.RowDefinitions.Add(new RowDefinition());
LayoutThumbnailsViewport.SetValue(Grid.RowProperty, 0);

if (_numPages > 0) {
// Navigation Arrows
LayoutRoot.RowDefinitions.Add(new RowDefinition { Height = new GridLength(30) });
RightNav.SetValue(Grid.RowProperty, 1);
LeftNav.SetValue(Grid.RowProperty, 1);
}
}

void DrawThumbnails()
{
for (int row = 0; row < _rowCount; row++) {
LayoutThumbnails.RowDefinitions.Add(new RowDefinition { Height = new GridLength(_thumbSize.Height + (_borderPlusPaddingWidth * 2)) });
}

for (int col = 0; col < _colCount * _numPages; col++) {
LayoutThumbnails.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(_thumbSize.Width + (_borderPlusPaddingWidth * 2)) });
}

int imageIndex = 0;
int startCol = 0;
for (int page = 0; page < _numPages; page++) {
for (int row = 0; row < _rowCount; row++) {
for (int col = 0; col < _colCount && imageIndex < _parent.Settings.ImagesSettings.Count; col++) {
CreateImage(row, col + startCol, imageIndex);
imageIndex++;
}
}
startCol += _colCount;
}

LeftNav.MouseLeftButtonUp += (sender, e) => {
ScrollLeft();
};

RightNav.MouseLeftButtonUp += (sender, e) => {
ScrollRight();
};
}

public void CreateImage(int row, int col, int imageIndex)
{
Image thumb = new Image();
thumb.Source = new BitmapImage(new Uri(_parent.Settings.ThumbPath + _parent.Settings.ImagesSettings[imageIndex].Name, UriKind.Relative));

Border border = new Border {
Width = _thumbSize.Width + (_borderWidth*2),
Height = _thumbSize.Height + (_borderWidth*2),
BorderThickness = new Thickness(_borderWidth),
BorderBrush = new SolidColorBrush(_parent.Settings.FrameColor),
Margin = new Thickness(_parent.Settings.ThumbPadding),
Opacity = 0.6
};
border.SetValue(Grid.RowProperty, row);
border.SetValue(Grid.ColumnProperty, col);
border.SetValue(NameProperty, "Image" + imageIndex.ToString());
border.Tag = imageIndex.ToString(); // Tag can only handle Strings as of SL2 B1 release

border.Child = thumb;
LayoutThumbnails.Children.Add(border);

border.MouseLeftButtonUp += (sender, e) => {
Rect LayoutRootRect = new Rect(0, 0, LayoutRoot.RenderSize.Width, LayoutRoot.RenderSize.Height);
if (LayoutRootRect.Contains(e.GetPosition(LayoutRoot))) { // ****3****
Border currentBorder = (Border)sender;
SelectedThumbnailIndex = Int32.Parse((string)currentBorder.Tag);
}
};
}

bool CanScrollRight { get { return ((_currPage + 1) < _numPages); }}
bool CanScrollLeft { get { return (_currPage > 0); }}

void ScrollRight()
{
if (CanScrollRight) {
_currPage++;
GridScrollAnimation.To = -(_thumbSize.Width + (_borderPlusPaddingWidth * 2)) * _colCount * _currPage;
GridScrollStoryboard.Begin();
}
}

void ScrollLeft()
{
if (CanScrollLeft) {
_currPage--;
GridScrollAnimation.To = -(_thumbSize.Width + (_borderPlusPaddingWidth * 2)) * _colCount * _currPage;
GridScrollStoryboard.Begin();
}
}
}

Most of the code is self explanatory but I would like to elaborate on a few key points which is highlighted in the code:
  1. The width of LayoutThumbnailsViewport should be equal to or greater than the width of the entire grid. If the width is not explicitly set or is less than the width of the entire grid, translatetransform will clip the contents that lie outside the area of the grid
  2. The width of LayoutRoot should be set to the viewing area. This should be less than the width of LayoutThumbnailsViewport if scrolling is desired
  3. One undesired effect of creating this layout is that the underlying grid (LayoutThumbnails) and hence the thumbnails receive mouse events even for the contents that is outside the visible area (LayoutRoot). To circumvent this I had to put this check - if (LayoutRootRect.Contains(e.GetPosition(LayoutRoot))) - in the border.MouseLeftButtonUp event handler. (Note that border is synonymous with thumbnail)

3 comments:

Anonymous said...

Can u please tell me , when i am creating an Object of Page then .Settings is not coming , DO i need to include any namespace , bec for me this is not working

CAn u please tell me the nsamespace

Anonymous said...

Can u please tell me what namespace has to be included for .Settings because i have created an Object of Page but .Settings is not coming

Thank u in advance

Nidhi said...

.Settings i am not getting please tell me if i need to include any namespace