WPF

A really simply slider puzzle in WPF

Within WPF there it is possible to create a Brush based on an Image, this is known as an ImageBrush. You are then able to use this ImageBrush anywhere that you would be able to use another Brush. One of the less documented features of the ImageBrush is its ViewBox property. Using the ViewBox property you are able to grab certain portions of the original image. You may have seen this in use in the Blendables zoom box, where a small box shows a zoomed section of the full image.

I was thinking about this the other day, and was a little miffed with the world, so decided to write a small puzzle in WPF. I used the ImageBrush and its ViewBox property to create a small 9 square slider puzzle just like the ones you got for MXAS in your stocking (cracker if you were very lucky).

You know like this.

image

And here is all the code that makes it work

 

XAML

   1:  <Window x:Class="SlideGame.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      Title="Window1" Height="300" Width="300">
   5:      <DockPanel LastChildFill="True">
   6:   
   7:   
   8:          <Button x:Name="btnPickImage" Content="Pick Image" 
   9:                  Click="btnPickImage_Click" DockPanel.Dock="Top"/>
  10:          <!-- Puzzle -->    
  11:          <Grid x:Name="gridMain">
  12:              <Grid.ColumnDefinitions>
  13:                  <ColumnDefinition Width="*"/>
  14:                  <ColumnDefinition Width="*"/>
  15:                  <ColumnDefinition Width="*"/>
  16:              </Grid.ColumnDefinitions>
  17:   
  18:              <Grid.RowDefinitions>
  19:                  <RowDefinition Height="*"/>
  20:                  <RowDefinition Height="*"/>
  21:                  <RowDefinition Height="*"/>
  22:              </Grid.RowDefinitions>
  23:   
  24:          </Grid>
  25:   
  26:   
  27:      </DockPanel>
  28:  </Window>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

 

