Make a modal context menu in C#

modal context menu

This example shows how you can make a modal context menu. Normally when a context menu appears, the user can press Esc or can click off of the menu to make it disappear. Some while ago, someone asked me whether you could make a modal context menu. He wanted to be able to display a floating menu and make the user pick a selection before continuing.

The ContextMenuStrip control doesn’t work that way. Context menus are supposed to pop up when the user wants them not when the program wants to display them. I think the idea is that if the user makes the menu appear, then the user should be able to make the menu disappear.

However, with some work you can make a form that behaves a lot like a modal context menu. This is a fairly long post so it’s divided into two sections:

The Dialog Form

This example includes a form named ModalMenuForm that contains a single ListBox named lstItems. The program displays the user’s choices (Pink, Light Green, and Light Blue) in the ListBox, but ListBox controls don’t behave like ContextMenuStrips, so the form needs to do some extra work to make it look like a context menu.

To make the ListBox look like a context menu, the ListBox is owner drawn. For information about owner-drawn ListBox controls, see these posts:

When the form loads, it uses the following code to set up the owner-drawn ListBox.

// Get the form ready.
private void ModalMenuForm_Load(object sender, EventArgs e)
{
    // Make the ListBox owner-drawn.
    lstItems.DrawMode = DrawMode.OwnerDrawVariable;

    // Set form properties.
    this.FormBorderStyle = FormBorderStyle.None;
    this.KeyPreview = true;

    // Make the form fit the ListBox.
    this.ClientSize = lstItems.Size;
}

This code makes the ListBox owner-drawn. It also sets the form’s FormBorderStyle property to false so it doesn’t display window controls, a title bar, or a border.

The code sets the form’s KeyPreview property to true so it can look for the Enter and Esc keys. Finally this code resizes the form to fit its ListBox.

The form uses the following code to draw the owner-drawn ListBox.

// Calculate the size of an item.
private int ItemMargin = 5;
private void lstItems_MeasureItem(object sender,
    MeasureItemEventArgs e)
{
    // Get the ListBox and the item.
    ListBox lst = sender as ListBox;
    string txt = lst.Items[e.Index].ToString();

    // Measure the string.
    SizeF txt_size = e.Graphics.MeasureString(txt, this.Font);

    // Set the required size.
    e.ItemHeight = (int)txt_size.Height + 2 * ItemMargin;
    e.ItemWidth = (int)txt_size.Width;
}

// Draw the item.
private void lstItems_DrawItem(object sender, DrawItemEventArgs e)
{
    // Get the ListBox and the item.
    ListBox lst = sender as ListBox;
    string txt = lst.Items[e.Index].ToString();

    // Draw the background.
    e.DrawBackground();

    if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
    {
        // Selected. Draw with the system highlight color.
        e.Graphics.DrawString(txt, this.Font,
            SystemBrushes.HighlightText, e.Bounds.Left,
            e.Bounds.Top + ItemMargin);
    }
    else
    {
        // Not selected. Draw with ListBox's foreground color.
        using (SolidBrush br = new SolidBrush(e.ForeColor))
        {
            e.Graphics.DrawString(txt, this.Font, br,
                e.Bounds.Left, e.Bounds.Top + ItemMargin);
        }
    }

    // Don't draw the focus rectangle for
    // this example because the user cannot use
    // the arrow keys to change the selection.
    //// Draw the focus rectangle if appropriate.
    //e.DrawFocusRectangle();
}

The MeasureItem event handler returns the size needed for each of the menu items. The DrawItem event handler actually draws the items when needed. (See the links earlier for more details.)

There are three points worth mentioning in the DrawItem event handler. First, if the mouse is over the item being drawn, the DrawBackground method draws an appropriate highlighted background. (In the picture, the Pink choice is highlighted.)

Second, if the item being drawn is selected, the code draws its text with the system highlight color so it looks like a highlighted menu item.

Third, the code does not draw a focus rectangle around the selected item. The focus rectangle shows which item is selected if you are tabbing among controls. Because you can’t tab off of the modal ListBox, there’s no reason to do this. (Besides, it looks funny.)

The form needs a few other pieces of code to make the ListBox look like a context menu. The following two event handlers make the ListBox select an appropriate item when the mouse moves over it.

// Select the ListBox item under the mouse.
private void lstItems_MouseMove(object sender, MouseEventArgs e)
{
    int index = lstItems.IndexFromPoint(e.Location);
    if (lstItems.SelectedIndex != index)
        lstItems.SelectedIndex = index;
}

// If the form isn't closing, deselect the ListBox item.
private void lstItems_MouseLeave(object sender, EventArgs e)
{
    // If the form is closing, leave the selection alone.
    if (DialogResult != DialogResult.None) return;
    if (lstItems.SelectedIndex != -1) lstItems.SelectedIndex = -1;
}

The MouseMove event handler selects the item that is under the mouse.

The MouseLeave event handler deselects all items.

If the user clicks on an item, the following code executes.

private void lstItems_Click(object sender, EventArgs e)
{
    if (lstItems.SelectedIndex < 0)
        DialogResult = DialogResult.Cancel;
    else DialogResult = DialogResult.OK;
}

This code sets the form’s DialogResult property to OK or Cancel, depending on whether the ListBox has an item selected. Setting DialogResult automatically closes the form and returns control to the program that displayed it. It also makes the call to ShowDialog used to display the form return the value of DialogResult (either OK or Cancel).

If the user presses a key while the context menu form is visible, the following code executes.

// Close the dialog if the user presses Escape or Enter.
private void ModalMenuForm_KeyDown(object sender, KeyEventArgs e)
{
    // If the user pressed Escape, return DialogResult.Cancel.
    if (e.KeyCode == Keys.Escape)
        DialogResult = DialogResult.Cancel;

    // If the user pressed Escape, return
    // DialogResult.OK if a color is selected.
    if (e.KeyCode == Keys.Enter)
    {
        if (lstItems.SelectedIndex < 0)
            DialogResult = DialogResult.Cancel;
        else DialogResult = DialogResult.OK;
    }
}

If the user presses Esc, this code sets DialogResult to Cancel to close the form.

If the user presses Enter, the code sets DialogResult to either OK or Cancel, depending on whether an item is currently selected. (If you don’t want the user to be able to close the context menu without making any selection, comment out all of the statements that set DialogResult equal to Cancel.)

The final piece of the context menu form is the following SelectedColor property.

// Return the color selected in the ListBox.
public Color SelectedColor
{
    get
    {
        if (lstItems.SelectedItem == null)
            return SystemColors.Control;
        string color_name =
            lstItems.SelectedItem.ToString().Replace(" ", "");
        return Color.FromName(color_name);
    }
}

This read-only property returns the color that is currently selected in the ListBox. The code that displays the context menu should use this property to see which color the user selected. (In a program that uses the menu to select something else, you probably wouldn’t want this property to return a color. Instead it could return the selection’s index, the selection’s text, or some other appropriate value.)

To find the color selected by the user, this code takes the name of the selected ListBox item (such as “Light Green”), removes any spaces (to get LightGreen), and uses the Color class’s FromName method to get the appropriate color.


The Main Form

The main form uses the following code to display the context menu form.

// Display the "modal context menu."
private void btnModalMenu_Click(object sender, EventArgs e)
{
    // Create and initialize the dialog.
    ModalMenuForm dlg = new ModalMenuForm();
    Point pt = new Point(btnModalMenu.Right, btnModalMenu.Bottom);
    PositionDialog(dlg, pt);

    // Display the dialog.
    if (dlg.ShowDialog() == DialogResult.OK)
    {
        this.BackColor = dlg.SelectedColor;
    }
}

This code creates a new context menu form. It then gets a Point representing the location where the modal context menu should appear. This example positions the context menu at the lower right corner of the form’s “Modal Menu” button. (See the picture.)

Next the code calls the PositionDialog method (shown shortly) to position the context menu form at that location.

The code then displays the context menu form by calling its ShowDialog method. ShowDialog displays the form modally and returns OK or Cancel depending on whether the user accepted or canceled the menu.

The following code shows the final piece to the example, the PositionDialog method.

// Position the dialog over the indicated point
// in this form's coordinates.
private void PositionDialog(Form dlg, Point location)
{
    // Translate into screen coordinates.
    Point pt = this.PointToScreen(location);
    Screen screen = Screen.FromControl(dlg);

    // Adjust if this is at an edge of the screen.
    if (pt.X < screen.WorkingArea.X)
        pt.X = screen.WorkingArea.X;
    if (pt.Y < screen.WorkingArea.Y)
        pt.Y = screen.WorkingArea.Y;
    if (pt.X > screen.WorkingArea.Right - dlg.Width)
        pt.X = screen.WorkingArea.Right - dlg.Width;
    if (pt.Y > screen.WorkingArea.Bottom - dlg.Height)
        pt.Y = screen.WorkingArea.Bottom - dlg.Height;

    // Position the dialog.
    dlg.StartPosition = FormStartPosition.Manual;
    dlg.Location = pt;
}

This method converts the indicated point form the main form’s coordinates to screen coordinates. It then gets the screen that holds the dialog. It adjusts the indicated point if necessary to keep the dialog on the screen. The method finishes by setting the dialog’s StartPosition and Location properties so it begins in the desired location when it is displayed.

The process isn’t as easy as displaying a ContextMenuStrip, but if you follow each of the steps you should be able to display a homegrown modal context menu.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in forms, menus and tagged , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *