Menus, part IV : collapsible menu

by gleblanc 10. November 2008 17:23

In this post, we will implement a collapsible menu, animated of course. It's amazing to see how easily any .NET educated programmer (even WinForms only) can add functionnalities to Silverlight applications.


Project / source code

Our collapsible menu is declared in a XML file (Menu.xml, inserted as a resource) :

 <?xml version="1.0" encoding="utf-8" ?>
 <Items>
  <Item Text="China" Id="10" />
  <Item Text="USA" Id="30" >
   <Item Text="Grand Canyon" Id="31" />
   <Item Text="Yosemite"     Id="32" />
   .....
  </Item>
  <Item Text="Egypt" Id="40" />
  .....
 </Items>

To build the collapsible menu in our Silverlight application, we first add a Silverlight user control, named CollapsibleMenu (the CollapsibleMenu.xaml and CollapsibleMenu.xaml.cs files are then created by Visual Studio). We force the namespace named glMenu (gl for our own initials). In the CollapsibleMenu.xaml file, we suppress Width and Height properties. Business as usual for a user control.

Our collapsible menu is implemented as an horizontal StackPanel, with two elements : one (a vertical StackPanel with menu items) for the menu itself (horizontally collapsible) and a bordered TextBlock (22 pixels wide) for the tab (always visible, with "Menu" written vertically).

 <StackPanel Orientation="Horizontal" >
  <StackPanel x:Name="spMenu" Orientation="Vertical" ..... >
   .....
  </StackPanel>
  <Border CornerRadius="0,15,15,0" Width="22" VerticalAlignment="Top" ..... >
   <TextBlock x:Name="tab" Text="Menu" TextWrapping="Wrap" ...... />
  </Border>
 </StackPanel>

For the TextBlock (named tab), we also specify some font properties (FontFamily, FontSize, etc.) and handle three events : MouseEnter and MouseLeave but also MouseMove (leaving the tab thru the left or right sides must have a different effect : keeping the menu as it is or collapsing it).

The menu itself is a vertical StackPanel (with an initial width of 0 pixel) that will be populated by program in the function that handles the Loaded event :

 <StackPanel x:Name="spMenu" Orientation="Vertical" Width="0" Loaded="spMenu_Loaded"
             MouseLeave="spMenu_MouseLeave" >
   .....
 </StackPanel>

We handle the Loaded event (to initialize the menu from the XML file) and the MouseLeave event (to progressively reduce its width to 0). Inside this StackPanel, we define an animation (to horizontally and progressively expand or collapse the menu in one second) :

 <StackPanel x:Name="spMenu" ..... >
  <StackPanel.Resources>
   <Storyboard x:Name="stb" >
    <DoubleAnimation x:Name="da" Storyboard.TargetName="spMenu"
                     Storyboard.TargetProperty="Width" Duration="0:0:1" />
   </Storyboard>
  </StackPanel.Resources>
 </StackPanel>
Q

In the Loaded function, we read the XML file and prepare the menu (first as a list of MenuItem objects) and then (in the PrepareMenu function) as a set of XAML tags :

 List<MenuItem> lstMenuItems;
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  lstMenuItems = new List<MenuItem>();
  XElement xmlRoot = XElement.Load("Menu.xml");
  getMenuItems(xmlRoot);
  PrepareMenu();
 }

The MenuItem class (also created in the glMenu namespace) is

 public class MenuItem
 {
  public MenuItem(string aText, int aId, int aLevel, int acountSubItems)
  {
   Text = aText; Id = aId; Level = aLevel; countSubItems = acountSubItems;
  }
  public string Text {get; set;}
  public int Id {get; set;}
  public int Level { get; set; }
  public int countSubItems {get; set;}
 }

Level is 0 for a main menu item (for instance USA) and 1 for a submenu item (for instance Yosemite). The field countSubItems contains the number of sub-items for a main menu item (5 for Italy).

Linq provides an incomparable help in the task of analyzing the XML file (do not forget to add a reference to System.Xml.Linq) :

 void getMenuItems(XElement xml)
 {
  var lstItems = from p in xml.Elements("Item") select p;
  foreach (var el in lstItems)
  {
   string Text = el.Attribute("Text").Value;
   int Id; Int32.TryParse(el.Attribute("Id").Value, out Id);
   if (el.HasElements == false)
    lstMenuItems.Add(new MenuItem(Text, Id, 0, 0));
   else
   {
    // this main menu item has one or several sub-menu items
    lstMenuItems.Add(new MenuItem(Text, Id, 0, el.Elements("Item").Count()));
    var subItems = from pSub in el.Elements("Item") select pSub;
    foreach (var sub in subItems)
    {
     Text = sub.Attribute("Text").Value;
     Int32.TryParse(sub.Attribute("Id").Value, out Id);
     lstMenuItems.Add(new MenuItem(Text, Id, 1, 0));
    }
   }
  }
 }

The menu is a vertical StackPanel. Each main menu item is a Grid. Each sub-menu item is also a Grid but included in a vertical StackPanel containing all sub-menu items :

 void PrepareMenu()
 {
  int N = lstMenuItems.Count;
  for (int n=0; n<N; n++)
  {
   MenuItem mi = lstMenuItems[n];
   PrepareMainMenuItem(mi);
   if (mi.countSubItems > 0)
   {
    PrepareSubMenuItems(mi);
    n += mi.countSubItems;
   }
  }
  spMenu.Width = 0;  // menu initially fully collapsed
  ..... // add storyboard to main items with submenu
 }

At the end of the PrepareMenu function, we also need to add a Storyboard (to vertically and progressively expand or callapse a sub-menu). This is only necessary for main menu items that have sub-menu items.

In the PrepareMainMenuItem function, we prepare the XAML for a specific main menu item : a Grid (with a linear gradient brush for Background) plus a TextBlock for the text. We handle the MouseLeftButtonDown (to open the associated sub-menu but it could be to activate a main menu item without sub-menu).

In the PrepareSubMenuItems function, we prepare the XAML for the sub-menu of a specific main menu item : a vertical StackPanel containing as many Grid as they are sub-menu items in this main menu item). This Grid is made of a Rectangle (for Background and Stroke) and a TextBlock.

As we did in previous posts, our collapsible menu is able to generate the Click event (the MenuEventArgs is just a class derived from EventArgs, plus the Id field as complementary information) :

 public delegate void MenuEventHandler(object sender, MenuEventArgs e);
 public event MenuEventHandler Click;
 ....
 MenuEventArgs args = new MenuEventArgs(); args.itemId = Id;
 if (Click != null)     // if Click event handled by the user
    Click(this, args);

In Page.xaml, the user specifies the collapsible menu with (do not forget to put it on top) :

<UserControl .....
    xmlns:gl="clr-namespace:glMenu"
    ..... >
    <Grid x:Name="LayoutRoot" Background="White">
      <Grid x:Name="menuGrid" Canvas.ZIndex="99"  >
       <gl:CollapsibleMenu x:Name="menu" VerticalAlignment="Top" Margin="0, 20, 0, 0"
                           Click="myMenu_Click" />
      </Grid>
      <Grid ..... >        here the main grid for the Silverlight application
       .....
      </Grid>
    </Grid>

