Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allow each item to use multiple lines in a Winforms Combobox (or Listbox)

Tags:

winforms

Can this be done with relative ease?

like image 839
Riddari Avatar asked Oct 22 '10 22:10

Riddari


2 Answers

I was able to do the following in 15 minutes, so yes. The main idea is to handle the DrawItem event.

Following is my take on the problem (you can see another example, drawing icons in the items here).

public partial class Form1 : Form  
{
    public Form1()
    {
        InitializeComponent();

        this.comboBox1.DrawMode = DrawMode.OwnerDrawVariable;            
        this.comboBox1.DrawItem += new DrawItemEventHandler(comboBox1_DrawItem);
        this.comboBox1.Items.Add("Some text that needs to be take up two lines...");
        this.comboBox1.ItemHeight = 30;
    }

    IEnumerable<string> WrapString(string str, Graphics g, Font font, 
                                   int allowedWidth)
    {            
        string[] arr = str.Split(' ');            
        StringBuilder current = new StringBuilder();
        foreach (string token in arr)
        {                
            // TODO: You'll have to fix this, might break in marginal cases
            int width = 
              (int)g.MeasureString(current.ToString() + " " + token, font).Width;
            if (width > allowedWidth)
            {
                yield return current.ToString();
                current.Clear();
            }

            current.Append(token + " ");

        }
        yield return current.ToString();
    }

    void comboBox1_DrawItem(object sender, DrawItemEventArgs e)
    {
        Brush backgroundBrush, forgroundBrush;

        if (e.State == (DrawItemState.Selected | 
                    DrawItemState.NoAccelerator | DrawItemState.NoFocusRect) ||
            e.State == DrawItemState.Selected) 
        {
            forgroundBrush = Brushes.Black;
            backgroundBrush = Brushes.White;
        }
        else
        {
            forgroundBrush = Brushes.White;
            backgroundBrush = Brushes.Black;
        }

        // some way to wrap the string (on a space)           
        string str = (string)comboBox1.Items[e.Index];


        Rectangle rc = 
           new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width, e.Bounds.Height);
        e.Graphics.FillRectangle(forgroundBrush, rc);

        int stringHeight = 
               (int)e.Graphics.MeasureString(str, comboBox1.Font).Height;
        int lineNo = 0;
        foreach (string line in 
                      WrapString(str, e.Graphics, comboBox1.Font, e.Bounds.Width))
        {
            e.Graphics.DrawString(line, comboBox1.Font, backgroundBrush, 
                                  new PointF(0, lineNo * stringHeight + 5));
            lineNo++;
        }            
    }             
}

Usage: Create a regular form and drop one combobox on it.

(Note that this is of course only a naïve proof of concept - there's obviously room for improvement. Also it's just assuming that there will only be two lines rather than one. But it shows that this is possible.)

like image 89
steinar Avatar answered Nov 11 '22 20:11

steinar


