Menus, part V : Sliding menu

by gleblanc 25. November 2008 02:06

We continue our serie of animated menus, this time : the sliding menu.


Project / source code

As usual, our sliding menu is declared in a XML file, Menu.xml, inserted as a resource :
  <?xml version="1.0" encoding="utf-8" ?>
 <Menu>
  <Item Text="Italy" >
   <Item Miniature="MiniVenice.jpg" Id="11" Text="Venice, Grand Canal"/>
   .....
   <Item Miniature="MiniVatican.jpg" Id="15" Text="Vatican, St. Peter's Square" />
  </Item>
  <Item Text="USA" >
   <Item Miniature="MiniYosemite.jpg" Id="21" Text="Yosemite National park" />
   .....
   <Item Miniature="MiniGrand Canyon.jpg" Id="23" Text="Grand Canyon National Park"/>
  </Item>
  .....
 </Menu>

The user has just one constraint : reserve space for the menu on top of anything else in the window (this explains the Canvas.ZIndex) . Miniatures must be included as resources (they are displayed in sub-menus). The user can then handle the Click event :
<Grid x:Name="LayoutRoot" Background="White" >
  <Grid x:Name="menuGrid" Canvas.ZIndex="99" Height="40" VerticalAlignment="Top" >
   <gl:Menu Click="menuGrid_Click" />
  </Grid>
  <Grid x:Name="mainGrid" >
   .....
  </Grid>
 </Grid>

As we did for other menu, we build a Silverlight user control named Menu.xaml in the glMenu namespace. Our menu as a Grid containing a Canvas for the sub-menus and an horizontal StackPanel for the main menu items. Why a Canvas for the sub-menus ? They are animated (down animation for opening and up animation for closing) and it is not (unfortunately) possible to animate the Margin property :
<Grid x:Name="MenuRoot" >
  .....
  <Canvas x:Name="canSubMenus" />
  <StackPanel x:Name="spMainMenu" Orientation="Horizontal" Loaded="spMainMenu_Loaded" >
   ....
  </StackPanel>
 </Grid>

Since main menu items and sub-menu items are quite different, we create two classes : MainMenuItem and SubMenuItem. In memory, we also keep a list of main menu items and a table for sub-menu items (the number of entries in this table depends on the number of main menu items) :
 List<MainMenuItem> lstMainMenuItems;
 List<SubMenuItem>[] tabSubMenuItems;

The main menu is first build in the spMainMenu_Loaded function. Thanks to Linq for Xml, we analyze the XML file to find the main menu items :
lstMainMenuItems = new List<MainMenuItem>();
 XElement xmlRoot = XElement.Load("Menu.xml");
 getMainMenuItems(xmlRoot);
 .....
 void getMainMenuItems(XElement xml)
 {
  var lstItems = from p in xml.Elements("Item") select p;
  foreach (var el in lstItems)
   lstMainMenuItems.Add(new MainMenuItem {Text=el.Attribute("Text").Value, xElem=el});
 }

The main menu is then build using Silverlight UI elements : a Grid including a TextBlock (indeed two TextBlock to give a 3D effect to the display). For each main menu item, we handle the MouseEnter event (to make visible the corresponding sub-menu), the MouseLeave (to hide it) but also the MouseMove (leaving the main menu item thru the bottom side must leave the sub-menu as it is).

The sub-menus are then build (at this time, we know the number of sub-menu items). All the sub-menus are placed in a Canvas (canSubMenus). Each sub-menus (three here) are build on a canvas. Each sub-menu item (we have 5 for Italy and 3 for USA) are also based on a canvas containing an image (the miniature). For each of these canvas, we handle the MouseEnter, MouseLeave and MouseLeftButtonDown events.

Source code is included, as usual.

Tags:

Page turner updated for Silverlight 2 RTM

by gleblanc 17. November 2008 21:57

Mitsu Furuta (from Microsoft France) updated his Page Turner to Silverlight 2 RTM. Let's explain how to use it.

Let's first demonstrate the page turner, with paintings from Pablo Picasso (the Spanish-born painter is currently honored in Paris with several outstanding exhibitions). Please note that some pages are more than just images.


Project / source code

You can find Mitsu's component at the following address : www.codeplex.com/wpfbookcontrol. Codeplex is really becoming a major player ! Download the zip-file and unzip it. You will find SLMitsuControls.dll (a 35 KB file!) in the SLMitsuControls/ClientBin folder. The component is under Ms-Pl licence, meaning you can use it in your own products, even commercial, for free (no royaltie to Microsoft, even if your product is not free) but, of course, you can't claim rights on ownership.

Let's now create a Silverlight application. Add a reference to SLMitsuControls.dll (in the Silverlight part of the project, right-click on the project, Add Reference and Browse to the folder containing the SLMitsuControls.dll file).

Let's first assume that our book-like control just contains images. These images need to be copied in the ClientBin folder (the one containing the XAP file). We first need to populate our book with these images. We keep file names (without extension) in an array of string (in Page.xaml.cs) :
 string[] ts = {"Picasso", "Head", "Blind man", "At the Lapin Agile", "Chicks from Avignon",
                "Woman crying", "Guitar player", "Auto portrait"};

