Click here to Skip to main content
Click here to Skip to main content
Go to top

Icon Menu for the .NET Compact Framework

, 21 Apr 2009
Rate this:
Please Sign up or sign in to vote.
How to implement an iPhone style icon menu using the .NET Compact Framework.

Introduction

Some time ago, I was complaining to a friend that I had no interesting projects to implement in my spare time; my friend told me to stop whining and just implement an iPhone style menu.

This article demonstrates a way to create an animated, editable icon menu similar to the one found on the iPhone home screen.

As a disclaimer, I'd like to say that this isn't an attempt to directly copy the iPhone menu; this article aims to describe some coding techniques that we can employ when animating a GUI (the iPhone root menu is far, far, far better than this implementation).

Some of the code in this article is similar to the animation code in my Implementing a smoothly animated ListBox article, but this one uses custom drawing and offscreens which are concepts that can be very useful when spicing up a GUI.

Requirements

Before I started this implementation, I decided that my menu had to fulfill the following requirements:

  • Easy to operate using fingers instead of stylus.
  • Ability to manually re-arrange the layout of the icons during runtime.
  • Ability to manually delete icons during runtime.

I haven't implemented support for the neat paging functionality found on the iPhone where the menu extends over multiple screens, but I'm tempted to see if I can hook this menu up to my smooth listbox and achieve the same effect.

Using the Code

The Zip file contains a Visual Studio solution with two projects: one for PocketPC 2003, and one for the Windows Desktop. I find it easier and faster to test my stuff on the desktop as I don't have to wait for the emulator then.

The menu works in this way:

  • Single click an icon to launch or open it.
  • Click and hold to enter edit mode; edit mode is indicated by the icons wiggling and a delete icon in the upper left corner of the icons that can be deleted. Drag and release to re-arrange the icons.
  • To exit edit mode, single click somewhere. If the the delete icon is clicked, the icon is deleted.

In the example implementation, only the Calculator icon has a handler attached to it, so that's the only one where a single click has an effect.

The Menu

For this project, I started out with creating a User Control for the menu. This control is responsible for hosting the icons and animating them, but it is not responsible for the actual position of each icon, that is up to the Icon class. The IconMenu class only tells its icons where they probably should be, it's up to the icons to actually render themselves in that position if they want.

Align to Grid

On the iPhone, the icons align themselves to a neat grid, therefore my IconMenu class contains a list of Slots, which is a class that has a Rectangle defining its bounds and possibly a reference to an icon. The IconMenu has a list of Slots rather than a two dimensional array of Slots as it re-arranges the slots when the screen is tilted.

public class Slot
{
    private Rectangle bounds;

    private Icon icon;

    public Slot(Rectangle bounds)
    {
        this.bounds = bounds;
    }

    public bool IsEmpty
    {
        get { return icon == null; }
    }

    public Icon Icon
    {
        get { return icon; }
        set { icon = value; }
    }

    public Rectangle Bounds
    {
        get { return bounds; }
    }
}

The IconMenu Class

The IconMenu class has four tasks as its main responsibility:

  • Own the icons and provide the preferred layout for them
  • Handle animation
  • Handle mouse/stylus input
  • Fire events to client code

Icon Ownership

By maintaining a list of Icons as well as a list of Slots, the IconMenu can assign each Icon to a Slot, and since the Slots have layout information stored as a Rectangle, the IconMenu will have the preferred position for each icon as soon as it's assigned to a Slot.

The Icons also keep track of the Slot they belong to in order to be able to get their preferred position when they need to be rendered to screen.

The Icons and Slots are assigned in the AssignSlotToIcons method:

private void AssignSlotToIcons()
{
    foreach (Slot slot in slots)
    {
        slot.Icon = null;
    }

    foreach (Icon icon in icons)
    {
        icon.Slot = null;
        foreach (Slot slot in slots)
        {
            if (slot.IsEmpty)
            {
                icon.Slot = slot;
                slot.Icon = icon;
                break;
            }
        }
    }
}

This is done whenever the IconMenu needs to re-arrange the Icons, which in turn is whenever the IconMenu size changes or an Icon is deleted.

The Relayout method takes care of rearranging the Slots:

