Search code examples
c#.netwinformscontextmenustrip

ToolStripLayoutStyle.Table not working in ContextMenuStrip


I want to shown a context menu where the menu items are images that are laid out in a grid. However, when I set LayoutStyle to ToolStripLayoutStyle.Table in the ToolStripDropDown of a menu, it will only give a grid layout of the menu items if a new ToolStripDropDown object is created.

My problem is that I can create and assign a new ToolStripDropDown for a sub-menu, but not for ContextMenuStrip, because it is the ToolStripDropDown.

The following code demonstrates the problem. It will display a context menu that contains colours swatch images and also has two sub-menus with the same images. All three menus have the LayoutStyle property set to ToolStripLayoutStyle.Table, but only one will actually show as a grid.

private void FillDropDown(ToolStripDropDown drop_down)
{
    // Set the drop down to a 2 column table layout
    drop_down.LayoutStyle = ToolStripLayoutStyle.Table;
    TableLayoutSettings table_layout_settings = (TableLayoutSettings)drop_down.LayoutSettings;
    table_layout_settings.ColumnCount = 2;

    // Fill the menu with some colour swatches
    Color[] colours = { Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Blue, Color.Purple };
    foreach (Color colour in colours) {
        ToolStripMenuItem item = new ToolStripMenuItem();
        Bitmap swatch = new Bitmap(64, 64);
        using (Graphics g = Graphics.FromImage(swatch))
        using (SolidBrush b = new SolidBrush(colour)) {
            g.FillRectangle(b, 0, 0, 64, 64);
        }
        item.Image = swatch;
        item.DisplayStyle = ToolStripItemDisplayStyle.Image;
        item.Margin = new Padding(2, 2, 2, 2);
        drop_down.Items.Add(item);
    }
}

private void ShowColorMenu(Point screen_location)
{
    ContextMenuStrip context_menu = new ContextMenuStrip();

    // The root context menu will not layout as a grid
    FillDropDown(context_menu);

    // This sub-menu will not layout as a grid
    ToolStripMenuItem sub_menu = new ToolStripMenuItem("Sub-menu");
    FillDropDown(sub_menu.DropDown);
    context_menu.Items.Add(sub_menu);

    // A sub-menu will layout as a grid if we create a new ToolStripDropDown for it
    ToolStripMenuItem grid_sub_menu = new ToolStripMenuItem("Grid Sub-menu");
    ToolStripDropDown new_drop_down = new ToolStripDropDown();
    FillDropDown(new_drop_down);
    grid_sub_menu.DropDown = new_drop_down;
    context_menu.Items.Add(grid_sub_menu);

    context_menu.Show(screen_location);
}

On my machine the result appears as follows:

Context menu showing grid layout in sub-menu and linear layout in root menu

I would like to have a grid of images in the root of the context menu. It would also be nice to understand why it is behaving like this. I have looked through the .NET reference source, but that didn't help on this occasion.


Solution

  • The ContextMenuStrip cannot show its ToolStripMenuItems in a Table layout, because of a limitation in Menu presentation/layout and you also cannot cast ContextMenuStrip to ToolStripDropDown (or the other way around; it's a wrapper around ToolStripDropDownMenu, it cannot be transformed into its indirect ancestor: it will retain its specific functionality (e.g., you can see both a TextBox and a ListBox as Control, but it doesn't mean that, now, setting the Text of a ListBox will actually show a text somewhere, just because the Text property belongs to the Control class).

    But you can directly use and show a ToolStripDropDown the same way as a ContextMenuStrip. The ToolStripDropDown's LayoutSettings can be cast directly to TableLayoutSettings and a LayoutStyle of type ToolStripLayoutStyle.Table is fully supported.


    In the example, a ToolStripDropDown object, containing ToolStripMenuItems arranged in a Table layout, is used as a ContextMenuStrip to select a colored Image to be applied to a PictureBox Control, while the Color name is shown in a Label control.

    ToolStripDropDown as ContextMenuStrip

    The dropdown menu is created when the Form is initialized, it's shown clicking the Mouse right Button inside the Form's ClientArea and disposed of when the Form closes:

    Note1: here, I'm using a Lambda to subscribe to the ToolStripDropDown.ItemClicked event. It's however preferable, with this type of control, to use a method delegate instead.

    Note2: the ToolStripDropDown is disposed of calling contextColorMenu.Dispose();. If the container Form is opened and closed frequently, it may be better to explicitly dispose of the ToolStripMenuItems Images.

    using System.Drawing;
    using System.Windows.Forms;
    
    public partial class SomeForm : Form
    {
        private ToolStripDropDown contextColorMenu = null;
    
        public SomeForm()
        {
            InitializeComponent();
            contextColorMenu = new ToolStripDropDown();
            contextColorMenu.ItemClicked += (o, a) => {
                // Assign the selected Bitmap to a PitureBox.Image
                picColor.Image = a.ClickedItem.Image;
                // Show the Color description in a Label
                lblColor.Text = ((Color)a.ClickedItem.Tag).ToString();
            };
            FillDropDown(contextColorMenu);
        }
        
        private void ShowColorMenu(Point location) => contextColorMenu.Show(location);
    
        private void FillDropDown(ToolStripDropDown dropDown)
        {
            dropDown.LayoutStyle = ToolStripLayoutStyle.Table;
            (dropDown.LayoutSettings as TableLayoutSettings).ColumnCount = 2;
            (dropDown.LayoutSettings as TableLayoutSettings).GrowStyle = TableLayoutPanelGrowStyle.AddRows;
    
            Color[] colors = { Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Blue, Color.Purple };
    
            foreach (Color color in colors) {
                var swatch = new Bitmap(64, 64);
                using (var g = Graphics.FromImage(swatch)) {
                    g.Clear(color);
                }
                var item = new ToolStripMenuItem() {
                    DisplayStyle = ToolStripItemDisplayStyle.Image,
                    Image = swatch,
                    Tag = color,
                    Margin = new Padding(2),
                    Padding = new Padding(2, 1, 2, 1)  // Fine tune the Items' Cell border
                };
                dropDown.Items.Add(item);
            }
        }
    
        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);
            if (e.Button == MouseButtons.Right) {
                ShowColorMenu(MousePosition);
            }
        }
    
        protected override void OnFormClosed(FormClosedEventArgs e)
        {
            base.OnFormClosed(e);
            contextColorMenu?.Dispose();
        }
    }