In the UserControl tag (in Page.xaml), we add a xmlns tag for the control (any other name than local is OK), handle the Loaded event and insert our book in the first row of the main grid (the second row will contain a status bar implemented as a TextBlock) :
 <UserControl x:Class="TurnThePage2.Page"
    ..... 
    xmlns:local="clr-namespace:SLMitsuControls;assembly=SLMitsuControls"
    ..... 
    Loaded="UserControl_Loaded" >
  <Grid x:Name="LayoutRoot" Background="LightBlue" >
   <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition Height="25" />
   </Grid.RowDefinitions>
   <local:UCBook x:Name="book" Grid.Row="0" OnPageTurned="book_OnPageTurned" Margin="20" />
   .....
  </Grid>
 </UserControl>
In the Page.xaml.cs file, we need to add a using, declare that the Page class implements the IDataProvider interface and implement two functions (just two small lines, always the same) :
 .....
 using SLMitsuControls;
 .....
 namespace TurnThePage2
 {
  public partial class Page : UserControl, IDataProvider
  {
   ..... 
   public int GetCount()
   {
    return pages.Count;
   }
   public object GetItem(int index)
   {
    return pages[index];
   }
  }
 }
In case our pages are just images, it's possible to keep them in a List<Image>. But some of our pages will be more elaborate (a page could for instance be a complex grid). So, we prefer List<UIElement> since UIElement is the base class for UI elements. But here, our book is (to begin) just a collection of images (only two images here, no need to show more) :
 List<UIElement> pages;
 .....
 private void UserControl_Loaded(object sender, RoutedEventArgs e)
 {
  pages = new List<UIElement> {
   new Image {Source=new BitmapImage(new Uri(ts[1]+".jpg",
              UriKind.Relative)), Stretch=Stretch.UniformToFill },
   new Image {Source=new BitmapImage(new Uri(ts[2]+".jpg", UriKind.Relative)),
              Stretch=Stretch.UniformToFill }
   };
  book.SetData(this);
 }
It's enough to get a page turner for images ! It's possible to handle the OnPageTurned event :
 private void book_OnPageTurned(int leftPageIndex, int rightPageIndex)
 {
  .....
 }
After a page turn, the arguments give you the left and right pages that are visible : starting at 1 (be careful !) and with -1 for absence of page.

Let's now complicate a bit : our first page will present an animated text on top of a photo and the last page will present a button on top of an image (remember : the images must be copied in the ClientBin folder). Our downloadable project is still a bit more complex (with a list box). We dynamically specify attributes for the grid in first page and dynamically add a TextBlock :  
 private void UserControl_Loaded(object sender, RoutedEventArgs e)
 {
  pages = new List {
            new Grid (),    // first page
            new Image ..... // second page (image)
            .....
            new Image ....
            new Grid()      // last page
           };
  // specify first page
  Grid g = pages[0] as Grid;
  g.Background = new ImageBrush { ImageSource = new BitmapImage(new Uri("Picasso.jpg", 
                                  UriKind.Relative)),
                                  Stretch = Stretch.UniformToFill }; 
  TextBlock tb = new TextBlock { Text = "Pablo\nPicasso", FontSize = 70,
                    Foreground = new SolidColorBrush(Color.FromArgb(255, 173, 216, 230)),
                    TextWrapping = TextWrapping.Wrap,
                    VerticalAlignment = VerticalAlignment.Center, 
                    HorizontalAlignment = HorizontalAlignment.Center,
                    TextAlignment = TextAlignment.Center };
  .....    // here the animation on FontSize
  g.Children.Add(tb);
  ......
The code to animate the FontSize property is :
 Storyboard stbPage0 = new Storyboard();
 Storyboard.SetTarget(stbPage0, tb);
 DoubleAnimation daSize = new DoubleAnimation {From = 70, To = 0,
                                RepeatBehavior = RepeatBehavior.Forever,
                                AutoReverse = true };
 daSize.Duration = new Duration(new TimeSpan(0, 0, 5));
 Storyboard.SetTargetProperty(daSize, new PropertyPath("FontSize"));
 stbPage0.Children.Add(daSize);  
 stbPage0.Begin();

The last page (with a button on top of an image) is similar :
 g = pages[pages.Count - 1] as Grid;
 g.Background = new ImageBrush { ImageSource = new BitmapImage(new Uri("Picasso2.jpg",
                                 UriKind.Relative)), Stretch = Stretch.UniformToFill };
 Button b = new Button { Content = "Back to the first page", Width = 130, Height = 50 };
 b.Click += new RoutedEventHandler(b_Click); 
 g.Children.Add(b);

The function that handles the click being, as usual :
 void b_Click(object sender, RoutedEventArgs e)
 {
  ..... 
 }
To go back to the first page, we just need to write :
 book.CurrentSheetIndex = 0;
It's possible to launch an animation to switch to a next or previous page : 
 book.AnimateToNextPage(1000);
 .....
 book.AnimateToPreviousPage(1000);
where the argument is the duration of the animation, expressed in milliseconds.

Tags:

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:

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