A couple days ago, I was playing around in Blend (4) because I wanted to make a menu for a keyboard that runs a modified version of Silverlight 3. I thought that I might be able to do the whole menu without writing much code, since there are a bunch of built-in controls and behaviors. In the end, though, my menu ended up being about 99% C# (and 1% auto-generated cruft), since I needed the menu for later parts in my project and I knew I could code it quickly enough.
Here’s a build of the menu from before I integrated it with a real data source: AdaptiveMenu
(Pressing Edit, File on the top, Recent, and then “x” should give you a good idea of what how it animates.)
If I had known how to use Blend… might I have been able to make this menu mostly from the design mode? Would that have been simpler?
So you can see how it was structured, here’s the main C# file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
namespace AdaptiveMenu
{
public partial class MainPage : UserControl
{
private static int ADAPTIVE_WIDTH = 1366;
private static int ADAPTIVE_MENU_WIDTH = 1266;
private static int ADAPTIVE_HEIGHT = 216;
private static int ADAPTIVE_TAB_HEIGHT = 66;
private static int ADAPTIVE_MAIN_HEIGHT = 150;
private List<AdaptiveButton> buttons;
private List<AdaptiveButton> subButtons;
// hard-coded demo menu. these will be supplied by the host computer's UIAutomation
private string[] tabTitles = {"Google.com", "ROC HCI", "to.", "example.com"};
private string[] items = {"File", "Edit", "View", "Object", "Project", "Back"};
private string[] fileItems = {"New", "Open", "Recent", "Close", "Save"};
private string[] editItems = {"Undo", "Redo", "Cut", "Copy", "Paste", "Find", "Replace"};
private string[] viewItems = {"Zoom In", "Zoom Out", "Actual Size"};
private string[] recentItems = {"files.txt", "secretInfo.docx", "hmmm.xlsx"};
private string[] subItems = {"Copy", "Paste", "Preferences"};
private List<AdaptivePath> tabs;
private List<AdaptiveButton> tabButtons;
private List<AdaptiveButton> mainButtons;
private List<AdaptiveButton> topButtons;
private List<AdaptiveButton> incomingButtons;
private Button resetButton;
private Dictionary<AdaptivePath, List<AdaptivePath>> menuMap;
private AdaptivePath root;
public MainPage()
{
// Required to initialize variables
InitializeComponent();
menuMap = new Dictionary<AdaptivePath, List<AdaptivePath>>();
InitMenu();
this.Loaded += new RoutedEventHandler(OnLoad);
}
void OnLoad(object sender, RoutedEventArgs e){
InitInitialButtons();
InitResetButton();
}
void InitInitialButtons(){
// at the start, we display the root (null) menu and some tabs
mainButtons = MakeButtons(GetRow(null), ADAPTIVE_MAIN_HEIGHT);
topButtons = MakeButtons(tabs, ADAPTIVE_TAB_HEIGHT);
// position main-sized buttons
for(int i = 0; i < mainButtons.Count; i++){
AdaptiveButton b = mainButtons*;
b.Click += new RoutedEventHandler(OnClick);
Canvas.SetTop(b, ADAPTIVE_TAB_HEIGHT);
}
}
void InitResetButton(){
resetButton = new Button();
resetButton.Content = "x";
resetButton.Width = ADAPTIVE_WIDTH - (ADAPTIVE_MENU_WIDTH);
resetButton.Height = ADAPTIVE_MAIN_HEIGHT + ADAPTIVE_TAB_HEIGHT;
LayoutRoot.Children.Add(resetButton);
Canvas.SetLeft(resetButton, ADAPTIVE_MENU_WIDTH);
resetButton.IsEnabled = false;
resetButton.Click += new RoutedEventHandler(OnResetClick);
}
// handler for "main" button clicks
void OnClick(object sender, RoutedEventArgs e){
AdaptiveButton b = sender as AdaptiveButton;
AdaptivePath p = b.Path;
// don't do anything unless this button leads somewhere
// TODO: in actuality, this needs to contact the main app and use UIAutomation here
if(p != null && GetRow(p) != null){
incomingButtons = MakeButtons(GetRow(p), ADAPTIVE_MAIN_HEIGHT);
for(int i = 0; i < incomingButtons.Count; i++){
AdaptiveButton ib = incomingButtons*;
Canvas.SetTop(ib, ADAPTIVE_MAIN_HEIGHT + ADAPTIVE_TAB_HEIGHT);
}
CompositionTarget.Rendering += new EventHandler(AnimateShiftUp);
}
}
// handler for the top-row buttons
void OnClickTop(object sender, RoutedEventArgs e){
AdaptiveButton b = sender as AdaptiveButton;
AdaptivePath p = b.Path;
// TODO: again, we need UIAutomation here for the actual implementation of this menu
if(p != null && GetRow(p) != null){
// remove old main buttons
foreach(AdaptiveButton ob in mainButtons){
ob.Click -= OnClick;
LayoutRoot.Children.Remove(ob);
}
// add replacement buttons
mainButtons = MakeButtons(GetRow(p), ADAPTIVE_MAIN_HEIGHT);
for(int i = 0; i < mainButtons.Count; i++){
AdaptiveButton mb = mainButtons*;
Canvas.SetTop(mb, ADAPTIVE_TAB_HEIGHT);
mb.Opacity = 0;
}
CompositionTarget.Rendering += new EventHandler(AnimateFadeIn);
}
}
void OnResetClick(object sender, RoutedEventArgs e){
foreach(AdaptiveButton b in topButtons){
b.Click -= OnClickTop;
LayoutRoot.Children.Remove(b);
}
foreach(AdaptiveButton b in mainButtons){
b.Click -= OnClick;
LayoutRoot.Children.Remove(b);
}
InitInitialButtons();
foreach(AdaptiveButton b in topButtons){
b.Opacity = 0;
}
foreach(AdaptiveButton b in mainButtons){
b.Opacity = 0;
}
resetButton.IsEnabled = false;
CompositionTarget.Rendering += new EventHandler(AnimateResetFadeIn);
}
void AnimateShiftUp(object sender, EventArgs e){
double dIncoming = Canvas.GetTop(incomingButtons[0]) - ADAPTIVE_TAB_HEIGHT;
double dRest = Canvas.GetTop(mainButtons[0]);
double dScale = mainButtons[0].Height - ADAPTIVE_TAB_HEIGHT;
double moveAmountIncoming = MoveFunc(dIncoming);
double moveAmountRest = MoveFunc(dRest);
double scaleAmount = MoveFunc(dScale);
if(Canvas.GetTop(mainButtons[0]) > 0){ // still animating
foreach(AdaptiveButton b in topButtons){
Canvas.SetTop(b, Canvas.GetTop(b) - moveAmountRest);
}
foreach(AdaptiveButton b in mainButtons){
Canvas.SetTop(b, Canvas.GetTop(b) - moveAmountRest);
b.Height -= scaleAmount;
}
foreach(AdaptiveButton b in incomingButtons){
Canvas.SetTop(b, Canvas.GetTop(b) - moveAmountIncoming);
}
} else { // done animating
foreach(AdaptiveButton b in topButtons){
b.Click -= OnClickTop;
LayoutRoot.Children.Remove(b);
}
foreach(AdaptiveButton b in mainButtons){
b.Click -= OnClick;
b.Click += new RoutedEventHandler(OnClickTop);
}
foreach(AdaptiveButton b in incomingButtons){
b.Click += new RoutedEventHandler(OnClick);
}
topButtons = mainButtons;
mainButtons = incomingButtons;
resetButton.IsEnabled = true;
CompositionTarget.Rendering -= AnimateShiftUp;
}
}
void AnimateFadeIn(object sender, EventArgs e){
double d = 100 * (1 - mainButtons[0].Opacity);
double delta = MoveFunc(d, true) / 100;
// still animating opacity up to 1
if(mainButtons[0].Opacity < 1){
foreach(AdaptiveButton b in mainButtons){
b.Opacity += delta;
}
} else { // done animating
foreach(AdaptiveButton b in mainButtons){
b.Click += new RoutedEventHandler(OnClick);
}
CompositionTarget.Rendering -= AnimateFadeIn;
}
}
// very similar to AnimateFadeIn, but it also fades in the top buttons
void AnimateResetFadeIn(object sender, EventArgs e){
double d = 100 * (1 - mainButtons[0].Opacity);
double delta = MoveFunc(d, true) / 100;
if(mainButtons[0].Opacity < 1){
foreach(AdaptiveButton b in topButtons){
b.Opacity += delta;
}
foreach(AdaptiveButton b in mainButtons){
b.Opacity += delta;
}
} else {
CompositionTarget.Rendering -= AnimateResetFadeIn;
}
}
// easing function for animating button motion
double MoveFunc(double distance, bool slow = false){
double speed = 20;
if(slow){
speed = 6;
}
int step = 1;
if(slow){
step = 4;
}
double moveAmount = distance * (speed / 100);
return Math.Min(Math.Max(moveAmount, step), distance);
}
// a wrapper for Dictionary access that handles invalid keys and the null key as our root
List<AdaptivePath> GetRow(AdaptivePath pressed){
if(pressed == null){
return menuMap[root];
}
if(!menuMap.ContainsKey(pressed)){
return null;
}
return menuMap[pressed];
}
List<AdaptiveButton> MakeButtons(List<AdaptivePath> row, int height){
List<AdaptiveButton> buttonRow = new List<AdaptiveButton>(row.Count);
for(int i = 0; i < row.Count; i++){
AdaptivePath p = row*;
AdaptiveButton b = new AdaptiveButton();
b.Path = p;
//b.Name = p.Name; // names MUST be unique
b.Content = p.Name;
b.Width = ADAPTIVE_MENU_WIDTH / row.Count;
b.Height = height;
buttonRow.Add(b);
LayoutRoot.Children.Add(b);
Canvas.SetLeft(b, i * ((double) ADAPTIVE_MENU_WIDTH / row.Count));
}
return buttonRow;
}
/**
* This is just a hardcoded menu, because normally, Silverlight won't be generating the menu
* structure or items, and it won't know it ahead of time.
*/
void InitMenu(){
root = new AdaptivePath();
root.Name = "~Root~";
List<AdaptivePath> topLevel = new List<AdaptivePath>(items.Length);
foreach(string s in items){
AdaptivePath ap = new AdaptivePath();
ap.Name = s;
topLevel.Add(ap);
if(s == "File"){
List<AdaptivePath> fileLevel = new List<AdaptivePath>(fileItems.Length);
foreach(string ss in fileItems){
AdaptivePath ap2 = new AdaptivePath();
ap2.Name = ss;
fileLevel.Add(ap2);
if(ss == "Recent"){
List<AdaptivePath> recentLevel = new List<AdaptivePath>(recentItems.Length);
foreach(string s3 in recentItems){
AdaptivePath ap3 = new AdaptivePath();
ap3.Name = s3;
recentLevel.Add(ap3);
// this conditional makes a recursive section
if(s3 == "hmmm.xlsx"){
menuMap[ap3] = fileLevel;
}
if(s3 == "secretInfo.docx"){
menuMap[ap3] = topLevel;
}
}
menuMap[ap2] = recentLevel;
}
}
menuMap[ap] = fileLevel;
} else if(s == "Edit"){
List<AdaptivePath> editLevel = new List<AdaptivePath>(editItems.Length);
foreach(string ss in editItems){
AdaptivePath ap2 = new AdaptivePath();
ap2.Name = ss;
editLevel.Add(ap2);
}
menuMap[ap] = editLevel;
} else if(s == "View"){
List<AdaptivePath> viewLevel = new List<AdaptivePath>(viewItems.Length);
foreach(string ss in viewItems){
AdaptivePath ap2 = new AdaptivePath();
ap2.Name = ss;
viewLevel.Add(ap2);
}
menuMap[ap] = viewLevel;
}
}
menuMap[root] = topLevel;
tabs = new List<AdaptivePath>();
foreach(string title in tabTitles){
AdaptivePath tp = new AdaptivePath();
tp.Name = title;
tabs.Add(tp);
}
}
}
}