C# Code behind

 

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Windows;
   6:  using System.Windows.Controls;
   7:  using System.Windows.Data;
   8:  using System.Windows.Documents;
   9:  using System.Windows.Input;
  10:  using System.Windows.Media;
  11:  using System.Windows.Media.Imaging;
  12:  using System.Windows.Navigation;
  13:  using System.Windows.Shapes;
  14:   
  15:  namespace SlideGame
  16:  {
  17:      /// <summary>
  18:      /// A simple 9 square sliding puzzle using an 
  19:      /// image that the user picks
  20:      /// </summary>
  21:      public partial class Window1 : Window
  22:      {
  23:          #region Data
  24:          BitmapImage image;
  25:          Image img;
  26:          List<Rectangle> initialUnallocatedParts = new List<Rectangle>();
  27:          List<Rectangle> allocatedParts = new List<Rectangle>();
  28:          #endregion
  29:   
  30:          #region Ctor
  31:          /// <summary>
  32:          /// Creates a new Window
  33:          /// </summary>
  34:          public Window1()
  35:          {
  36:              InitializeComponent();
  37:          }
  38:          #endregion
  39:   
  40:          #region Private Methods
  41:   
  42:          /// <summary>
  43:          /// Randomizes the tiles such that they are not
  44:          /// show in the order they were created
  45:          /// </summary>
  46:          private void RandomizeTiles()
  47:          {
  48:              Random rand = new Random(15);
  49:              int allocated = 0;
  50:              while (allocated != 8)
  51:              {
  52:                  int index = 0;
  53:                  if (initialUnallocatedParts.Count > 1)
  54:                  {
  55:                      index = (int)(rand.NextDouble() * 
  56:                          initialUnallocatedParts.Count);
  57:                  }
  58:                  allocatedParts.Add(initialUnallocatedParts[index]);
  59:                  initialUnallocatedParts.RemoveAt(index);
  60:                  allocated++;
  61:              }
  62:          }
  63:   
  64:          /// <summary>
  65:          /// Creates all the puzzles squares, which will either
  66:          /// be a portion of the original image or will be the
  67:          /// single blank puzzle square
  68:          /// </summary>
  69:          private void CreatePuzzleForImage()
  70:          {
  71:              initialUnallocatedParts.Clear();
  72:              allocatedParts.Clear();
  73:   
  74:              //row0
  75:              CreateImagePart(0, 0, 0.33333, 0.33333);
  76:              CreateImagePart(0.33333, 0, 0.33333, 0.33333);
  77:              CreateImagePart(0.66666, 0, 0.33333, 0.33333);
  78:              //row1
  79:              CreateImagePart(0, 0.33333, 0.33333, 0.33333);
  80:              CreateImagePart(0.33333, 0.33333, 0.33333, 0.33333);
  81:              CreateImagePart(0.66666, 0.33333, 0.33333, 0.33333);
  82:              //row2
  83:              CreateImagePart(0, 0.66666, 0.33333, 0.33333);
  84:              CreateImagePart(0.33333, 0.66666, 0.33333, 0.33333);
  85:              RandomizeTiles();
  86:              CreateBlankRect();
  87:   
  88:              int index = 0;
  89:              for (int i = 0; i < 3; i++)
  90:              {
  91:                  for (int j = 0; j < 3; j++)
  92:                  {
  93:                      allocatedParts[index].SetValue(Grid.RowProperty, i);
  94:                      allocatedParts[index].SetValue(Grid.ColumnProperty, j);
  95:                      gridMain.Children.Add(allocatedParts[index]);
  96:                      index++;
  97:                  }
  98:              }
  99:          }
 100:   
 101:          /// <summary>
 102:          /// Creates the single blank Rectangle for the puzzle
 103:          /// </summary>
 104:          private void CreateBlankRect()
 105:          {
 106:              Rectangle rectPart = new Rectangle();
 107:              rectPart.Fill = new SolidColorBrush(Colors.White);
 108:              rectPart.Margin = new Thickness(0);
 109:              rectPart.HorizontalAlignment = HorizontalAlignment.Stretch;
 110:              rectPart.VerticalAlignment = VerticalAlignment.Stretch;
 111:              allocatedParts.Add(rectPart);
 112:          }
 113:   
 114:          /// <summary>
 115:          /// Creates a ImageBrush using x/y/width/height params
 116:          /// to create a Viewbox to only show a portion of original
 117:          /// image. This ImageBrush is then used to fill a Rectangle
 118:          /// which is added to the internal list of unallocated
 119:          /// Rectangles
 120:          /// </summary>
 121:          /// <param name="x">x position in the original image</param>
 122:          /// <param name="y">y position in the original image</param>
 123:          /// <param name="width">the width to use</param>
 124:          /// <param name="height">the hieght to use</param>
 125:          private void CreateImagePart(double x, double y, double width, double height)
 126:          {
 127:              ImageBrush ib = new ImageBrush();
 128:              ib.Stretch = Stretch.UniformToFill;
 129:              ib.ImageSource = image;
 130:              ib.Viewport = new Rect(0, 0, 1.0, 1.0);
 131:              //grab image portion
 132:              ib.Viewbox = new Rect(x, y, width, height); 
 133:              ib.ViewboxUnits = BrushMappingMode.RelativeToBoundingBox;
 134:              ib.TileMode = TileMode.None;
 135:   
 136:              Rectangle rectPart = new Rectangle();
 137:              rectPart.Fill = ib;
 138:              rectPart.Margin = new Thickness(0);
 139:              rectPart.HorizontalAlignment = HorizontalAlignment.Stretch;
 140:              rectPart.VerticalAlignment = VerticalAlignment.Stretch;
 141:              rectPart.MouseDown += new MouseButtonEventHandler(rectPart_MouseDown);
 142:              initialUnallocatedParts.Add(rectPart);
 143:          }
 144:   
 145:          /// <summary>
 146:          /// Swaps the blank puzzle square with the square clicked if its a
 147:          /// valid move
 148:          /// </summary>
 149:          private void rectPart_MouseDown(object sender, MouseButtonEventArgs e)
 150:          {
 151:              //get the source Rectangle, and the blank Rectangle
 152:              //NOTE : Blank Rectangle never moves, its always the last Rectangle
 153:              //in the allocatedParts List, but it gets re-allocated to 
 154:              //different Gri Row/Column
 155:              Rectangle rectCurrent = sender as Rectangle;
 156:              Rectangle rectBlank = allocatedParts[allocatedParts.Count - 1];
 157:   
 158:              //get current grid row/col for clicked Rectangle and Blank one
 159:              int currentTileRow = (int)rectCurrent.GetValue(Grid.RowProperty);
 160:              int currentTileCol = (int)rectCurrent.GetValue(Grid.ColumnProperty);
 161:              int currentBlankRow = (int)rectBlank.GetValue(Grid.RowProperty);
 162:              int currentBlankCol = (int)rectBlank.GetValue(Grid.ColumnProperty);
 163:   
 164:              //create possible valid move positions
 165:              List<PossiblePositions> posibilities = new List<PossiblePositions>();
 166:              posibilities.Add(new PossiblePositions 
 167:                  { Row = currentBlankRow - 1, Col = currentBlankCol });
 168:              posibilities.Add(new PossiblePositions 
 169:                  { Row = currentBlankRow + 1, Col = currentBlankCol });
 170:              posibilities.Add(new PossiblePositions 
 171:                  { Row = currentBlankRow, Col = currentBlankCol-1 });
 172:              posibilities.Add(new PossiblePositions 
 173:                  { Row = currentBlankRow, Col = currentBlankCol + 1 });
 174:   
 175:              //check for valid move
 176:              bool validMove = false;
 177:              foreach (PossiblePositions position in posibilities)
 178:                  if (currentTileRow == position.Row && currentTileCol == position.Col)
 179:                      validMove = true;
 180:   
 181:              //only allow valid move
 182:              if (validMove)
 183:              {
 184:                  rectCurrent.SetValue(Grid.RowProperty, currentBlankRow);
 185:                  rectCurrent.SetValue(Grid.ColumnProperty, currentBlankCol);
 186:   
 187:                  rectBlank.SetValue(Grid.RowProperty, currentTileRow);
 188:                  rectBlank.SetValue(Grid.ColumnProperty, currentTileCol);
 189:              }
 190:              else
 191:                  return;
 192:          }
 193:   
 194:          /// <summary>
 195:          /// Allows user to pick a new image to create a puzzle for
 196:          /// </summary>
 197:          private void btnPickImage_Click(object sender, RoutedEventArgs e)
 198:          {
 199:              Microsoft.Win32.OpenFileDialog ofd = new Microsoft.Win32.OpenFileDialog();
 200:              ofd.Filter = "Image Files(*.BMP;*.JPG;*.GIF;*.PNG)|*.BMP;*.JPG;*.GIF;*.PNG" +
 201:                          "|All Files (*.*)|*.*";
 202:              ofd.Multiselect = false;
 203:              if (ofd.ShowDialog() == true)
 204:              {
 205:                  try
 206:                  {
 207:                      image = new BitmapImage(new Uri(ofd.FileName, 
 208:                          UriKind.RelativeOrAbsolute));
 209:                      img = new Image { Source = image };
 210:                      CreatePuzzleForImage();
 211:                  }
 212:                  catch
 213:                  {
 214:                      MessageBox.Show("Couldnt load the image file " + 
 215:                          ofd.FileName);
 216:                  }
 217:              }
 218:          }
 219:          #endregion
 220:      }
 221:   
 222:      #region PossiblePositions STRUCT
 223:      /// <summary>
 224:      /// Simply struct to store Row/Column data
 225:      /// </summary>
 226:      struct PossiblePositions
 227:      {
 228:          public int Row { get; set; }
 229:          public int Col { get; set; }
 230:      }
 231:      #endregion
 232:  }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

 

 

