Allow each item to use multiple lines in a Winforms Combobox (or Listbox)
Can this be done with relativ开发者_JAVA百科e ease?
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.)
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
This question is kind of old, but I found that the DrawString
with a RectangleF
parameter not only clips text to that rectangle, but also wraps it.
Sample code:
StringFormat sf = StringFormat.GenericTypographic;
sf.Trimming = StringTrimming.EllipsisCharacter;
g.DrawString(text, font, foregroundBrush, e.Bounds, sf);
See also MS Docs: How to: Display Side-Aligned Tabs with TabControl
I enhanced Steinar's answer, which gave me a wonderful combobox that could hold variable multilined texts, including a tooltip for each item in the list.
Here is what to do:
Setup DrawItem, MeasureItem events for the combobox as follows. Also use the Wrapstring() method to yield each chunk of the multiline to compute line count and height of each line:
// Goes in Form load / constructor
cmbMulti.DrawMode = DrawMode.OwnerDrawVariable;
cmbMulti.DrawItem += new DrawItemEventHandler(cmbMulti_DrawItem);
cmbMulti.MeasureItem += new MeasureItemEventHandler(cmbMulti_MeasureItem);
cmbMulti.Items.Add("Multiline Text 1");
cmbMulti.Items.Add("Multiline Text 2");
cmbMulti.Items.Add("Multiline Text ");
ToolTip tooltip = new ToolTip();
// Set dropdown height
int totalLines = 0, stringHeight = 0;
foreach (string item in cmbMulti.Items)
{
foreach (string str in WrapString(item, cmbMulti.CreateGraphics(), cmbMulti.Font, cmbMulti.Width))
{
totalLines++;
if (totalLines == 1)
{
stringHeight = (int)cmbMulti.CreateGraphics().MeasureString(item, cmbMulti.Font).Height;
}
}
}
cmbMulti.DropDownHeight = TotalDropdownHeight + 2;
private void cmbMulti_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index > -1)
{
string itemText = cmbMulti.Items[e.Index].ToString();
Brush backgroundBrush, forgroundBrush;
if (e.State == (DrawItemState.Selected | DrawItemState.NoAccelerator | DrawItemState.NoFocusRect) ||
e.State == DrawItemState.Selected)
{
forgroundBrush = new SolidBrush(SystemColors.Highlight);
backgroundBrush = new SolidBrush(SystemColors.Window);
this.tooltip.Show(itemText, cmbMulti, e.Bounds.Right, e.Bounds.Bottom);
}
else
{
forgroundBrush = new SolidBrush(SystemColors.Window);
backgroundBrush = Brushes.Black;
this.tooltip.Hide(cmbMulti);
}
if ((e.State & DrawItemState.Focus) == 0)
{
e.Graphics.FillRectangle(forgroundBrush, e.Bounds);
e.Graphics.DrawString(itemText, Font, backgroundBrush,
e.Bounds);
e.Graphics.DrawRectangle(new Pen(SystemColors.Highlight), e.Bounds);
}
else
{
e.Graphics.FillRectangle(forgroundBrush, e.Bounds);
e.Graphics.DrawString(itemText, Font, backgroundBrush,
e.Bounds);
}
}
}
int TotalDropdownHeight = 0;
protected void cmbMulti_MeasureItem(object sender, MeasureItemEventArgs e)
{
if (e.Index > -1)
{
string itemText = cmbMulti.Items[e.Index].ToString();
SizeF sf = e.Graphics.MeasureString(itemText, Font, cmbMulti.Width);
int multiLineCount = 0;
foreach (string item in WrapString(itemText, e.Graphics, cmbMulti.Font, cmbMulti.Width))
{
multiLineCount++;
}
e.ItemHeight = multiLineCount * 15;
TotalDropdownHeight += e.ItemHeight;
e.ItemWidth = cmbMulti.Width;
}
}
IEnumerable<string> WrapString(string str, Graphics g, Font font,
int allowedWidth)
{
string[] arr = str.Split(' ');
StringBuilder current = new StringBuilder();
foreach (string token in arr)
{
int width =
(int)g.MeasureString(current.ToString() + " " + token, font).Width;
if (width > allowedWidth)
{
yield return current.ToString();
current = new StringBuilder();
}
current.Append(token + " ");
}
yield return current.ToString();
}
精彩评论