private void Relayout()
{
    // This forces a new offscreen to be generated
    // when the screen changes size.
    offscreen = null;
    
    // Calculate how many vertical and horizontal slots will fit
    int numberOfHorizontalSlots = Width / slotWidth;
    int numberOfVerticalSlots = Height / slotHeight;

    // Figure out how much padding is required to center the icons
    int horizontalPadding = (Width % slotWidth) / 2;
    int verticalPadding = (Height % slotHeight) / 2;

    // Sort the icons according to the slot they're currently in
    icons.Sort(delegate(Icon left, Icon right)
    {
        if (left.Slot == null)
            return 1;
        else
        {
            if (right.Slot == null)
                return -1;
        }

        return slots.IndexOf(left.Slot) - slots.IndexOf(right.Slot);
    });

    slots = new List<slot>();

    // Iterate over as many slots that fit on the screen
    for (int i = 0; verticalPadding + (i + 1) * slotHeight < Height; ++i)
    {
        for (int j = 0; j < numberOfHorizontalSlots; ++j)
        {
            // Add a new Slot with the preferred position 
            slots.Add(
                new Slot(
                new Rectangle(horizontalPadding + j * slotWidth, 
                verticalPadding + i * slotHeight, 
                slotWidth, 
                slotHeight)));
        }
    }
    
    // Assign the Slots to Icons and vice versa
    AssignSlotToIcons();
    Invalidate();
}

This way, the Icons will always (almost) have a Slot where they belong; the exception to this rule is when the layout of the icons are manually changed during runtime by the user. In that scenario, it's the icon's responsibility to ignore its assigned slot and render itself where the stylus/mouse is.

Rendering

In order to improve performance, I decided to go for custom rendering, and it's convenient as it also gives me more control over how to draw the control.

To reduce flicker, I used an "offscreen"; this is a concept where all the UI elements are first rendered to a non-visible memory buffer, and then the contents of that buffer are copied onto the screen memory. As the copy operation is fast, this prevents any half drawn controls to be momentarily displayed to the user. And, that reduces or removes the flicker.

The offscreen is actually just a Graphics object from an Image, and the Image is recreated to the size of the IconMenu whenever the menu's size changes.

public class IconMenu : UserControl
{
    ...

    private Image offscreenImage = null;
    private Graphics offscreen = null;
    

    private void CreateOffscreen()
    {
        offscreenImage = new Bitmap(Math.Max(Width, 1), Math.Max(Height, 1));
        offscreen = Graphics.FromImage(offscreenImage);
    }    
    
    ...
}

Rendering is done in OnPaintBackground by first filling the entire offscreen with the background color and then requesting all Icons, except the currently selected one, to render themselves onto the offscreen. The reason the selected one is exempted is so that it can be rendered last and thus be rendered on top of all the others as it would be annoying to have the current control partly or completely obscured by another control.

protected override void OnPaintBackground(PaintEventArgs e)
{
    if (offscreen == null)
    {
        CreateOffscreen();
    }

    offscreen.FillRectangle(new SolidBrush(BackColor), ClientRectangle);

    foreach (Icon icon in icons)
    {
        if (!icon.IsMouseControlled)
        {
            icon.Paint(offscreen, state == State.Edit, deleteMarker);
        }
    }

    if (selectedIcon != null)
    {
        selectedIcon.Paint(offscreen, state == State.Edit, deleteMarker);
    }
    e.Graphics.DrawImage(offscreenImage, 0, 0);
}

The Icon Class

The Icon class is responsible for the visual representation of an icon, its position, and for maintaining the delegates that should be called if the icon is clicked.

In order to get the Icons to move around to a new slot when a user is manually re-arranging the icons, I decided to store two positions: the actual location and the desired location. That means that if a user drags the selected Icon into another Icon's Slot, I only need to set a new desired location for the Icon that needs to move out of the way to give room to the selected Icon. The Icon can figure out itself how to get there and at what speed.

public class Icon
{
    ...
    
    private Vector location = new Vector();
    private Vector desiredLocation = new Vector();

    ...

    public void UpdateOnMouseLocation(Point mouseLocation)
    {
        desiredLocation = (Vector)mouseLocation;
        
        // Adjust the desired location based on this Icon's Image's size 
        // (right shifting one is a neat way of dividing by two).
        desiredLocation.X -= image.Width >> 1;
        desiredLocation.Y -= image.Height >> 1;
    }

}

Animating the Icon

The Icon's animation method re-calculates the Icon's actual position, and it considers two things:

  • Is the Icon currently being dragged (mouseControlled is true)?
  • Is the IconMenu being manually re-arranged by the user?

If the Icon is being dragged, then its location is always set to its desired location, which is the location of the mouse/stylus.

If the IconMenu is being re-arranged, then a random offset is added to make the Icons wobble around a bit. Unfortunately, I can't easily apply a rotational transformation like on the iPhone, so I settled for just having the icons wiggle a bit instead. It doesn't look as good as on the iPhone, but it's good enough for this article I think.