I have written a more detail account of how this works in this article at codeproject so if you want to download the code you can do it from there.

Advertisements
WPF

A Tabbable 3d WPF Panel

A while back I started work on a custom 3D WPF Panel, that uses a viewport3D to host items. I wanted my custom Panel to be equally at home as a standalone Panel or as replacement for the standard StackPanel that is often seen within ItemsControl. I also wanted the following functionality from my 3D Panel

 

  • The user is able to tab around the items using CTRL+TAB
  • The 3D Panel would not show all items in 3D space, but rather would show a viewable window, such that it saved memory. Its my own kind of virtualization
  • The 3D Panel would allow items to have reflection or not
  • The 3D Panel would raise an event with the current FrameworkElement, when the user stopped tabbing, which could then be used to show in a custom control that I also wrote

 

I have managed to achieve this, but not without the help of not just 1 but 2 WPF heavy hitters. It would not have been possible without Josh Smith or Dr WPF, both of whom have been excellent in supporting this article. Josh for his original Adorner idea, which was used to to host the viewport3D. Although I don’t use this any more, it was still a cool idea, so thanks Josh. And Dr WPF, for his excellent ConceptualPanel, that allows children to be neither Logical or Visual, by the use of a clever (very clever) disconnected list of FrameworkElements.

It appears that this little idea of mine truly got people thinking as it not only inspired me but it also inspired Josh and Dr WPF to create their own articles over at codeproject. Both Josh and I are using something from Dr WPF article.

Here are the links to both Joshs and Dr WPFs articles

 

 

This article has been a huge challenge and I believe represents some of the finest work that I have done, though like I say I would not have been able to do this without Dr WPFs ConceptualPanel, which is truly brilliant. So make sure to check that out.

Here is a video of my 3D tabbing Panel in action

Click the image or here to view the video

I would suggest waiting for the ENTIRE video to finish streaming then watch it. It will make most sense that way.

 

Here is a link to the full WPF 3D Tabbing Panel article

 

Oh and THANKS Dr WPF, I owe you one big style. If you ever need a tour guide in London, I’m your man.

WPF

Custom WPF Panels

Recently I wrote my own panel or 2 for WPF, and I just received a nice email from Rudi Grobler (one of my fellow WPF Disciples) stating that he had brought everyone’s custom WPF Panels together in one application. This is cool, and it can be found over at Rudis blog, right here

CodeProject, Introduction

Creating Custom Panels In WPF

WPF has a number of layout Panels that you could use straight out the box, there is

  • WrapPanel
  • StackPanel
  • Grid
  • Canvas
  • DockPanel

All of which are great, but occasionally you want something a little bit special. Whilst its probably true that you make most creations using a combination of the existing layouts, its sometimes just more convenient to wrap this into a custom Panel .

Now when creating custom Panels, there are just 2 methods that you need to override, these are  :

  • Size MeasureOverride(Size constraint)
  • Size ArrangeOverride(Size arrangeBounds)

One of the best articles I’ve ever seen on creating custom Panels is the article by Paul Tallett over at codeproject, Fisheye Panel, paraphrasing Pauls excellent article.

To get your own custom panel off the ground, you need to derive from System.Windows.Controls.Panel and implement two overrides: MeasureOverride and LayoutOverride. These implement the two-pass layout system where during the Measure phase, you are called by your parent to see how much space you’d like. You normally ask your children how much space they would like, and then pass the result back to the parent. In the second pass, somebody decides on how big everything is going to be, and passes the final size down to your ArrangeOverride method where you tell the children their size and lay them out. Note that every time you do something that affects layout (e.g., resize the window), all this happens again with new sizes.

So what am I trying to achieve with this blog, well I am working on a hobby project where I wanted a column based panel that wrapped to a new column, when it ran out of space in the current column. Now I could have just used a DockPanel, that contained loads of vertical  StackPanels, but that defeats what I am after. I want the Panel to work out how many items are in a column based on the available size.

So I set to work exploring, and I found an excellent start within the superb Pro WPF in C# 2008: Windows Presentation Foundation with .NET 3.5, by Mathew McDonald, so my code is largely based on Mathews book example.

It looks like this:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.Windows.Controls;
   5:  using System.Windows;
   6:  using System.Windows.Media;
   7:  
   8:  namespace CustomPanel
   9:  {
  10:      /// <summary>
  11:      /// A column based layout panel, that automatically
  12:      /// wraps to new column when required. The user
  13:      /// may also create a new column before an element
  14:      /// using the 
  15:      /// </summary>
  16:      public class ColumnedPanel : Panel
  17:      {
  18:  
  19:          #region Ctor
  20:          static ColumnedPanel()
  21:          {
  22:              //tell DP sub system, this DP, will affect
  23:              //Arrange and Measure phases
  24:              FrameworkPropertyMetadata metadata =
  25:                  new FrameworkPropertyMetadata();
  26:              metadata.AffectsArrange = true;
  27:              metadata.AffectsMeasure = true;
  28:              ColumnBreakBeforeProperty =
  29:                  DependencyProperty.RegisterAttached(
  30:                  "ColumnBreakBefore",
  31:                  typeof(bool), typeof(ColumnedPanel),
  32:                  metadata);
  33:          }
  34:          #endregion
  35:  
  36:          #region DPs
  37:  
  38:          /// <summary>
  39:          /// Can be used to create a new column with the ColumnedPanel
  40:          /// just before an element
  41:          /// </summary>
  42:          public static DependencyProperty ColumnBreakBeforeProperty;
  43:  
  44:          public static void SetColumnBreakBefore(UIElement element,
  45:              Boolean value)
  46:          {
  47:              element.SetValue(ColumnBreakBeforeProperty, value);
  48:          }
  49:          public static Boolean GetColumnBreakBefore(UIElement element)
  50:          {
  51:              return (bool)element.GetValue(ColumnBreakBeforeProperty);
  52:          }
  53:          #endregion
  54:  
  55:          #region Measure Override
  56:          // From MSDN : When overridden in a derived class, measures the 
  57:          // size in layout required for child elements and determines a
  58:          // size for the FrameworkElement-derived class
  59:          protected override Size MeasureOverride(Size constraint)
  60:          {
  61:              Size currentColumnSize = new Size();
  62:              Size panelSize = new Size();
  63:  
  64:              foreach (UIElement element in base.InternalChildren)
  65:              {
  66:                  element.Measure(constraint);
  67:                  Size desiredSize = element.DesiredSize;
  68:  
  69:                  if (GetColumnBreakBefore(element) ||
  70:                      currentColumnSize.Height + desiredSize.Height >
  71:                      constraint.Height)
  72:                  {
  73:                      // Switch to a new column (either because the 
  74:                      //element has requested it or space has run out).
  75:                      panelSize.Height = Math.Max(currentColumnSize.Height,
  76:                          panelSize.Height);
  77:                      panelSize.Width += currentColumnSize.Width;
  78:                      currentColumnSize = desiredSize;
  79:  
  80:                      // If the element is too high to fit using the 
  81:                      // maximum height of the line,
  82:                      // just give it a separate column.
  83:                      if (desiredSize.Height > constraint.Height)
  84:                      {
  85:                          panelSize.Height = Math.Max(desiredSize.Height,
  86:                              panelSize.Height);
  87:                          panelSize.Width += desiredSize.Width;
  88:                          currentColumnSize = new Size();
  89:                      }
  90:                  }
  91:                  else
  92:                  {
  93:                      // Keep adding to the current column.
  94:                      currentColumnSize.Height += desiredSize.Height;
  95:  
  96:                      // Make sure the line is as wide as its widest element.
  97:                      currentColumnSize.Width =
  98:                          Math.Max(desiredSize.Width,
  99:                          currentColumnSize.Width);
 100:                  }
 101:              }
 102:  
 103:              // Return the size required to fit all elements.
 104:              // Ordinarily, this is the width of the constraint, 
 105:              // and the height is based on the size of the elements.
 106:              // However, if an element is higher than the height given
 107:              // to the panel,
 108:              // the desired width will be the height of that column.
 109:              panelSize.Height = Math.Max(currentColumnSize.Height,
 110:                  panelSize.Height);
 111:              panelSize.Width += currentColumnSize.Width;
 112:              return panelSize;
 113:  
 114:          }
 115:          #endregion
 116:  
 117:          #region Arrange Override
 118:          //From MSDN : When overridden in a derived class, positions child
 119:          //elements and determines a size for a FrameworkElement derived
 120:          //class.
 121:  
 122:          protected override Size ArrangeOverride(Size arrangeBounds)
 123:          {
 124:              int firstInLine = 0;
 125:  
 126:              Size currentColumnSize = new Size();
 127:  
 128:              double accumulatedWidth = 0;
 129:  
 130:              UIElementCollection elements = base.InternalChildren;
 131:              for (int i = 0; i < elements.Count; i++)
 132:              {
 133:  
 134:                  Size desiredSize = elements[i].DesiredSize;
 135:  
 136:                  //need to switch to another column
 137:                  if (GetColumnBreakBefore(elements[i]) ||
 138:                      currentColumnSize.Height +
 139:                      desiredSize.Height >
 140:                      arrangeBounds.Height)
 141:                  {
 142:                      arrangeColumn(accumulatedWidth,
 143:                          currentColumnSize.Width,
 144:                          firstInLine, i, arrangeBounds);
 145:  
 146:                      accumulatedWidth += currentColumnSize.Width;
 147:                      currentColumnSize = desiredSize;
 148:  
 149:                      //the element is higher then the constraint - 
 150:                      //give it a separate column 
 151:                      if (desiredSize.Height > arrangeBounds.Height)
 152:                      {
 153:                          arrangeColumn(accumulatedWidth,
 154:                              desiredSize.Width, i, ++i, arrangeBounds);
 155:                          accumulatedWidth += desiredSize.Width;
 156:                          currentColumnSize = new Size();
 157:                      }
 158:                      firstInLine = i;
 159:                  }
 160:                  else //continue to accumulate a column
 161:                  {
 162:                      currentColumnSize.Height += desiredSize.Height;
 163:                      currentColumnSize.Width =
 164:                          Math.Max(desiredSize.Width,
 165:                          currentColumnSize.Width);
 166:                  }
 167:              }
 168:  
 169:              if (firstInLine < elements.Count)
 170:                  arrangeColumn(accumulatedWidth,
 171:                      currentColumnSize.Width,
 172:                      firstInLine, elements.Count,
 173:                      arrangeBounds);
 174:  
 175:              return arrangeBounds;
 176:          }
 177:          #endregion
 178:  
 179:          #region Private Methods
 180:          /// <summary>
 181:          /// Arranges a single column of elements
 182:          /// </summary>
 183:          private void arrangeColumn(double x,
 184:              double columnWidth, int start,
 185:              int end, Size arrangeBounds)
 186:          {
 187:              double y = 0;
 188:              double totalChildHeight = 0;
 189:              double widestChildWidth = 0;
 190:              double xOffset = 0;
 191:  
 192:              UIElementCollection children = InternalChildren;
 193:              UIElement child;
 194:  
 195:              for (int i = start; i < end; i++)
 196:              {
 197:                  child = children[i];
 198:                  totalChildHeight += child.DesiredSize.Height;
 199:                  if (child.DesiredSize.Width > widestChildWidth)
 200:                      widestChildWidth = child.DesiredSize.Width;
 201:              }
 202:  
 203:              //work out y start offset within a given column
 204:              y = ((arrangeBounds.Height - totalChildHeight) / 2);
 205:  
 206:  
 207:              for (int i = start; i < end; i++)
 208:              {
 209:                  child = children[i];
 210:                  if (child.DesiredSize.Width < widestChildWidth)
 211:                  {
 212:                      xOffset = ((widestChildWidth -
 213:                          child.DesiredSize.Width) / 2);
 214:                  }
 215:  
 216:                  child.Arrange(new Rect(x + xOffset, y,
 217:                      child.DesiredSize.Width, columnWidth));
 218:                  y += child.DesiredSize.Height;
 219:                  xOffset = 0;
 220:              }
 221:          }
 222:          #endregion
 223:  
 224:      }
 225:  
 226:  
 227:  }

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

I think the code is fairly self explanatory, it just keeps adding children to the current column if there is enough space. If there isn’t enough space within the current column or the current children has opted to be in a new column, by using the ColumnBreakBefore DP, the remaining children will start within a new column. This is repeated for all children.

As I just stated the child can opt to be in a new column, using the ColumnBreakBefore DP, this is shown below. Without the ColumnBreakBefore DP declaration the Button would fit within the current column.

   1:  <Window x:Class="CustomPanel.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      xmlns:local="clr-namespace:CustomPanel;assembly="
   5:      Title="Window1" Height="300" Width="300">
   6:  
   7:  
   8:      <local:ColumnedPanel Width="auto" Height="200"
   9:                           VerticalAlignment="Center" Background="WhiteSmoke">
  10:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  11:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  12:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  13:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  14:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  15:          <!-- Without the DP ColumnedPanel.ColumnBreakBefore set here, 
  16:               this button would fit in the current column-->
  17:          <Button local:ColumnedPanel.ColumnBreakBefore="True"
  18:                  FontWeight="Bold" Width="80" Height="80">New Column</Button>
  19:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  20:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  21:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  22:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  23:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  24:          <Rectangle Fill="Black" Width="50" Height="50" Margin="10"/>
  25:      </local:ColumnedPanel>
  26:  </Window>

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }And finally here is a screen shot.

image

And here is the demo project ColumnedPanel.zip

Introduction

Creating A Scrollable Control Surface In WPF

Have you ever had a requirement that called for the user to be able to scroll around a large object, such as a diagram. Well I have, and I have just started working on a hobby project where I need just such a feature. We probably all know that WPF has a ScrollViewer control which allows the users to scroll using the scrollbars, which is fine, but it just looks ugly. What I want is for the user to not really ever realise that there is a scroll area, I want them to just use the mouse to pan around the large area.

To this end I set about looking around, and I have pieced together a little demo project to illustrate this. Its not very elaborate, but it does the job well.

In the end you still use the native WPF ScrollViewer but you hide its ScrollBars, and just respond to mouse events. I have now responded to people requests to add some friction (well my old team leader did it, as its his area) so we have 2 versions, the XAML is the same for both

 

Lets see some code shall we.

   1:  <Window x:Class="ScrollableArea.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      Title="Window1" Height="300" Width="300">
   5:      <Window.Resources>
   6:   
   7:          <!-- scroll viewer Style -->
   8:          <Style x:Key="ScrollViewerStyle" 
   9:                      TargetType="{x:Type ScrollViewer}">
  10:              <Setter Property="HorizontalScrollBarVisibility" 
  11:                      Value="Hidden" />
  12:              <Setter Property="VerticalScrollBarVisibility" 
  13:                      Value="Hidden" />
  14:          </Style>
  15:   
  16:      </Window.Resources>
  17:   
  18:      <ScrollViewer x:Name="ScrollViewer" 
  19:                    Style="{StaticResource ScrollViewerStyle}">
  20:          <ItemsControl x:Name="itemsControl" 
  21:                    VerticalAlignment="Center"/>
  22:      </ScrollViewer>
  23:   
  24:  </Window>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

It can be seen that there is a single ScrollViewer which contains an ItemsControl, but the ItemsControl could be replaced with a Diagram control or something else, you choose. The only important part here is that the ScrollViewer has its HorizontalScrollBarVisibility/VerticalScrollBarVisibility set to be Hidden, so that they are not visible to the user.

 

FRICTIONLESS VERSION

Next we need to respond to the Mouse events. This is done as follows:

   1:  protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
   2:  {
   3:      if (ScrollViewer.IsMouseOver)
   4:      {
   5:          // Save starting point, used later when determining 
   6:          //how much to scroll.
   7:          scrollStartPoint = e.GetPosition(this);
   8:          scrollStartOffset.X = ScrollViewer.HorizontalOffset;
   9:          scrollStartOffset.Y = ScrollViewer.VerticalOffset;
  10:   
  11:          // Update the cursor if can scroll or not.
  12:          this.Cursor = (ScrollViewer.ExtentWidth > 
  13:              ScrollViewer.ViewportWidth) ||
  14:              (ScrollViewer.ExtentHeight > 
  15:              ScrollViewer.ViewportHeight) ?
  16:              Cursors.ScrollAll : Cursors.Arrow;
  17:   
  18:          this.CaptureMouse();
  19:      }
  20:   
  21:      base.OnPreviewMouseDown(e);
  22:  }
  23:   
  24:   
  25:  protected override void OnPreviewMouseMove(MouseEventArgs e)
  26:  {
  27:      if (this.IsMouseCaptured)
  28:      {
  29:          // Get the new scroll position.
  30:          Point point = e.GetPosition(this);
  31:   
  32:          // Determine the new amount to scroll.
  33:          Point delta = new Point(
  34:              (point.X > this.scrollStartPoint.X) ?
  35:                  -(point.X - this.scrollStartPoint.X) :
  36:                  (this.scrollStartPoint.X - point.X),
  37:   
  38:              (point.Y > this.scrollStartPoint.Y) ?
  39:                  -(point.Y - this.scrollStartPoint.Y) :
  40:                  (this.scrollStartPoint.Y - point.Y));
  41:   
  42:          // Scroll to the new position.
  43:          ScrollViewer.ScrollToHorizontalOffset(
  44:              this.scrollStartOffset.X + delta.X);
  45:          ScrollViewer.ScrollToVerticalOffset(
  46:              this.scrollStartOffset.Y + delta.Y);
  47:      }
  48:   
  49:      base.OnPreviewMouseMove(e);
  50:  }
  51:   
  52:   
  53:   
  54:  protected override void OnPreviewMouseUp(
  55:      MouseButtonEventArgs e)
  56:  {
  57:      if (this.IsMouseCaptured)
  58:      {
  59:          this.Cursor = Cursors.Arrow;
  60:          this.ReleaseMouseCapture();
  61:      }
  62:   
  63:      base.OnPreviewMouseUp(e);
  64:  }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

 