That's all, folks !

Tags:

Menus, part III : animated multi-level drop-down menu

by gleblanc 2. November 2008 20:22
In this post, we will add (dynamically and by program) an animation to our previous drop-down and XML-driven menu. Just of few lines to add !


Project / source code

The artwork initially presented is "La montagne Sainte-Victoire" from Paul Cézanne.

As seen in the previous post, our sub-menus and menu items are dynamically created when needed. When a sub-menu is created, we just need to dynamically add a Storyboard object. The code here is added in our CreateSubMenu fonction, that accepts mnu, of type Menu, as argument. In our Menu class, we added a field, named stbShow and of type Storyboard :

 if (mnu.parentMenu != null)  // not applicable to the main (horizontal) menu
 {
  // dynamically create storyboard
  Storyboard stb = new Storyboard();
  Storyboard.SetTarget(stb, mnu.menuContainer);

  DoubleAnimation daHeight = new DoubleAnimation();
  daHeight.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400));
  daHeight.From = 0; daHeight.To = mnu.lstMenuItems.Count * cellItemHeight;
  Storyboard.SetTargetProperty(daHeight, new PropertyPath("Height"));

  DoubleAnimation daWidth = new DoubleAnimation();
  daWidth.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400));
  daWidth.From = 0; daWidth.To = mnu.Width;
  Storyboard.SetTargetProperty(daWidth, new PropertyPath("Width"));

  stb.Children.Add(daHeight); stb.Children.Add(daWidth); 
  mnu.menuContainer.Resources.Add("stbShowHide", stb);
  mnu.stbShow = stb;
 }
 .....
 mnu.stbShow.Begin();

Just a few explanations : our menu is a Menu object (Menu being defined in our program). It's implemented as a Border container containing a vertical StackPanel. A reference to this Border container is kept in the menuContainer field of our Menu class. We define two DoubleAnimation to increase Height and Width properties from 0 to their final values in 400 millisec. Final Height depends on number of menu items (value given by the lstMenuItems list of menu items) and final Width depends on largest width for menu items (value kept in the Width field). The two DoubleAnimation are added as children of the Storyboard object just created. The Storyboard is then added as resource of the menu container (ie the Border container).

That's it ! 

Tags:

Menus, part II : multi-level drop-down menu

by gleblanc 26. October 2008 14:43

In this post, we will implement a multi-level drop down menu, ie eventually with any number of sub-menus. The drop-down menu presented here is XML driven, though without animation, as possible with Silverlight. We'll add animation effects in the forthcoming post.


Project / source code

The art-work initially displayed is "Tomorrow" from Jean-Yves Tanguy, a French painter who became an American citizen.

Our menu is XML-driven, as a set of Item tags. An Item tag can itself contain any number of Item tags (corresponding to any number of sub-menus and any number of sub-menu items) : 

<?xml version="1.0" encoding="utf-8" ?>
<Menu>
  <Item Id="10" Text="Antarctica" />
  <Item Id="20" Text="Europe" >
    <Item Id="201" Text="Italy" >
      <Item Id="2011" Text="Florence" />
      .....
    </Item>
    <Item Id="202" Text="France" >
      <Item Id="2021" Text="Paris" />
      <Item Id="2022" Text="French riviera" >
        <Item Id="20221" Text="Nice" />
        <Item Id="20222" Text="Monaco" >
          <Item Id="202221" Text="Monaco" />
          <Item Id="202222" Text="Monte-Carlo" />
        </Item>
        <Item Id="20223" Text="Saint-Jean Cap Ferrat" />
      </Item>
      <Item Id="2023" Text="Provence" />
    </Item>
    .....
  <Item Id="30" Text="America" >
  .....
  </Item>
  .....
  <Item Id="50" Text="Oceania" />
</Menu>

For each menu item, you need to provide an Id value as well as a Text. A Click event is raised whenever the user clicks on a menu item (the Id can then be retrieved from the second parameter of the event handling function).

As usual, we create a Silverlight application that will host the drop-down menu we are building here. Let's assume the application is named UseDropDownMenu. Visual Studio creates a namespace for the program and, by default, this namespace is named UseDropDownMenu, as the application. We first create a DropDownMenu XAML file and we specify glMenu as namespace (thus, as seen in the previous post, one change in the xaml file and another one in the xaml.cs file, these two files being created by Visual Studio).

In the hosting Silverlight application, we need to reserve space for the main menu (top horizontal bar, whose height is 30 pixels). The menu part must remain on top (this is why the Canvas.ZIndex attribute, applicable to a Grid container, is given a large value) :
 <UserControl x:Class="UseDropDownMenu.Page"
    .....
    xmlns:mnu="clr-namespace:glMenu"
    ..... >
    <Grid x:Name="LayoutRoot" Background="White">
     <Grid x:Name="menuGrid" Canvas.ZIndex="99" >
      <mnu:DropDownMenu x:Name="myMenu" Click="myMenu_Click" />
     </Grid>
     <Grid x:Name="g">
      .....        user grid here
     </Grid>
    </Grid>
 </UserControl> 

In the DropDownMenu.xaml file, we give the XAML description of the main menu bar (implemented as an horizontal StackPanel). Sub-menus (that are vertical StackPanel) will be dynamically added, as needed (and later on removed) :
 <UserControl x:Class="glMenu.DropDownMenu"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >
  <Grid x:Name="menuGrid" HorizontalAlignment="Stretch" VerticalAlignment="Top" >
   <StackPanel x:Name="spMainMenu" Loaded="spMainMenu_Loaded"  Orientation="Horizontal"
               Height="30" >
    <StackPanel.Background>
     <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
      <GradientStop Offset="0" Color="#FEF4C1" />
      <GradientStop Offset="1" Color="#F4E186" />
     </LinearGradientBrush>
    </StackPanel.Background>
   </StackPanel>
  </Grid>
 </UserControl>


Thanks to Linq, it's easy to retrieve the Item children (just the children, not the grand-children) of a specific Item (an XElement within the XML file) :
 List<MenuItem> getMenuItems(XElement xml)
 {
  var lstItems = from p in xml.Elements("Item")
                 select new MenuItem
                 {
                  Text = (string)p.Attribute("Text").Value,
                  Id = (int)p.Attribute("Id"),
                  hasSubMenu = p.HasElements,
                  XElem = p
                 };
  return lstItems.ToList();
 }

MenuItem is a class created to hold attributes of a menu item. Menu is another class created to hold attributes of a menu (main menu or sub-menu). Menus displayed at one time are kept in a list of menus :
 List<Menu> lstMenus = new List<Menu>();

The main menu is created at Loaded time while sub-menus are created (and removed) as needed. A menu is first created in memory (each menu item is implemented as a Grid containing a rectangle - to mark MouseOver - and a TextBlock for the Text) :
 void PrepareMenuPresentation(Menu mnu) 
 {
   .....
  int N = mnu.lstMenuItems.Count;
  for (int n = 0; n < N; n++)
  {
   TextBlock txtItem = new TextBlock(); 
   .....    
   Rectangle rcItem = new Rectangle(); 
   ......
   Grid gridItem = new Grid();
   .....  
   gridItem.MouseLeave += new MouseEventHandler(item_MouseLeave);
   gridItem.MouseEnter += new MouseEventHandler(item_MouseEnter);
   gridItem.MouseLeftButtonDown += new MouseButtonEventHandler(item_MouseLeftButtonDown);
   gridItem.MouseMove += new MouseEventHandler(item_MouseMove);
   gridItem.Children.Add(rcItem); gridItem.Children.Add(txtItem);
   mnu.StackPan.Children.Add(gridItem);
  }
   .....   
 } 


The mouse events are handled to control navigation from menu items to menu items (and, remember, sub-menus are dynamically added or removed).

Our code (300 lines) is not that long and complex but it would be too annoying - and useless - to explained it here line by line. Moreover, I tried to document the code as well as I could.

Tags:

Menus. Part I

by gleblanc 12. October 2008 12:54

With this post, we are starting a serie of posts dedicated to menus. In this post, we present the basis with a basic menu, though XML driven and, eventually, linked to a status bar. We'll improve it in the forthcoming posts.


Project / source code

As usual, we create a Silverlight application that will host the menu we are building here. Let's assume the application is named Menu1. Visual Studio creates a namespace for the program and, by default, this namespace is named Menu1, as the application.

Our menu is described in a XML file (Menu.xml) and this XML file is inserted as resource into the Silverlight application (Solution Explorer, Add, New element, XML file) :
 <?xml version="1.0" encoding="utf-8" ?>
 <Menu>
  <Item Id="10" Text="Monet" Help="Claude Monet : Thames at Westminster" />
  <Item Id="20" Text="Turner" Help="Joseph Turner : Oxford High Street" />
  <Item Id="30" Text="Renoir" Help="Auguste Renoir : Pont des arts, Paris" />
  <Item Id="40" Text="Pissaro" Help="Camille Pissaro : L'Hermitage" Selected="True" />
  <Item Id="50" Text="Vermeer" Help="Johannes Vermeer : View of Delft" />
 </Menu>

Each menu item must have an Id and a Text attributes and, eventually, a Help sentence (a tooltip eventually displayed in the status bar) and a Selected attribute (the menu item with a Selected attribute is displayed in red and, at program startup, a request is made to the hosting application to respond to that item, as if the user had clicked on that menu item). This makes that menu item the currently selected menu item.

Let's now create the menu, by adding a Silverlight User Control (Solution Explorer, Add, Add new element, Silverlight User Control), that we name Menu.xaml. By default, it's created in the Menu1 namespace (as the hosting program). Since we intend to create a menu that will be easily used in another hosting application, we prefer a specific namespace (here Menu) for the menu (we'll do the same later on for the status bar). This requires two changes : one in the Menu.xaml file (value of x:Class attribute changed to Menu.Menu instead of Menu1.Menu) and one in the Menu.xaml.cs file (namespace Menu instead of namespace Menu1).

In the Menu.xaml file, we delete the Width and Height attributes and force an horizontal StackPanel as container (instead of a Grid). We alse force a Background color with a gradient (it's generally considered nicer) :
 <UserControl x:Class="Menu.Menu"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >
  <StackPanel x:Name="spMenu" HorizontalAlignment="Stretch" Height="40"
              Loaded="spMenu_Loaded" Orientation="Horizontal" >
   <StackPanel.Background>
    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
     <GradientStop Offset="0" Color="#FEF4C1" />
     <GradientStop Offset="1" Color="#F4E186" />
    </LinearGradientBrush>
   </StackPanel.Background>
  </StackPanel>
 </UserControl>

We create a class (named MenuItem) for a menu item (Solution Explorer, Add, New element, Class), again in the Menu namespace (one line to change in the MenuItem.cs file) :
 namespace Menu
 {
  public class MenuItem
  {
   public int Id { get; set; }
   public string Text { get; set; }
   public string Help { get; set; }
   public bool Selected { get; set; }
  }
 }

In the spMenu_Loaded function, we'll open the Menu.xml file and analyze it using the Linq technology. To do so, need to add a reference (Solution Explorer, Add reference, .NET) to System.Xml.Linq. We also need to add using System.Xml.Linq; to the Menu.xaml.cs file. To analyze our XML file and create a list of MenuItem, we write in Menu.xaml.cs (remember, Help and Selected attributes could be absent : easy for Help, thanks to the null value for a string but a check is necessary for Selected - whatever the value, we consider here, for the sake of simplicity, that the item is selected if the Selected attribute is present) :
 List<MenuItem> tabItems;
 .....
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  XElement xml = XElement.Load("Menu.xml");
  var listItems = from p in xml.Elements("Item")
                  select new MenuItem
                  {
                   Text = (string)p.Attribute("Text").Value,
                   Id = (int)p.Attribute("Id"),
                   Help = (string)p.Attribute("Help"),
                   Selected=p.Attribute("Selected")!=null ? true : false
                  };
  tabItems = listItems.ToList();
  .....
 }

In the menu bar, each menu item is implemented as a single-cell Grid containing a rectangle (for the color change on MouseOver), a TextBlock (for menu item text) and a vertical Line as separator. We also handle mouse events for each menu item :
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  .....
  int N = tabItems.Count;
  for (int n=0; n<N; n++)
  {
   TextBlock txtItem = new TextBlock();
   txtItem.Text = tabItems[n].Text;
   txtItem.VerticalAlignment = VerticalAlignment.Center;
   txtItem.HorizontalAlignment = HorizontalAlignment.Center;
   switch (tabItems[n].Selected)
   {
    case true : txtItem.Foreground = new SolidColorBrush(Colors.Red); break;
    case false : txtItem.Foreground = new SolidColorBrush(Color.FromArgb(255, 37, 36, 85));
                 break;
   }
   Rectangle rcItem = new Rectangle(); rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   rcItem.Width = txtItem.ActualWidth +6;
   Grid gridItem = new Grid();
   gridItem.Width = txtItem.ActualWidth+55;
   gridItem.Tag = n;

   Line liSep = new Line();
   liSep.X1 = gridItem.ActualWidth-5; liSep.Y1 = 5;
   liSep.X2 = gridItem.ActualWidth-5; liSep.Y2 = 35; liSep.StrokeThickness = 5;
   liSep.Stroke = new SolidColorBrush(Color.FromArgb(255, 255, 245, 190));

   gridItem.MouseLeave += new MouseEventHandler(c_MouseLeave);
   gridItem.MouseEnter += new MouseEventHandler(c_MouseEnter);
   gridItem.MouseLeftButtonDown += new MouseButtonEventHandler(c_MouseLeftButtonDown);

   gridItem.Children.Add(rcItem);
   gridItem.Children.Add(txtItem); gridItem.Children.Add(liSep);
   spMenu.Children.Add(gridItem);
   .....
  }
At the end of the spMenu_Loaded function, we need to warn the hosting application that the Selected menuItem (here the item with Id 40) has to be activated. To do so, we first need a class (here named MenuEventArgs) derived from EventArgs : Solution Explorer, Add, New element, Class, again in the Menu namespace. Visual Studio creates the MenuEventArgs.cs file, that contains (forgetting a few using at the beginning) :
 namespace Menu
 {
  public class MenuEventArgs : EventArgs
  {
   public int ItemId { get; set; }
  }
 }
In the Menu.xaml.cs file, we need to add a event (here named Click) to warn the hosting application that a menu item has been selected (nSelected containing the ordinal position of the menu item selected in the XML file) :
 public delegate void MenuEventHandler(object sender, MenuEventArgs e);
 public event MenuEventHandler Click;
 .....
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  .....
  if (nItemSelected!=-1)
  {
   MenuEventArgs args = new MenuEventArgs(); args.ItemId = tabItems[nItemSelected].Id;
   if (Click != null) Click(this, args);
  }
 }
In the mouse event functions, we mark the MouseOver selection or inform the hosting application that a menu item was clicked :
 void c_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
 {
  Grid gridItem = sender as Grid;
  int nItem = (int)gridItem.Tag;
  MenuEventArgs args = new MenuEventArgs(); args.ItemId = tabItems[nItem].Id;
  TextBlock txtItem = gridItem.Children[1] as TextBlock;
  txtItem.Foreground = new SolidColorBrush(Colors.Red);
  if (nItemSelected!=-1) 
  {
   Grid gSel = spMenu.Children[nItemSelected] as Grid;
   TextBlock txtSelItem = gSel.Children[1] as TextBlock;
   txtSelItem.Foreground = new SolidColorBrush(Color.FromArgb(255, 37, 36, 85));
  }
  nItemSelected=nItem;
  if (Click != null) Click(this, args); 
  } 

  void c_MouseEnter(object sender, MouseEventArgs e)
  {
   Grid gridItem = sender as Grid;
   Rectangle rcItem = gridItem.Children[0] as Rectangle;
   rcItem.Fill = new SolidColorBrush(Color.FromArgb(255, 203, 203, 239));
   int nItem = (int)gridItem.Tag;
   .....
  }

  void c_MouseLeave(object sender, MouseEventArgs e)
  {
   Grid gridItem = sender as Grid;
   Rectangle rcItem = gridItem.Children[0] as Rectangle;
   rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   .....
  }
Now, let's add the menu in the hosting application. In Page.xaml, we inform Visual Studio that we'll use Menu (one line to add) :
 <UserControl x:Class="Menu1.Page"
   .....
   xmlns:mnu="clr-namespace:Menu"
   .....
   >
mnu is now the prefix we decide to give to reference our menu. The user inserts for instance the menu in a grid row (in Page.xaml)  :
 <mnu:Menu x:Name="myMenu" Grid.Row="0" Click="myMenu_Click" />
And the myMenu_Click function (in Page.xaml.cs) is something like :
 private void myMenu_Click(object sender, MenuEventArgs e)
 {
  ..... // menu item clicked in e.Id
 }
And now the status bar : we create a Silverlight User Control (StatusBar.xaml), again in the Menu namespace (do not forget the namespace change in StatusBar.xaml.cs). The StatusBar.xaml file becomes :
 <UserControl x:Class="Menu.StatusBar"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      >
  <Grid x:Name="LayoutRoot" Background="White">
   <Border Background="#F4E186" >
    <TextBlock x:Name="txtToolTip" FontSize="10" Foreground="Blue"
               VerticalAlignment="Center"  />
   </Border>
  </Grid>
 </UserControl>
To make the link between the menu and the status bar, we add a property in the Menu class :
 public StatusBar SB { get; set; }
The user inserts a status bar (in Page.xaml) :
 <mnu:StatusBar x:Name="myStatusBar" Grid.Row="2" />
and force the link between the menu and the status bar (for instance in the Loaded function in Page.xaml.cs) :
 myMenu.SB = myStatusBar;

Nothing Silverlight-ish in this menu but we are now ready for more Silverlight-like menus. See you in next posts for that. 

Tags:

Turn the page !

by gleblanc 29. September 2008 19:24