I found this class made by Tim Mackey which worked out really well in my project (Tim's Blog Entry):

C# Version:

using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Collections.Generic;

namespace HortLaptopApp
{
    class ComboBoxWrap : ComboBox
    {            
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);

        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;        // x position of upper-left corner
            public int Top;         // y position of upper-left corner
            public int Right;       // x position of lower-right corner
            public int Bottom;      // y position of lower-right corner
        }

        public const int SWP_NOZORDER = 0x0004;
        public const int SWP_NOACTIVATE = 0x0010;
        public const int SWP_FRAMECHANGED = 0x0020;
        public const int SWP_NOOWNERZORDER = 0x0200;

        public const int WM_CTLCOLORLISTBOX = 0x0134;

        private int _hwndDropDown = 0;

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_CTLCOLORLISTBOX)
            {
                if (_hwndDropDown == 0)
                {
                    _hwndDropDown = m.LParam.ToInt32();

                    RECT r;
                    GetWindowRect((IntPtr)_hwndDropDown, out r);

                    //int newHeight = 0;
                   // for(int i=0; i<Items.Count && i < MaxDropDownItems; i++)
                    //    newHeight += this.GetItemHeight(i);

                    int total = 0;
                    for (int i = 0; i < this.Items.Count; i++)
                        total += this.GetItemHeight(i);
                    this.DropDownHeight = total + SystemInformation.BorderSize.Height * (this.Items.Count + 2);


                    SetWindowPos((IntPtr)_hwndDropDown, IntPtr.Zero,
                        r.Left,
                                 r.Top,
                                 DropDownWidth,
                                 DropDownHeight,
                                 SWP_FRAMECHANGED |
                                     SWP_NOACTIVATE |
                                     SWP_NOZORDER |
                                     SWP_NOOWNERZORDER);
                }
            }

            base.WndProc(ref m);
        }

        protected override void OnDropDownClosed(EventArgs e)
        {
            _hwndDropDown = 0;
            base.OnDropDownClosed(e);
        }

        public ComboBoxWrap() : base()
        {
            // add event handlers
            this.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable;
            this.DrawItem += new DrawItemEventHandler(ComboBoxWrap_DrawItem);
            this.MeasureItem += new MeasureItemEventHandler(ComboBoxWrap_MeasureItem);
        }

        void ComboBoxWrap_MeasureItem(object sender, MeasureItemEventArgs e)
        {
            // set the height of the item, using MeasureString with the font and control width
            ComboBoxWrap ddl = (ComboBoxWrap)sender;
            string text = ddl.Items[e.Index].ToString();
            SizeF size = e.Graphics.MeasureString(text, this.Font, ddl.DropDownWidth); 
            e.ItemHeight = (int)Math.Ceiling(size.Height) + 1;  // plus one for the border
            e.ItemWidth = ddl.DropDownWidth;
            System.Diagnostics.Trace.WriteLine(String.Format("Height {0}, Text {1}", e.ItemHeight, text));
        }

        void ComboBoxWrap_DrawItem(object sender, DrawItemEventArgs e)
        {
            if (e.Index < 0)
                return;

            // draw a lighter blue selected BG colour, the dark blue default has poor contrast with black text on a dark blue background
            if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
                e.Graphics.FillRectangle(Brushes.PowderBlue, e.Bounds);
            else
                e.Graphics.FillRectangle(Brushes.White, e.Bounds);

            // get the text of the item
            ComboBoxWrap ddl = (ComboBoxWrap)sender;
            string text = ddl.Items[e.Index].ToString();

            // don't dispose the brush afterwards
            Brush b = Brushes.Black;
            e.Graphics.DrawString(text, this.Font, b, e.Bounds, StringFormat.GenericDefault);

            // draw a light grey border line to separate the items
            Pen p = new Pen(Brushes.Gainsboro, 1);
            e.Graphics.DrawLine(p, new Point(e.Bounds.Left, e.Bounds.Bottom-1), new Point(e.Bounds.Right, e.Bounds.Bottom-1));
            p.Dispose();

            e.DrawFocusRectangle();
        }
    }
}

VB Version:

Imports System.Drawing
Imports System.Linq
Imports System.Windows.Forms
Imports System.Runtime.InteropServices
Imports System.Collections.Generic

Namespace HortLaptopApp
    Class ComboBoxWrap
        Inherits ComboBox           
        <DllImport("user32.dll")> _
        Public Shared Function GetWindowRect(hwnd As IntPtr, ByRef lpRect As RECT) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

        <DllImport("user32.dll", SetLastError := True)> _
        Private Shared Function SetWindowPos(hWnd As IntPtr, hWndInsertAfter As IntPtr, x As Integer, y As Integer, cx As Integer, cy As Integer, _
            uFlags As UInteger) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

        <StructLayout(LayoutKind.Sequential)> _
        Public Structure RECT
            Public Left As Integer
            ' x position of upper-left corner
            Public Top As Integer
            ' y position of upper-left corner
            Public Right As Integer
            ' x position of lower-right corner
            Public Bottom As Integer
            ' y position of lower-right corner
        End Structure

        Public Const SWP_NOZORDER As Integer = &H4
        Public Const SWP_NOACTIVATE As Integer = &H10
        Public Const SWP_FRAMECHANGED As Integer = &H20
        Public Const SWP_NOOWNERZORDER As Integer = &H200

        Public Const WM_CTLCOLORLISTBOX As Integer = &H134

        Private _hwndDropDown As Integer = 0

        Protected Overrides Sub WndProc(ByRef m As Message)
            If m.Msg = WM_CTLCOLORLISTBOX Then
                If _hwndDropDown = 0 Then
                    _hwndDropDown = m.LParam.ToInt32()

                    Dim r As RECT
                    GetWindowRect(DirectCast(_hwndDropDown, IntPtr), r)

                    'int newHeight = 0;
                    ' for(int i=0; i<Items.Count && i < MaxDropDownItems; i++)
                    '    newHeight += this.GetItemHeight(i);

                    Dim total As Integer = 0
                    For i As Integer = 0 To Me.Items.Count - 1
                        total += Me.GetItemHeight(i)
                    Next
                    Me.DropDownHeight = total + SystemInformation.BorderSize.Height * (Me.Items.Count + 2)


                    SetWindowPos(DirectCast(_hwndDropDown, IntPtr), IntPtr.Zero, r.Left, r.Top, DropDownWidth, DropDownHeight, _
                        SWP_FRAMECHANGED Or SWP_NOACTIVATE Or SWP_NOZORDER Or SWP_NOOWNERZORDER)
                End If
            End If

            MyBase.WndProc(m)
        End Sub

        Protected Overrides Sub OnDropDownClosed(e As EventArgs)
            _hwndDropDown = 0
            MyBase.OnDropDownClosed(e)
        End Sub

        Public Sub New()
            MyBase.New()
            ' add event handlers
            Me.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable
            Me.DrawItem += New DrawItemEventHandler(AddressOf ComboBoxWrap_DrawItem)
            Me.MeasureItem += New MeasureItemEventHandler(AddressOf ComboBoxWrap_MeasureItem)
        End Sub

        Private Sub ComboBoxWrap_MeasureItem(sender As Object, e As MeasureItemEventArgs)
            ' set the height of the item, using MeasureString with the font and control width
            Dim ddl As ComboBoxWrap = DirectCast(sender, ComboBoxWrap)
            Dim text As String = ddl.Items(e.Index).ToString()
            Dim size As SizeF = e.Graphics.MeasureString(text, Me.Font, ddl.DropDownWidth)
            e.ItemHeight = CInt(Math.Ceiling(size.Height)) + 1
            ' plus one for the border
            e.ItemWidth = ddl.DropDownWidth
            System.Diagnostics.Trace.WriteLine([String].Format("Height {0}, Text {1}", e.ItemHeight, text))
        End Sub

        Private Sub ComboBoxWrap_DrawItem(sender As Object, e As DrawItemEventArgs)
            If e.Index < 0 Then
                Return
            End If

            ' draw a lighter blue selected BG colour, the dark blue default has poor contrast with black text on a dark blue background
            If (e.State And DrawItemState.Selected) = DrawItemState.Selected Then
                e.Graphics.FillRectangle(Brushes.PowderBlue, e.Bounds)
            Else
                e.Graphics.FillRectangle(Brushes.White, e.Bounds)
            End If

            ' get the text of the item
            Dim ddl As ComboBoxWrap = DirectCast(sender, ComboBoxWrap)
            Dim text As String = ddl.Items(e.Index).ToString()

            ' don't dispose the brush afterwards
            Dim b As Brush = Brushes.Black
            e.Graphics.DrawString(text, Me.Font, b, e.Bounds, StringFormat.GenericDefault)

            ' draw a light grey border line to separate the items
            Dim p As New Pen(Brushes.Gainsboro, 1)
            e.Graphics.DrawLine(p, New Point(e.Bounds.Left, e.Bounds.Bottom - 1), New Point(e.Bounds.Right, e.Bounds.Bottom - 1))
            p.Dispose()

            e.DrawFocusRectangle()
        End Sub
    End Class
End Namespace
like image 3
DDA Avatar answered Nov 11 '22 18:11

DDA