FRICTION VERSION

 

Use the Friction property to set a value between 0 and 1, 0 being no friction 1 is full friction meaning the panel won’t "auto-scroll".

 

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Windows;
   6:  using System.Windows.Controls;
   7:  using System.Windows.Data;
   8:  using System.Windows.Documents;
   9:  using System.Windows.Input;
  10:  using System.Windows.Media;
  11:  using System.Windows.Media.Imaging;
  12:  using System.Windows.Navigation;
  13:  using System.Windows.Shapes;
  14:  using System.Windows.Threading;
  15:  using System.Diagnostics;
  16:   
  17:  namespace ScrollableArea
  18:  {
  19:      /// <summary>
  20:      /// Demonstrates how to make a scrollable (via the mouse) area that
  21:      /// would be useful for storing a large object, such as diagram or
  22:      /// something like that
  23:      /// </summary>
  24:      public partial class Window1 : Window
  25:      {
  26:          #region Data
  27:          // Used when manually scrolling.
  28:          private Point scrollTarget;
  29:          private Point scrollStartPoint;
  30:          private Point scrollStartOffset;
  31:          private Point previousPoint;
  32:          private Vector velocity;
  33:          private double friction; 
  34:          private DispatcherTimer animationTimer = new DispatcherTimer();
  35:          #endregion
  36:   
  37:          #region Ctor
  38:   
  39:          public Window1()
  40:          {
  41:              InitializeComponent();
  42:              this.LoadStuff();
  43:   
  44:              friction = 0.95;
  45:      
  46:              animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
  47:              animationTimer.Tick += new EventHandler(HandleWorldTimerTick);
  48:              animationTimer.Start();
  49:          }
  50:          #endregion
  51:   
  52:          #region Load DUMMY Items
  53:          void LoadStuff()
  54:          {
  55:              //this could be any large object, imagine a diagram...
  56:              //though for this example im just using loads
  57:              //of Rectangles
  58:              itemsControl.Items.Add(CreateStackPanel(Brushes.Salmon));
  59:              itemsControl.Items.Add(CreateStackPanel(Brushes.Goldenrod));
  60:              itemsControl.Items.Add(CreateStackPanel(Brushes.Green));
  61:              itemsControl.Items.Add(CreateStackPanel(Brushes.Yellow));
  62:              itemsControl.Items.Add(CreateStackPanel(Brushes.Purple));
  63:              itemsControl.Items.Add(CreateStackPanel(Brushes.SeaShell));
  64:              itemsControl.Items.Add(CreateStackPanel(Brushes.SlateBlue));
  65:              itemsControl.Items.Add(CreateStackPanel(Brushes.Tomato));
  66:              itemsControl.Items.Add(CreateStackPanel(Brushes.Violet));
  67:              itemsControl.Items.Add(CreateStackPanel(Brushes.Plum));
  68:              itemsControl.Items.Add(CreateStackPanel(Brushes.PapayaWhip));
  69:              itemsControl.Items.Add(CreateStackPanel(Brushes.Pink));
  70:              itemsControl.Items.Add(CreateStackPanel(Brushes.Snow));
  71:              itemsControl.Items.Add(CreateStackPanel(Brushes.YellowGreen));
  72:              itemsControl.Items.Add(CreateStackPanel(Brushes.Tan));
  73:   
  74:          }
  75:   
  76:          private StackPanel CreateStackPanel(SolidColorBrush color)
  77:          {
  78:   
  79:              StackPanel sp = new StackPanel();
  80:              sp.Orientation = Orientation.Horizontal;
  81:   
  82:              for (int i = 0; i < 50; i++)
  83:              {
  84:                  Rectangle rect = new Rectangle();
  85:                  rect.Width = 100;
  86:                  rect.Height = 100;
  87:                  rect.Margin = new Thickness(5);
  88:                  rect.Fill = i % 2 == 0 ? Brushes.Black : color;
  89:                  sp.Children.Add(rect);
  90:              }
  91:              return sp;
  92:          }
  93:          #endregion
  94:   
  95:          #region Friction Stuff
  96:          private void HandleWorldTimerTick(object sender, EventArgs e)
  97:          {
  98:              if (IsMouseCaptured)
  99:              {
 100:                  Point currentPoint = Mouse.GetPosition(this);
 101:                  velocity = previousPoint - currentPoint;
 102:                  previousPoint = currentPoint;
 103:              }
 104:              else
 105:              {
 106:                  if (velocity.Length > 1)
 107:                  {
 108:                      ScrollViewer.ScrollToHorizontalOffset(scrollTarget.X);
 109:                      ScrollViewer.ScrollToVerticalOffset(scrollTarget.Y);
 110:                      scrollTarget.X += velocity.X;
 111:                      scrollTarget.Y += velocity.Y;
 112:                      velocity *= friction;
 113:                  }
 114:              }
 115:          }
 116:   
 117:          public double Friction
 118:          {
 119:              get { return 1.0 - friction; }
 120:              set { friction = Math.Min(Math.Max(1.0 - value, 0), 1.0); }
 121:          }
 122:          #endregion
 123:   
 124:          #region Mouse Events
 125:          protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
 126:          {
 127:              if (ScrollViewer.IsMouseOver)
 128:              {
 129:                  // Save starting point, used later when determining how much to scroll.
 130:                  scrollStartPoint = e.GetPosition(this);
 131:                  scrollStartOffset.X = ScrollViewer.HorizontalOffset;
 132:                  scrollStartOffset.Y = ScrollViewer.VerticalOffset;
 133:   
 134:                  // Update the cursor if can scroll or not.
 135:                  this.Cursor = (ScrollViewer.ExtentWidth > ScrollViewer.ViewportWidth) ||
 136:                      (ScrollViewer.ExtentHeight > ScrollViewer.ViewportHeight) ?
 137:                      Cursors.ScrollAll : Cursors.Arrow;
 138:   
 139:                  this.CaptureMouse();
 140:              }
 141:   
 142:              base.OnPreviewMouseDown(e);
 143:          }
 144:   
 145:          
 146:          protected override void OnPreviewMouseMove(MouseEventArgs e)
 147:          {
 148:              if (this.IsMouseCaptured)
 149:              {
 150:                  Point currentPoint = e.GetPosition(this);
 151:   
 152:                  // Determine the new amount to scroll.
 153:                  Point delta = new Point(scrollStartPoint.X - 
 154:                      currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
 155:   
 156:                  scrollTarget.X = scrollStartOffset.X + delta.X;
 157:                  scrollTarget.Y = scrollStartOffset.Y + delta.Y;
 158:   
 159:                  // Scroll to the new position.
 160:                  ScrollViewer.ScrollToHorizontalOffset(scrollTarget.X);
 161:                  ScrollViewer.ScrollToVerticalOffset(scrollTarget.Y);
 162:              }
 163:   
 164:              base.OnPreviewMouseMove(e);
 165:          }
 166:   
 167:          protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
 168:          {
 169:              if (this.IsMouseCaptured)
 170:              {
 171:                  this.Cursor = Cursors.Arrow;
 172:                  this.ReleaseMouseCapture();
 173:              }
 174:   
 175:              base.OnPreviewMouseUp(e);
 176:          }
 177:          #endregion
 178:   
 179:   
 180:   
 181:      }
 182:  }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

And that’s it, we now have a nice scrollable design surface.  Here is a screen shot of the demo app, where the user can happily scroll around using the mouse (mouse button must be down)

image

And here is a link to the demo app (Frictionless) scrollablearea.zip

And here is a link to the demo app (Friction) scrollablearea_friction.zip

Introduction

Find databinding mistakes using PresentationTracesources.TraceLevel

When working with XAML and databinding there is not really much help available to aiding the developer/designer when a databinding is not working. Luckily in .NET 3.5, this changed and we can now include an extra diagnostics trace in a binding, which will show some helpful trace messages about a binding.

One my WPF Disciple buddies Bea Costa published a blog entry about how to do this. You can read more over at her excellent blog entry http://www.beacosta.com/blog/?p=52. I had originally included a link to Corrados (another WPF Disciple, but Corrados link broke, so I had to change to Bea’s post instead).

Introduction

Disciples Of Annihilation (er I mean WPF Disciples meeting)

Yesterday evening I met up with a fellow blogger Marlon Grech. Marlon was over in the UK to have a Silverlight 2.0 training course with Ian G. Marlon was nice enough to come to where I live, Brighton. So we went out had a few beers and a meal and had a chat where we set the world to rights, my girlfriend (wife to be) even turned up for the occasion, jolly decent of her (though I think she was out for the food actually…she doesn’t like code, she likes chocolate…Mmmm perhaps I have the wrong woman after all).

For me its always a buzz meeting people that I admire, and I definitely admire Marlons work, and he is young man, hes only 23, and when you look at the great body of work he has produced its excellent. For those that don’t know Marlons work, I suggest you read his excellent blog it is full of awesome stuff.

So Marlon if you read this, thanks for coming down man, it was cool to meet you. Hope to do it again some other time.

 

Oh and thanks for the free booze, hick it was very nice.