public void Animate(float elapsedTime, bool inEditState)
{
    if (mouseControlled)
    {
        location = desiredLocation;
    }
    else
    {
        if (slot != null)
        {
            int x = slot.Bounds.Location.X + 
                ((slot.Bounds.Width - size.Width) >> 1);
            int y = slot.Bounds.Location.Y + 
                ((slot.Bounds.Height - size.Height) >> 1);

            if (inEditState)
            {
                x += random.Next(3, 6) * (random.Next(10) > 5 ? -1 : 1);
                y += random.Next(3, 6) * (random.Next(10) > 5 ? -1 : 1);
            }
            desiredLocation.Set(x, y);
        }
        
        // Calculate the direction from the current location to the 
        // desired location.
        Vector targetVector = desiredLocation - location;
        
        // Calculate the velocity with which the Icon should move from 
        // the current location to the desired location; this velocity 
        // is always the number of pixels to the target per second.
        float velocity = targetVector.Length;
        targetVector.Normalize();

        location += targetVector * velocity * elapsedTime;
    }
}

Rendering of the Icon

Rendering of the Icon takes care of rendering the Icon at its desired location as well as rendering the delete icon in the upper left corner in edit mode.

private void DrawImage(Graphics graphics, Image image, 
             Point point, int width, int height)
{
    graphics.DrawImage(image, new Rectangle(point.X, point.Y, width, height), 
                       0, 0, width, height, 
                       GraphicsUnit.Pixel, imageAttributes);
}

public void Paint(Graphics graphics, bool inEditState, Image deleteMarker)
{
    Point point = (Point)location;
    DrawImage(graphics, image, point, size.Width, size.Height);

    if (!readOnly && inEditState && deleteMarker != null)
    {
        DrawImage(graphics, deleteMarker, point, 
                  deleteMarker.Width, deleteMarker.Height);
    }
}

This image shows the icon menu in edit state:

Handling Input

As with my smooth list box, mouse down, move, and up events are handled to figure out what has been clicked or dragged. The first of these, mouse down, is simple enough:

private void HandleMouseDown(object sender, MouseEventArgs e)
{
    mouseIsDown = true;
    mouseDownTimestamp = DateTime.Now;

    mouseDownPosition = new Vector(e.X, e.Y);
    Icon icon = GetIconAtPoint((Point)mouseDownPosition);
    if (icon != null)
    {
        selectedIcon = icon;
    }
}

Simply record the time stamp so that we can figure out if this is a click or hold, and get the icon under the mouse/stylus (if any). This is done using one of two helper methods:

private Slot GetSlotAtPoint(Point point)
{
    foreach(Slot slot in slots)
    {
        if (slot.Bounds.Contains(point))
        {
            return slot;
        }
    }
    return null;
}

private Icon GetIconAtPoint(Point point)
{
    foreach (Icon icon in icons)
    {
        if (icon.Slot.Bounds.Contains(point))
        {
            return icon;
        }
    }
    return null;
}

The other method, GetSlotAtPoint, is, of course, used to get the slot under the mouse/stylus.

The second event, mouse move, is slightly more complicated as there are more things to handle if a mouse is selected (as detected in HandleMouseDown).

void HandleMouseMove(object sender, MouseEventArgs e)
{
    if (selectedIcon != null)
    {
        // Are we already in edit mode
        if (state == State.Edit)
        {
            // Figure out if the dragged icon has been dragged onto another
            // slot, and it that case, swap the Slots referenced Icons. 
            // Also move all the other icons out of the way as well so
            // that the current icon order is maintained.
            Point mouseLocation = new Point(e.X, e.Y);
            selectedIcon.UpdateOnMouseLocation(mouseLocation);

            Slot slot = GetSlotAtPoint(mouseLocation);
            if (slot != null)
            {
                int selectedIndex = slots.IndexOf(selectedIcon.Slot);
                int currentIndex = slots.IndexOf(slot);
                if (selectedIndex != currentIndex)
                {
                    for (int i = selectedIndex; 
                         i != currentIndex; 
                         i += Math.Sign(currentIndex - selectedIndex))
                    {
                        Slot slotA = slots[i];
                        Slot slotB = slots[i + Math.Sign(currentIndex - selectedIndex)];

                        slotA.Icon = slotB.Icon;
                        if (slotA.Icon != null)
                        {
                            slotA.Icon.Slot = slotA;
                        }
                    }

                    slot.Icon = selectedIcon;
                    selectedIcon.Slot = slot;
                }
            }
        }
        else
        {
            // If the mouse has been down for more than clickDelay go into edit mode
            if (DateTime.Now - mouseDownTimestamp >= clickDelay)
            {
                state = State.Edit;
                selectedIcon.IsMouseControlled = true;
            }
        }
    }
}

Last part, mouse up, also has a few things to do:

private void HandleMouseUp(object sender, MouseEventArgs e)
{
    mouseIsDown = false;
    if (selectedIcon != null)
    {
        // Was the mouse down long enough for a click?
        if (DateTime.Now - mouseDownTimestamp < clickDelay)
        {
            switch (state)
            {
                case State.Launch:
                    // Fire the launch event to let client code
                    // figure out how to start some application
                    selectedIcon.OnLaunch(); 
                    break;
                case State.Edit:
                    // If we're in edit mode and this was a click in the
                    // upper left corner of an icon and that is not
                    // read only, run delete icon code
                    Slot slot = GetSlotAtPoint((Point)mouseDownPosition);
                    if (deleteMarker != null && 
                        slot != null && 
                        !slot.IsEmpty && 
                        !slot.Icon.IsReadOnly)
                    {
                        Vector slotPosition = (Vector)slot.Icon.Location;
                        Vector positionInSlot = mouseDownPosition - slotPosition;

                        if (positionInSlot.X < deleteMarker.Width && 
                            positionInSlot.Y < deleteMarker.Height)
                        {
                            DialogResult result = 
                                MessageBox.Show(
                                    "Are you sure you want to delete this?", 
                                    "Delete", 
                                    MessageBoxButtons.OKCancel, 
                                    MessageBoxIcon.Question, 
                                    MessageBoxDefaultButton.Button2);
                                    
                            if (result == DialogResult.OK)
                            {
                                icons.Remove(slot.Icon);
                                OnDeleted(slot.Icon);
                                slot.Icon = null;
                                Relayout();
                            }
                            break;
                        }
                    }
                    state = State.Launch;
                    selectedIcon.IsMouseControlled = false;
                    break;
            }
        }
        else
        {
            int selectedIndex = slots.IndexOf(selectedIcon.Slot);
            if (selectedIndex > icons.Count - 1)
            {
                selectedIcon.Slot.Icon = null;
                Slot slot = slots[icons.Count - 1];
                slot.Icon = selectedIcon;
                selectedIcon.Slot = slot;
            }
        }
        selectedIcon.IsMouseControlled = false;
        selectedIcon = null;
    }
    else
    {
        // If we were in edit and got a single click we're leaving edit mode
        if (state == State.Edit)
        {
            state = State.Launch;
        }
    }
}

Missing Stuff

Like I stated in the Introduction, this isn't intended to be a complete implementation, it's just to show how this type of functionality can be achieved. Because of this, there are quite a few shortcomings in this implementation:

  • The animation should be paused when the icons are not visible to save CPU cycles.
  • The icons should have a defined pattern for moving offscreen and back on when an icon is launched.
  • There's a bug that will cause the list to break if there are more icons than slots.

Sorry about these.

As always, any comments and suggestions are most welcome.

History

  • 2009-04-21: First version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Fredrik Bornander
Software Developer (Senior)
Sweden Sweden
Article videos
Oakmead Apps Android Games
 
21 Feb 2014: Best VB.NET Article of January 2014 - Second Prize
18 Oct 2013: Best VB.NET article of September 2013
23 Jun 2012: Best C++ article of May 2012
20 Apr 2012: Best VB.NET article of March 2012
22 Feb 2010: Best overall article of January 2010
22 Feb 2010: Best C# article of January 2010

Comments and Discussions

 
GeneralI like it! Pinmemberanfuson18-Nov-09 20:01 
GeneralRe: I like it! PinmemberFredrik Bornander18-Nov-09 20:57 
GeneralRe: I like it! Pinmemberanfuson18-Nov-09 21:42 
It's a Shell UI for WM cellphone by .netcf. To use gdi or gdi+ as far as possible , while i want to choose directdraw.
However, without managed directdraw wrapper. Here is 【WindowsMoibleManagedDDraw】. But it's a little ... ha!
could you share your experience about directdraw with me?
Generalthere are some errors in run it PinmemberSeraph_summer25-Apr-09 4:42 
GeneralRe: there are some errors in run it PinmemberFredrik Bornander26-Apr-09 11:52 
GeneralRe: there are some errors in run it PinmemberDr.Luiji27-Apr-09 2:02 
GeneralGood job PinmemberDr.Luiji22-Apr-09 23:56 
GeneralRe: Good job PinmemberFredrik Bornander26-Apr-09 11:48 
GeneralRe: Good job PinmemberDr.Luiji27-Apr-09 1:57 
GeneralThis sucks, why would I want to use this PinmvpSacha Barber22-Apr-09 2:29 
GeneralRe: This sucks, why would I want to use this PinmemberJakub Mller22-Apr-09 4:14 
GeneralRe: This sucks, why would I want to use this PinmvpSacha Barber22-Apr-09 4:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140916.1 | Last Updated 22 Apr 2009
Article Copyright 2009 by Fredrik Bornander
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid