A couple of weeks ago someone suggested to me an idea for a particular type of layout panel. The idea was to be able to display a fixed set of tiles which are “wrapped” horizontally. So as you scroll horizontally the tiles will move off one edge of the panel and eventually re-appear on the other side. Think of how we commonly see the earth projected onto a rectangle – scrolling left or right to bring the particular geography into the centre of the screen.
I jumped into this without giving too much thought about how I would want to interact with the panel from an API perspective. For that reason its probably I’ve taken altogether the wrong approach but I thought I would post it up here anyway, ‘cause its unlikely I’ll take it any further than this proof of concept.
Define some XAML like so:
<panels:TilePanel x:Name="tilePanel" Height="170" Rows="3" Background="LightYellow" ClipToBounds="True" XOffset="{Binding ElementName=xOffsetSlider,Path=Value,Mode=OneWay}" YOffset="{Binding ElementName=yOffsetSlider,Path=Value,Mode=OneWay}" Scale="{Binding ElementName=scaleSlider,Path=Value,Mode=OneWay}"> <panels:TilePanel.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontSize" Value="32pt"/> <Setter Property="FontWeight" Value="Bold"/> <Setter Property="Padding" Value="3"/> <Setter Property="TextAlignment" Value="Center"/> <Setter Property="Background" Value="LightBlue"/> </Style> </panels:TilePanel.Resources> <TextBlock>A</TextBlock> <TextBlock>B</TextBlock> <TextBlock>C</TextBlock> <TextBlock>D</TextBlock> <TextBlock>E</TextBlock> <TextBlock>F</TextBlock> <TextBlock>G</TextBlock> <TextBlock>H</TextBlock> <TextBlock>I</TextBlock> <TextBlock>J</TextBlock> <TextBlock>K</TextBlock> <TextBlock>L</TextBlock> <TextBlock>M</TextBlock> <TextBlock>N</TextBlock> <TextBlock>O</TextBlock> <TextBlock>P</TextBlock> <TextBlock>Q</TextBlock> <TextBlock>R</TextBlock> <TextBlock>S</TextBlock> <TextBlock>T</TextBlock> <TextBlock>U</TextBlock> <TextBlock>V</TextBlock> <TextBlock>W</TextBlock> <TextBlock>X</TextBlock> <TextBlock>Y</TextBlock> <TextBlock>Z</TextBlock> <TextBlock>1</TextBlock> <TextBlock>2</TextBlock> <TextBlock>3</TextBlock> <TextBlock>4</TextBlock> <TextBlock>5</TextBlock> <TextBlock>6</TextBlock> <TextBlock>7</TextBlock> <TextBlock>8</TextBlock> <TextBlock>9</TextBlock> <TextBlock>0</TextBlock> </panels:TilePanel>
This generates the following layout where each of the items is laid out in three rows.
Items that are beyond the right edge are actually wrapped back to the left hand side (by subtracting the total width of all columns). So that when we supply a horizontal offset we get the desired effect.
Note that in the example each column width is automatically calculated based on the widest element. Likewise row heights are calculated based on the tallest elements. Although this allows for different widths/heights per column/row its works well when all items are the same width and height.
One example usage of this control would be as an ItemsPanel for an ItemsControl that displays tiled images that form a single scene. It so happens that ICE generates these types of tiles (at various zoom levels) for publishing its panoramas.
<ItemsControl ItemsSource="{Binding TileSet}"> <ItemsControl.ItemTemplate> <DataTemplate> <Border BorderThickness="1" BorderBrush="LightGray"> <Grid> <Image Source="{Binding ImagePath}" Stretch="Uniform" Height="Auto"/> <TextBlock Margin="4" TextAlignment="Center" FontSize="6pt" Foreground="White" Opacity="0.5"
VerticalAlignment="Center" HorizontalAlignment="Center"> <TextBlock.Text> <MultiBinding StringFormat="{}{0},{1}" > <Binding Path="ColumnIndex"/> <Binding Path="RowIndex"/> </MultiBinding> </TextBlock.Text> </TextBlock> </Grid> </Border> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <panels:TilePanel Margin="10" Background="LightYellow" ClipToBounds="True" Rows="{Binding TileSet.RowCount}" XOffset="{Binding ElementName=xOffsetSlider,Path=Value,Mode=OneWay}" YOffset="{Binding ElementName=yOffsetSlider,Path=Value,Mode=OneWay}" Scale="{Binding ElementName=scaleSlider,Path=Value,Mode=OneWay}"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl>
To load the ICE images into a collection of “Tiles” I wrote the following.
public static class TileLoader { public static TileSet LoadFromFolder( string path, int zoomLevel ) { var result = new TileSet(path, zoomLevel); if ( !Directory.Exists( path ) ) throw new ArgumentException( "The specified tile folder does not exist.", "path" ); if ( zoomLevel < 0 ) throw new ArgumentException( "The zoom level must be >= 0.", "zoomLevel" ); DirectoryInfo tileLevelFolder; try { var rootFolder = new DirectoryInfo( path ); var tileFolder = new DirectoryInfo( Path.Combine( path, "tiles" ) ); tileLevelFolder = new DirectoryInfo( Path.Combine( tileFolder.FullName, "l_" + zoomLevel ) ); } catch ( System.IO.IOException ex ) { throw new ArgumentException(
string.Format( "The tile set specified by the path {0} is corrupt.", path ), "path", ex ); } var column = 0; // Order the directories by numerical ascending, e.g. c_3 before c_21 foreach ( var columnFolder in tileLevelFolder.GetDirectories( "c_*" )
.OrderBy( d => int.Parse( d.Name.Substring( 2 ) ) ) ) { var row = 0; foreach ( var imageFile in columnFolder.GetFiles( "tile_*.wdp" )
.OrderBy( i => int.Parse( i.Name.Substring( 5, i.Name.IndexOf( '.' ) - 5 ) ) ) ) { var tile = new Tile() { ColumnIndex = column, RowIndex = row, ImagePath = imageFile.FullName }; result.Add( tile ); row++; if ( result.RowCount < row ) result.RowCount = row; } column++; } return result; } } [DebuggerDisplay("Column: {ColumnIndex}, Row: {RowIndex}, {ImagePath}")] public class Tile { public string ImagePath { get; set; } public int ColumnIndex { get; set; } public int RowIndex { get; set; } } public class TileSet : List<Tile> { public TileSet( string path, int zoomLevel ) { Path = path; ZoomLevel = zoomLevel; } public string Path { get; protected set; } public int ZoomLevel { get; protected set; } public int RowCount { get; set; } }
The panel’s arrange override code is as follows:
protected override Size ArrangeOverride(Size finalSize) { int row = 0; int column = 0; double fullWidth = _columnWidths.Sum(); double fullHeight = _rowHeights.Sum(); int maxColumns = (int) Math.Ceiling( (double) finalSize.Width / (double) _maxChildWidth); double x = -( XOffset * _maxChildWidth ); double y = - YOffset; foreach (UIElement child in Children) { x = x % fullWidth; if (x >= Math.Min(_maxChildWidth * maxColumns, fullWidth - _maxChildWidth + 1)) x -= fullWidth; else if (x < - _maxChildWidth) x += fullWidth; if ( x > -_maxChildWidth && x <= _maxChildWidth * maxColumns && y > -_maxChildHeight && y <= fullHeight ) { child.Arrange(
new Rect( new Point( x * Scale, y * Scale ),
new Size( child.DesiredSize.Width * Scale, child.DesiredSize.Height * Scale ) ) ); child.Visibility = Visibility.Visible; } else { child.Visibility = Visibility.Hidden; child.Arrange( new Rect( new Point( 0, 0 ), new Size( child.DesiredSize.Width * Scale, child.DesiredSize.Height * Scale ) ) ); } y += _rowHeights[ row ]; row = ( row + 1 ) % Rows; if ( row == 0 ) { x += _columnWidths[ column ]; //column * _childWidth ; y = -YOffset ; column++; } } //RenderTransform = new ScaleTransform(Scale, Scale, 0.0, YOffset); return finalSize; }
This is far from complete – sample project can be found here. [It’s a VS2010 project but should be easy enough to re-assemble for VS2008.]
There has been plenty of talk over the last handful of years about how we should be writing applications that are device resolution independent. For example, designing screen elements in terms of real world measurements like millimetres (or for the metrically challenged – inches). There is no point designing a button that is designed to be touched by a large thumb at a resolution of 100 pixels by 40 pixels when its deployed to a 300 ppi (pixels per inch) device.
In the past I’ve read a few articles that have claimed getting to the 300 ppi mark would mean that screens would become as readable as paper (ignoring light emitting vs. light absorbing for a moment). Well, I don’t know about that but I’ve finally got myself a (near) 300 ppi device – and it is an awesome screen. Shown here below at half pixel resolution and still larger than life size on most (96 ppi) desktop monitors (click for full image).
The latest HTC range comes with a WVGA resolution screen (480 x 800 pixels) crammed into a screen that is only 42 x 70 mm. By my calculations that is 290 ppi compared to the blurry old iPhone 3Gs at 163 ppi. Now all we need is say a 24” widescreen LCD monitor at this resolution – hmm… that’s around 5902 x 3688 pixels!
For the last week and a half I’ve been lucky enough to take some leave and work on some of my own (unpaid) projects. Here’s a few simple observations from that experience…
The view from my study window is better than the view from my third floor city office.
The coffee is better, closer and comes with snacks.
No background chatter, instead I have the rare opportunity to really crank up the music – awesome boost to developer productivity!
No 2+ hour commute. On the surface this would seem to be a huge benefit, but it does come with drawbacks. My daily commute normally involves walking 4km and I get precious little other daily exercise these days. Also, commute time via bus is my allocated reading and podcast listening time – so I’ve been getting a little behind there.
I also got to watch my son participate in his first Grand Prix event – priceless!
Overall a great experience. I thoroughly recommend it!
I wonder if they’d notice at work if I took a couple more weeks off?
I had to remove the source code from my previous post because it was doing weird things to my RSS feed. For those that may be interested though a sample project file can be found here. Note that its far from a finished control – the animation that happens when you click on it was just me messing around with Blend and the intention is for it to allow full navigation and selection.
XBAP demo (27kb) can be launched from here – note that XBAP version doesn’t have any drop shadows.
I’m on an insane spending spree at the moment. Its a combination of the fast approaching end of the financial year and a series of routine mishaps/malfunctions – reversing into the garage roller door (no I wasn’t driving), faulty speaker on my 4 year old i-Mate JAM, grossly overpriced kitchen appliances, attending conferences etc.
Inspired by what I saw at Remix regarding the launch of Windows Mobile 6.5 I figured that since my phone is on the fritz now must be the time to get a new one. For me the phone has to be a Windows Mobile device and by all the reviews I’ve looked at in the last couple of days the HTC devices seem to be the most well thought of.
So the order is placed. I should be getting an unlocked HTC Touch Diamond2 in the next few days. Cooked ROMS running Windows Mobile 6.5 are already available and I already have the WM6.5 SDK installed on my dev box.
I had a lot of fun late this afternoon putting together a Desk Calendar UserControl. The idea was triggered by me using a screenshot of the Vista Calendar Gadget in my last post. Also I’m working on a project at the moment that could use a nice date/calendar display and my previous WinForms attempts never looked that great.
Believing that imitation is the greatest form of flattery – here is my new Desk Calendar UserControl in all its glory. Of course being WPF its fully zoomable – no nasty bitmaps here – click the image to see it at higher res.
For the moment the control is read-only, although I did start working on a “flip page” animation. The layout was done exclusively in Blend (v3 Preview) hence the mark-up has some redundant elements and over-precise co-ords. In general though I was really happy with how easy this was to put together using Blend – the product has certainly matured well.
[Edit: Source code and XBAP demo can be found on subsequent post here.]
CodeCampSA organised by the Adelaide .NET User Group (ADNUG) is on again this year on the weekend of 18-19th July. Same venue as last year at Uni SA “City West” campus. For more details about the venue and to keep up to date on who will be speaking (and what they’ll be speaking about) visit the new website at www.codecampsa.com. Kudos go to David Gardiner for putting the site together!
So if you are in Adelaide make sure you take some time out of your weekend to come along. I’ll be amongst those presenting but don’t let that deter you!
My pick is Matt Morphett’s. The talk by Damian Edwards and Tatham Oddie was also well received (at least according to twitter) [being about website development and standards it had zero interest for me personally so I skipped it].
Here’s another quick WPF custom control derived from Panel. This one is a cross between a StackPanel and a WrapPanel. It supports two modes of layout – Inline and Block.
Inline elements are wrapped left to right much as the same as a WrapPanel with the default Orientation = Horizontal.
Block elements are stacked as per a StackPanel with the default Orientation = Vertical.
The benefit of combining (a simplified version) of this logic into one layout control is that an attached property can then be used on a per child basis to determine the layout (LayoutMode = Block or Inline). This is even more convenient when the property is set via an implicit style, e.g. all TextBlocks set to Inline by default, all Buttons as Block.
I also added some dependency properties for Padding (space between edge and top/left/right/bottom-most controls) and InternalPadding (the vertical and horizontal spacing between wrapped/stacked controls).
<panels:BoxPanel Height="Auto" Background="PaleGreen" InternalPadding="4,2" Padding="50,8,8,8"> <TextBlock>Some inline text.</TextBlock> <TextBlock>Followed by some more inline text.</TextBlock> <TextBlock>Followed by yet some more inline text.</TextBlock> <TextBlock panels:BoxPanel.LayoutMode="Block">A new paragraph.</TextBlock> <Button panels:BoxPanel.LayoutMode="Block">A Button</Button> <TextBlock>Below are three buttons in a line</TextBlock> <Button panels:BoxPanel.LayoutMode="Inline">One</Button> <Button panels:BoxPanel.LayoutMode="Inline">Two</Button> <Button panels:BoxPanel.LayoutMode="Inline">Three</Button> <panels:BoxPanel Background="LightYellow" Padding="10,2" InternalPadding="4,0"> <TextBlock>The</TextBlock> <Image Source="Passport_Photo_with_blue_background_half_size.png" Width="24"/> <TextBlock>End.</TextBlock> </panels:BoxPanel> </panels:BoxPanel>
Sound a bit obscure? Trust me, I had a reason
.
I’ve been spending a little more time recently with Visual Studio 2010 Beta 1. In particular building a solution from the ground up (including TFS integration) and doing a fair bit of prototyping. Some of the new editor improvements in VS2010 are really pretty neat.
One of the first changes that you notice in the editor of course is the changes to intellisense. It now includes auto-filtering, but not just by a “starts-with” search but using more of a “contains” approach. This includes the ability to search for multi-word names using abbreviations, e.g. “AQN” to match “AssemblyQualifiedName.“ Personally I think the filtering works particularly well. Having used Resharper’s intellisense filtering I wasn’t to keen on the idea, I find that implementation to be slow and it somehow feels invasive. The VS2010 implementation IMHO is much better.
I’ve started using the “consume-first” toggle, which is a great way to prevent Visual Studio from being over-zealous with auto-completion. This is useful when you’re building code against a set of yet-to-be-written APIs (i.e. consuming the API first).
Just press the Ctrl+Alt+Space combination and auto-completion is suspended.
The code generation options have also had a tweak with the “Generate other…” dialog.
Lots of good options here – particularly being able to specify in which project to create the new file.
(Refer here for more VS2010 code-focused changes).
{sigh} It’s getting harder and harder to go back to Visual Studio 2008.