!!!!!!! This page has been updated for Silverlight 2 RTM : see post of Nov 18th (http://www.gleblanc.eu/Blog/post/Page-turner-updated-for-Silverlight-2-TRM.aspx)  !!!!! 

In this post, we'll turn pages, thanks to an outstanding component developed and made freely available by Mitsu Furuta of Microsoft France.

Turn the pages using the mouse, as if it was a book ! There are five paintings of Brueghel (a painter of 16th century) to be seen.
 


Project / source code 

To add such a "page turner" into your Silverlight program, download the DLL that does the job. First, access the project on Codeplex. The component was developed by Mitsu Furuta of Microsoft France. Incidentally, Mitsu is blogging in French but also in English.

Download the zip file, unzip it, compile the project in the SLBookDemoApp directory and extract the SLMitsuControls.dll file that is created by the compiler in the ClientBin sub-directory.

The use of this component is under "Microsoft Public License" (MS-PL). I can read a computer book as if it was a novel but reading just a few paragraphs of lawyer's jargon always gives me headache. But here it's rather clear : you can use the component "as it is", even in commercial applications, royalty-free (I mean the component described here is royalty-free, not necessarily your application). Of course, you can't claim rights on this component... For me, it was obvious. Programmers and lawyers seem to have a brain somewhat different. 

In your application, add a reference to the DLL you have just extracted and saved somewhere. In the UserControl tag in the xaml file, add :
 xmlns:local="clr-namespace:SLMitsuControls;assembly=SLMitsuControls"
I chose local for future reference to the component but any name could be chosen.

Now, you need to add a component that has a visual appearence :
 <local:UCBook x:Name="book" ..... />
where ..... can be replaced by any property you usually find in a component (Grid.Row, Canvas.Left, etc.).

Now, let's see the changes that have to be made in your Page.xaml.cs file. Your Page class must implement the IDataProvider interface (and consequently implement GetItem and GetCount functions). Some initialization (one line) has also to be done in the function that handles the Loaded event :
 using SLMitsuControls;
 .....
 public partial class Page : UserControl,IDataProvider
 {
  .....
  private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
  {
   book.SetData(this);
  }
  public object GetItem(int index)
  {
   return pages.Items[index];
  }
  public int GetCount()
  {
   return pages.Items.Count;
  }
  .....
 }

The images (here five) have to be mentionned in Grid.Resources (or Canvas.Resources, depending on the container) :
 <Grid .... >
  <Grid.Resources>
  <ItemsControl x:Name="pages" >
   <Image Source="Brueghel1.jpg" Stretch="UniformToFill"  />
   <Image Source="Brueghel2.jpg" Stretch="UniformToFill"    />
   <Image Source="Brueghel3.jpg" Stretch="UniformToFill" />
   <Image Source="Brueghel4.jpg" Stretch="UniformToFill"   />
   <Image Source="Brueghel5.jpg" Stretch="UniformToFill"  />
  </ItemsControl>
 </Grid.Resources>
 ....
 </Grid>

The images (here 5 images, from Brueghel1.jpg to Brueghel5.jpg) need to be copied in the ClientBin directory (the directory of the xap file). This is often an overlooked step : adding the images in the project (as often done for images) doesn't help. The images need to be explicitly copied into the ClientBin directory. 

To know which pages are visible, just handle the OnPageTurned event :
 <local:UCBook x:Name="book" ..... OnPageTurned="book_OnPageTurned" />
 .....
 private void book_OnPageTurned(int leftPageIndex, int rightPageIndex)
 {
  .....
 }

The leftPageIndex and rightPageIndex arguments get the value -1 when a page is not displayed (for instance there is no page at the left when the book is initially opened).

A page is not restricted to images : it could contain a set of components. Just replace the <Image ..... /> tag by any other tag, for instance a grid tag (container for all the elements of a page).  

Tags:

Silverlight 2 RC0 and the programs presented here

by gleblanc 26. September 2008 22:22

In his blog, Scott Guthrie announces the availability of Silverlight 2 Release Candidate 0.

Please read carefully the terms, still more explicitely stated in Tim Heuer's blog. RC0 is for developers only (well, I guess my readers are), so they can prepare themselves to have their programs ready the day Silverlight 2 comes to the light : there are breaking changes and current Silverlight 2 beta 2 (as my programs currently are) will not run unchanged with Silverlight 2 (RC0 included).

As Tim states with insistance : applications developed using RC0 should not be deployed and there is no RC0 run-time available for download ! In other words, use RC0 just to update and test your own Silverlight 2 programs and only on your development machine.

Though I intend to document changes that will be necessary in my programs (very few, I hope), I am currently sticking to Silverlight 2 Beta 2. I will switch to SL2 RTM and update my programs (included the SL2 programs mentionned in my book) the day Silverlight 2 is officially out.

 Thanks for your comprehension.

Tags:

Rotating images

by gleblanc 26. September 2008 12:40

In this post, we will show how easy it is to implement spectacular rotating effects to images.


Project / source code 
First, we inserted five images (all 250x200) as resource into the project. The carrousel is a canvas made of two Image components and a TextBlock. All these three components will be animated : 
 <Canvas x:Name="Carrousel" Width="250" Height="200" Background="Black"
         Loaded="Carrousel_Loaded" >
  <Image x:Name="img1" Source="London.jpg" Width="250" Height="200" .... ></Image>
  <Image x:Name="img2" Source="Brussels.jpg" Width="250" Height="200" .... ></Image>
  <Canvas .... >
   <TextBlock x:Name="lCityName" ..... ></TextBlock>
  </Canvas> 
 </Canvas>

We apply a ScaleTransform to the two Image components :
 <Image x:Name="img1" Source="London.jpg" Width="250" Height="200" 
        RenderTransformOrigin="0.5,0.5"
        MouseEnter="img_MouseEnter" MouseLeave="img_MouseLeave" >
  <Image.RenderTransform>
   <ScaleTransform x:Name="scale1" CenterX="0" />
  </Image.RenderTransform>
 </Image>
 <Image x:Name="img2" Source="Brussels.jpg" Width="250" Height="200"
        MouseEnter="img_MouseEnter" MouseLeave="img_MouseLeave" >
  <Image.RenderTransform>
   <ScaleTransform x:Name="scale2" CenterX="250"/>
  </Image.RenderTransform>
 </Image>

The MouseEnter and MouseLeave events are intercepted just to pause and resume the animation.
We now define the 3-second animation (simultaneously, a ScaleTransform and an horizontal move on img1 and a ScaleTransform on img2) :
 <Grid x:Name="LayoutRoot" >
  <Grid.Resources>
   <Storyboard x:Name="stb" Completed="stb_Completed" >
    <DoubleAnimation Storyboard.TargetName="scale1" Storyboard.TargetProperty="ScaleX"
                     From="1" To="0" Duration="0:0:3" />
    <DoubleAnimation Storyboard.TargetName="img1" Storyboard.TargetProperty="(Canvas.Left)"
                     From="0" To="-125" Duration="0:0:3" />
    <DoubleAnimation Storyboard.TargetName="scale2" Storyboard.TargetProperty="ScaleX"
                     From="0" To="1" Duration="0:0:3" />
   </Storyboard>
  </Grid.Resources>
  .....
 </Grid>
The animation is fired in the Carrousel_Loaded function. In the stb_Completed function, we reload the img1 and img2 components with images (two images in succession in the ts array) and fire again the animation :
 using System.Windows.Media.Imaging;
 .....
 string[] ts = { "London", "Brussels", "Venice", "Moskow", "Amsterdam" };
 int N, nImg1, nImg2;
 private void stb_Completed(object sender, EventArgs e)
 {
  N++;
  nImg1 = N % ts.Length;
  nImg2 = (nImg1 + 1) % ts.Length;
  img1.Source = new BitmapImage(new Uri(ts[nImg1] + ".jpg", UriKind.Relative));
  img2.Source = new BitmapImage(new Uri(ts[nImg2] + ".jpg", UriKind.Relative));
  stb.Begin();
  .....
 }

It's all for images. Now, the animation on text. The lCityName TextBlock is inserted in a clipped canvas. The stbCityName animation just changes the Canvas.Left property, giving the right to left effect :
 <Canvas Canvas.Left="10" Canvas.Top="220" Width="500" Height="35"
         Clip="M0,0 h250 v35 h-250 z">
  <TextBlock x:Name="lCityName" Canvas.Left="260" Canvas.Top="0"
             FontSize="30" FontWeight="Bold" Foreground="Blue" Width="250" >
   <TextBlock.Resources>
    <Storyboard x:Name="stbCityName">
     <DoubleAnimation Storyboard.TargetName="lCityName"
                      Storyboard.TargetProperty="(Canvas.Left)" From="220" To="50"
                      Duration="0:0:2.5" BeginTime="0:0:0.5" />
    </Storyboard>
   </TextBlock.Resources>
  </TextBlock>
 </Canvas>

City names are also changed in the stb_Completed function, as we did for images :
 private void stb_Completed(object sender, EventArgs e)
 {
  .....
  lCityName.SetValue(Canvas.LeftProperty, 250.0); lCityName.Text = ts[nImg2];
  stbCityName.Begin();
 }

Instead of :
 <DoubleAnimation Storyboard.TargetName="scale1" Storyboard.TargetProperty="ScaleX" ..... />
it's possible to write :
 <DoubleAnimation Storyboard.TargetName="img1"
   Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
   ..... />


And now another rotating effect :


Project / source code
We first inserted five images (all 128x152) of outstanding scientists as resource into the project. The carrousel is made of an Image component and a TextBlock. These two elements will be animated :
 <Canvas Width="128" Height="152" >
  <Image x:Name="img" Source="Marie Curie.jpg" Width="128" Height="152" .... ></Image>
  <TextBlock x:Name="sName" .....></TextBlock>
 </Canvas>

The image is clipped to an ellipse and we apply a ScaleTransform to it :
 <Image x:Name="img" Source="Marie Curie.jpg" Width="128" Height="152"
        RenderTransformOrigin="0.5, 0.5">
  <Image.Clip>
   <EllipseGeometry Center="64,76" RadiusX="64" RadiusY="76" />
  </Image.Clip>
  <Image.RenderTransform>
   <ScaleTransform x:Name="imgScale"  />
  </Image.RenderTransform>
  .....
 </Image>
We define an animation for the Image :
 <Image x:Name="img" Source="Marie Curie.jpg" ..... >
  .....
  <Image.Resources>
   <Storyboard x:Name="stb" Completed="stb_Completed" >
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="imgScale"
                                   Storyboard.TargetProperty="ScaleX"  >
     <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" />
     <LinearDoubleKeyFrame KeyTime="0:0:2" Value="1" />
     <LinearDoubleKeyFrame KeyTime="0:0:4" Value="0" />
    </DoubleAnimationUsingKeyFrames>
   </Storyboard>
  </Image.Resources>
 </Image>
In the stb_Completed function, we load the img component with another image, whose names are found in the ts array :
 using System.Windows.Media.Imaging;
 .....
 string[] ts = { "Marie Curie", "Thomas Edison", "Alexander Fleming",
                 "Guglielmo Marconi", "Albert Einstein" };
 int N;
 private void stb_Completed(object sender, EventArgs e)
 {
  N++;
  int nImg = N % ts.Length;
  img.Source = new BitmapImage(new Uri(ts[nImg] + ".jpg", UriKind.Relative));
  stb.Begin();
 .....
 }
And now for the animation on names (similar, though applied to a TextBlock) :
 <TextBlock x:Name="sName" Width="128" Canvas.Left="0" Canvas.Top="180" TextAlignment="Center"
            RenderTransformOrigin="0.5, 0.5">
  <TextBlock.RenderTransform>
   <ScaleTransform x:Name="nameScale" />
  </TextBlock.RenderTransform>
  <TextBlock.Resources>
   <Storyboard x:Name="stbName">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="nameScale"
                                   Storyboard.TargetProperty="ScaleX">
     <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" />
     <LinearDoubleKeyFrame KeyTime="0:0:2" Value="1" />
     <LinearDoubleKeyFrame KeyTime="0:0:4" Value="0" />
    </DoubleAnimationUsingKeyFrames>
   </Storyboard>
  </TextBlock.Resources>
 </TextBlock>
In the stb_Completed function, we change names, as we did for images :
 private void stb_Completed(object sender, EventArgs e)
 {
  .....
  sName.Text = ts[nImg];
  stbName.Begin();
 }


In the next post, we will turn pages.

Tags:

Visual effects while displaying an image. Part III

by gleblanc 23. September 2008 17:12

In this post, we will animate the clipping area using the Silverlight animation technique (ie, no more the timer).
This time, the painting is "The station in the forest" from Paul Delvaux, a Belgian surrealist painter (1897-1994).


Project / source code
Again, as in part II, we display two (identical) images at the same location : the first one, in the background, with an opacity of 0.2 and the second one, on top, with a clipping area. The clipping area, representing a keyhole, is a closed figure (named pf) made of an arc segment and two line segments (since it's a closed figure, there is no need to specify the closing line segment) :
 <Image Source="Delvaux.jpg" Width="400" Height="270" Opacity="0.2" />
 <Image Source="Delvaux.jpg" Width="400" Height="270" >
  <Image.Clip>
   <PathGeometry >
    <PathFigure x:Name="pf" StartPoint="20,35" IsClosed="True" >
     <ArcSegment IsLargeArc="True" Point="40,35" Size="20,20" SweepDirection="Clockwise" />
     <LineSegment Point="45,75" />
     <LineSegment Point="15,75" />
    </PathFigure>
   </Image.Clip>
 </Image>

We'll move and scale the clipping area, ie the keyhole. To do so, we first define two transforms for the second image : a TranslateTranform (name keyholeXlate) and a ScaleTransform (named keyholeScale) :
 <Image Source="Delvaux.jpg" Width="400" Height="270" >
  <Image.Clip>
   <PathGeometry>
    <PathFigure ..... >
     .....
    </PathFigure>
    <PathGeometry.Transform>
     <TransformGroup>
      <TranslateTransform x:Name="keyholeXlate"/>
      <ScaleTransform x:Name="keyholeScale" />
     </TransformGroup>
    </PathGeometry.Transform>
   </PathGeometry>
  </Image.Clip>
 </Image>

We now define the animation. We have four elements to animate (not counting a last and dummy - though important - animation, whose purpose will be explained later on) :

  • we make the keyhole bigger and bigger : two ScaleTransform with ScaleX and ScaleY jumping from 1 to 5 in 15 seconds,
  • an horizontal move (TranslateTransform) in three steps (thus with four DoubleAnimationUsingKeyFrames) to sweep the painting in zigzag,
  • a vertical move (TranslateTransform).

<Grid x:Name="LayoutRoot" Background="White" Loaded="LayoutRoot_Loaded">
 <Grid.Resources>
  <Storyboard x:Name="stb" RepeatBehavior="Forever" >
   <DoubleAnimation Storyboard.TargetName="keyholeScale" Storyboard.TargetProperty="ScaleX"
                    From="1" To="5" Duration="0:0:15" />
   <DoubleAnimation Storyboard.TargetName="keyholeScale" Storyboard.TargetProperty="ScaleY"
                    From="1" To="5" Duration="0:0:15" />
   <DoubleAnimationUsingKeyFrames Storyboard.TargetName="keyholeXlate"
                                  Storyboard.TargetProperty="X" >
    <LinearDoubleKeyFrame KeyTime="0:0:0" Value="1" />
    <LinearDoubleKeyFrame KeyTime="0:0:5" Value="120" />
    <LinearDoubleKeyFrame KeyTime="0:0:10" Value="-17" />
    <LinearDoubleKeyFrame KeyTime="0:0:15" Value="50" />
   </DoubleAnimationUsingKeyFrames >
   <DoubleAnimation Storyboard.TargetName="keyholeXlate" Storyboard.TargetProperty="Y"
                    From="0" To="30" Duration="0:0:15"  />
   <PointAnimation Storyboard.TargetName="pf" Storyboard.TargetProperty="StartPoint"
                   From="20,35" To="20,35.001" Duration="0:0:15"  />
  </Storyboard>
 </Grid.Resources>
 .....
</Grid>

Why do we move the StartPoint vertically from 35 to 35.001 in 15 seconds ? Such an unnoticeable animation seems to be ridiculous, to say the least ! This line is just there to make things work ! There is a bug in beta 2 : without such a dummy move, nothing is ever redrawn and nothing happens !

Of course, we still need to execute stb.Begin() in the function handling the Loaded event.

Now, let's change things to have the keyhole fixed and the painting animated :


Project / source code
The image in front is a painting named "Castel in the Pyrrhénées" from René Magritte, another Belgian surrealist painter (1898-1967). A black keyhole was inserted in this masterpiece (shame on me). Using Microsoft Photo Editor, we force black as the transparent color and save the image in png format.
The two images (Delvaux in the background and Magritte in the foreground) are inserted in a canvas whose size is the size of the foreground image. The canvas is clipped to the foreground image :
 <Canvas Width="266" Height="460" Clip="M0,0 h266 v460 h-266 z" >
  <Image x:Name="img" Source="Delvaux.jpg" Width="400" Height="270" >
   .....
  </Image>
  <Image Source="Magritte.png" Width="266" Height="460" />
 </Canvas> 

We now define an animation for the background image, in four step : from left to right, from top to bottom, from right to left and finally from bottom to top :
 <Image x:Name="img" Source="Delvaux.jpg" Width="400" Height="270" >
  <Image.Resources>
   <Storyboard x:Name="stb" RepeatBehavior="Forever">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="img" 
        Storyboard.TargetProperty="(Canvas.Left)" >
     <LinearDoubleKeyFrame KeyTime="0:0:0" Value="105" />
     <LinearDoubleKeyFrame KeyTime="0:0:5" Value="-210" />
     <LinearDoubleKeyFrame KeyTime="0:0:7" Value="-210" />
     <LinearDoubleKeyFrame KeyTime="0:0:12" Value="105" />
     <LinearDoubleKeyFrame KeyTime="0:0:14" Value="105" />
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="img"
       Storyboard.TargetProperty="(Canvas.Top)" >
     <LinearDoubleKeyFrame KeyTime="0:0:0" Value="105" />
     <LinearDoubleKeyFrame KeyTime="0:0:5" Value="105" />
     <LinearDoubleKeyFrame KeyTime="0:0:7" Value="10" />
     <LinearDoubleKeyFrame KeyTime="0:0:12" Value="10" />
     <LinearDoubleKeyFrame KeyTime="0:0:14" Value="105" />
    </DoubleAnimationUsingKeyFrames>
   </Storyboard>
  </Image.Resources>
 </Image>

See you for a forthcoming post on visual effects.

Tags:

Visual effects while displaying an image. Part II

by gleblanc 21. September 2008 18:13

In this second (but not last) post, we will discuss different techniques for animating the clipping area.
The image is a painting ("The persistence of memory") from Salvador Dali (1904-1989), the famous Spanish (Catalan) surrealist painter.

In this post, we'll animate the clipping area using an old animation technique, ie the timer.


Project / source code

We have two identical images : one, in the background, with an opacity of 0.3 and the other, on top, with a clipping area limited to a growing ellipse that will appear (at random location) at regular interval :
 <Image Source="Dali.jpg" Width="500" Height="363" Opacity="0.3" />
 <Image Source="Dali.jpg" Width="500" Height="363" >
  <Image.Clip>
   <EllipseGeometry x:Name="ellGeo" />
  </Image.Clip>
 </Image>

The animation is here based on the timer, a technique used by veterans of animations in computer programs. Though animation techniques introduced in WPF and Silverlight have to be privileged (see next post), the technique based on the timer is still there (do not throw it away too fast). To use the timer :
 using System.Windows.Threading;
 .....
 DispatcherTimer timer;  // outside of function

In the function that handles the Loaded event :
 private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
 {
  timer = new DispatcherTimer();
  timer.Interval = new TimeSpan(0, 0, 0, 0, 50); // 50 millisec
  timer.Tick += new EventHandler(timer_Tick);
  timer.Start();
 }
The following function is executed at regular interval :   
  void timer_Tick(object sender, EventArgs e)
  {
   .....  
  }

Our timer_Tick function is simple :

  • we reinitialize the EllipseGeometry properties every 100 ticks : new Center (at random location) and reduced radius (also a random small value), 
  • we make this ellipse bigger every tick.

  Random rndm;
  .....
  rndm = new Random();
  .....
  int N;
  void timer_Tick(object sender, EventArgs e)
  {
   if (N % 100==0)
   {
    X = rndm.Next(500); Y = rndm.Next(380);
    W = rndm.Next(10);
    ellGeo.Center = new Point(X, Y);
   }
   W += 5; ellGeo.RadiusX = ellGeo.RadiusY = W;
   N++;
  }

Now, let's go a step further : the clipping area is now made from several basic figures, here several growing ellipses, that are dynamically added to the clipping area :


Project / source code

Since the clipping area is build dynamically (in the timer_Tick function), the XAML is just :
  <Image Source="Dali.jpg" Width="500" Height="363" Opacity="0.3" />
  <Image Source="Dali.jpg" Width="500" Height="363" >
   <Image.Clip>
    <GeometryGroup x:Name="geoGroup" FillRule="Nonzero"  />
   </Image.Clip>
  </Image>

It's important to specify NonZero in the FillRule property. Otherwise, the intersection of two or several ellipses is not part of the clipping area.

The timer_Tick function becomes :
   void timer_Tick(object sender, EventArgs e)
   {
    if (N % 100 == 0) geoGroup.Children.Clear(); // reset clipping area
    if (N % 2 == 0)
    {
     // create a new hole in the clipping area
     EllipseGeometry eg = new EllipseGeometry();
     eg.Center = new Point(rndm.Next(0, 500), rndm.Next(0, 360)); 
     eg.RadiusX = eg.RadiusY = rndm.Next(5, 20);
     geoGroup.Children.Add(eg);
    }
    // grow and move slightly each ellipse
    foreach (EllipseGeometry g in geoGroup.Children)
    {
     g.RadiusX += 2; g.RadiusY += 2;
     g.Center = new Point(g.Center.X, g.Center.Y - 1);
    }
    N++;
   }

In the next post, we will use the Silverlight animation technique to animate the clipping area.

Tags:

Visual effects while displaying an image. Part I

by gleblanc 17. September 2008 13:45

Let's review different visual effects while displaying an image, from simple to more sophisticated. In this serie, we'll discuss animation, clipping area and opacity mask. Here, a first post dedicated to clipping. 

The image used in this post is a painting from Alfred Sisley (1839-1899), a French impressionnist painter, though British citizen (he asked for French citizenship but died before the Administration took a decision, due to missing papers - nothing is never new in this world).


Project / source code

The effect is based on a clipping area. Nothing is displayed outside the clipping area, here an ellipse. We give a name (ellGeo) to this ellipse and animate the RadiusX and RadiusY properties. When stb1.Begin() is executed, RadiusX jumps from 0 to 400 in two seconds. Simultaneously, RadiusY jumps from 0 to 300 (to be sure that the 400x300 rectangle is fully inside the ellipse).

 <Image x:Name="img" Source="Sisley.jpg" Width="400" Height="300" >
  <Image.Clip>
   <EllipseGeometry x:Name="ellGeo" Center="200, 150" RadiusX="400" RadiusY="300" />
  </Image.Clip>
  <Image.Resources>
   <Storyboard x:Name="stb1">
    <DoubleAnimation Storyboard.TargetName="ellGeo" Storyboard.TargetProperty="RadiusX" 
                     From="0" To="400" Duration="0:0:2" />
    <DoubleAnimation Storyboard.TargetName="ellGeo" Storyboard.TargetProperty="RadiusY" 
                     From="0" To="300" Duration="0:0:2" />
   </Storyboard>
  </Image.Resources>
 </Image>

Instead of giving a name to EllipseGeometry, we could write :

 <Image x:Name="img" Source="Sisley.jpg" Width="400" Height="300" >
  <Image.Clip>
   <EllipseGeometry Center="200, 150" RadiusX="400" RadiusY="300" />
  </Image.Clip>
  <Image.Resources>
   <Storyboard x:Name="stb1">
    <DoubleAnimation Storyboard.TargetName="img"
                     Storyboard.TargetProperty="(UIElement.Clip).(EllipseGeometry.RadiusX)"
                     From="0" To="400" Duration="0:0:2" />
    <DoubleAnimation Storyboard.TargetName="img"
                     Storyboard.TargetProperty="(UIElement.Clip).(EllipseGeometry.RadiusY)"
                     From="0" To="300" Duration="0:0:2" />      
   </Storyboard>
  </Image.Resources>
 </Image> 

Fine for the EllipseGeometry, thanks to its two properties (RadiusX and RadiusY) that are of double type. Not so easy with RectangleGeometry that has a property (Rect) with four values ! It's easier to use the PathGeometry.


Project / source code

As PathGeometry, we define one PathFigure (closed figure starting at upper-left corner) and made of three line segments (remember, it's a closed figure). Each line ending point is specified in the Point property. of course of type Point. Our animation will now be based on PointAnimation :    

  <Image x:Name="img" Source="Sisley.jpg" Width="400" Height="300" >
   <Image.Clip>
    <PathGeometry>
      <PathFigure StartPoint="0,0">
       <LineSegment x:Name="ls1" Point="400, 0" />
       <LineSegment x:Name="ls2" Point="400, 300" />
       <LineSegment Point="0, 300" />
      </PathFigure>
    </PathGeometry>
   </Image.Clip>
   <Image.Resources>
    <Storyboard x:Name="stb2">
     <PointAnimation Storyboard.TargetName="ls1" Storyboard.TargetProperty="Point"
                     From="0, 0" To="400, 0" Duration="0:0:2" />
     <PointAnimation Storyboard.TargetName="ls2" Storyboard.TargetProperty="Point"
                     From="0, 300" To="400, 300" Duration="0:0:2" />
    </Storyboard>
   </Image.Resources>
  </Image>

Since a path can be made of a succession of paths, we can do more complex animations. For instance :


Project / source code

The clipping path is made of five triangles, all having in common the image's center :
The five clipping areas
The animation is now made of five animations in succession, each elementary triangle being drawn in one second. The initial Point values in the lsxB LineSegment are such that the image is initially invisible. The values are changed in the corresponding PointAnimation :

  <Image x:Name="img" Source="Sisley.jpg" Width="400" Height="300" >
   <Image.Clip>
    <PathGeometry>
     <PathGeometry.Figures>
      <PathFigure StartPoint="200,150">
       <LineSegment x:Name="ls1A" Point="200, 0" />
       <LineSegment x:Name="ls1B" Point="200, 0" />
      </PathFigure>
      <PathFigure StartPoint="200,150" >
       <LineSegment x:Name="ls2A" Point="400, 0" />
       <LineSegment x:Name="ls2B" Point="400, 0" />
      </PathFigure>
      <PathFigure StartPoint="200,150" >
       <LineSegment x:Name="ls3A" Point="400, 300" />
       <LineSegment x:Name="ls3B" Point="400, 300" />
      </PathFigure>
       <PathFigure StartPoint="200,150" >
       <LineSegment x:Name="ls4A" Point="0, 300" />
      <LineSegment x:Name="ls4B" Point="0, 300" />
      </PathFigure>
      <PathFigure StartPoint="200,150" >
       <LineSegment x:Name="ls5A" Point="0, 0" />
       <LineSegment x:Name="ls5B" Point="0, 0" />
      </PathFigure>
     </PathGeometry.Figures>
    </PathGeometry>
   </Image.Clip>
   <Image.Resources>
    <Storyboard x:Name="stb3">
     <PointAnimation Storyboard.TargetName="ls1B" Storyboard.TargetProperty="Point"
                     From="200, 0" To="400, 0" Duration="0:0:1" />
     <PointAnimation BeginTime="0:0:1" Storyboard.TargetName="ls2B"
                     Storyboard.TargetProperty="Point" From="400, 0" To="400, 300"
                     Duration="0:0:1" />
     <PointAnimation BeginTime="0:0:2" Storyboard.TargetName="ls3B"
                     Storyboard.TargetProperty="Point" From="400, 300" To="0, 300"
                     Duration="0:0:1" />
     <PointAnimation BeginTime="0:0:3" Storyboard.TargetName="ls4B"
                     Storyboard.TargetProperty="Point" From="0, 300" To="0, 0"
                     Duration="0:0:1" />
     <PointAnimation BeginTime="0:0:4" Storyboard.TargetName="ls5B"
                     Storyboard.TargetProperty="Point" From="0, 0" To="200, 0" 
                     Duration="0:0:1" />
    </Storyboard>
   </Image.Resources>
  </Image>

To restart an animation (just before executing st3.Begin), we need to reinitialize the clipping path, to have the image disappearing :
 ls1A.Point = ls1B.Point = new Point(200, 0);
 ls2A.Point = ls2B.Point = new Point(400, 0);
 ls3A.Point = ls3B.Point = new Point(400, 300);
 ls4A.Point = ls4B.Point = new Point(0, 300);
 ls5A.Point = ls5B.Point = new Point(0, 0);

Instead of writing 
 <PointAnimation BeginTime="0:0:3" Storyboard.TargetName="ls4B"
                 Storyboard.TargetProperty="Point" From="0, 300" To="0, 0"
                 Duration="0:0:1" /> >

it's possible to write (remember, it's the fourth PointAnimation and we animate the second LineSegment) :
 <PointAnimation BeginTime="0:0:3" Storyboard.TargetName="img"
                 Storyboard.TargetProperty="(UIElement.Clip)
                                            .(PathGeometry.Figures)[3]
                                            .(PathFigure.Segments)[1]
                                            .(LineSegment.Point)"
                 From="0, 300" To="0, 0" Duration="0:0:1" />

As said Lewis Carroll in "Alice in Wonderland" : "I could have thought of a much more complicated way of doing it, said the Red Queen, immensely proud". 

In a forthcoming post, we will show more sophisticated animations, still based on the clipping area.

 

Tags:

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

About the author

Gerard Leblanc is the author of several books (in french) on C++, C#, .NET and Silverlight (Eyrolles, Paris as publisher). See www.gleblanc.eu as the companion web site for these books (included sample programs).
He is Microsoft MVP for Silverlight.

MVP logo