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.