Reputation: 17
I'm struggling in creating customized functionality for a listbox. I need my ListBox to change the Forecolor of a specific ("marked") item. Not to be confused with Selected Item.
The Functionality Required:
Let's say the ListBox Collection Contains Several File Names.
When I double click an Item; that item index and object are stored into two variables (index and object).
These variables would then be used to set the Item ForeColor (when Listbox Item is Unselected).
Remaining Items and Item Rectangle should be drawn with default properties
(in this case they have their own color properties to allow further customization).
My problems:
I'm really confused. MSDN documentation is not very clear how to achieve this; nor how & when the DrawItem Event Occurs.
I've been messing with several alternatives; however deleted everything and got back to the current code attempting to understand the logic and behaviour.
1st Attempt code (Original Question Code):
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Custom_Controls.Controls
{
internal class MyPlaylist : ListBox
{
public MyPlaylist()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
// Enable ListBox Customized Design
DrawMode = DrawMode.OwnerDrawVariable;
//SelectionMode = SelectionMode.MultiExtended;
}
#region <Custom Properties>
private int markedIndex = -1;
public int MarkedIndex
{
get { return markedIndex; }
set { markedIndex = value; Invalidate(); }
}
private object markedItem = string.Empty;
public object MarkedItem
{
get { return markedItem; }
set
{
markedItem = value;
Invalidate();
}
}
private Color markedItemForeColor = Color.Red;
public Color MarkedItemForeColor
{
get { return markedItemForeColor; }
set { markedItemForeColor = value; Invalidate(); }
}
private Color markedItemBackColor = Color.DimGray;
public Color MarkedItemBackColor
{
get { return markedItemBackColor; }
set { markedItemBackColor = value; Invalidate(); }
}
private Color selectionBackColor = Color.DeepSkyBlue;
public Color SelectionBackColor
{
get { return selectionBackColor; }
set { selectionBackColor = value; Invalidate(); }
}
private Color selectionForeColor = Color.White;
public Color SelectionForeColor
{
get { return selectionForeColor; }
set { selectionForeColor = value; Invalidate(); }
}
#endregion
protected override void OnDrawItem(DrawItemEventArgs e) // When Selected?
{
e.DrawBackground();
e.DrawFocusRectangle();
//// Improve Graphic Quality and Pixel Precision
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
using (var defaultForeBrush = new SolidBrush(Color.White))
using (var markForeBrush = new SolidBrush(markedItemForeColor))
{
// Iterate over all the items
for (int i = 0; i < Items.Count; i++)
{
var item = Items[i];
// Draw "Marked" Item
if (i == markedIndex)
{
e.Graphics.DrawString(Items[markedIndex].ToString(), e.Font, markForeBrush, e.Bounds, StringFormat.GenericDefault);
}
// Draw Remaining Items
else
{
e.Graphics.DrawString(item.ToString(), e.Font, markForeBrush, e.Bounds, StringFormat.GenericDefault);
}
// Draw Selection Rectangle
// ...
}
}
}
#region <Overriden Events>
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
}
protected override void OnDoubleClick(EventArgs e)
{
base.OnDoubleClick(e);
SetMarkedItem();
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
base.OnMouseDoubleClick(e);
SetMarkedItem();
}
#region <Methods>
private void SetMarkedItem()
{
markedIndex = SelectedIndex;
markedItem = SelectedItem;
}
#endregion
}
}
My 2nd Attempt using Jimi's Help (Current Code)
Changes:
Current Issues: WndProc definitly needs to be reimplemented to clear the Drawing (Marked Item); and perhaps to Redraw the Control so it Updates the Marker ASAP.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Custom_Controls.Controls
{
internal class MyPlaylist : ListBox
{
#region <Constructor>
public MyPlaylist()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
DrawMode = DrawMode.OwnerDrawVariable;
BackColor = Color.FromArgb(255, 25, 25, 25);
ForeColor = Color.White;
BorderStyle = BorderStyle.FixedSingle;
}
#endregion
#region <Fields>
//private const int LB_RESETCONTENT = 0x0184;
//private const int LB_DELETESTRING = 0x0182;
//TextFormatFlags flags = TextFormatFlags.PreserveGraphicsClipping | TextFormatFlags.LeftAndRightPadding | TextFormatFlags.VerticalCenter;
#endregion
#region <Custom Properties>
// Tip: Always Verify that the new Values are Different from the Old Ones
private int markedIndex = -1;
public int MarkedIndex
{
get { return markedIndex; }
set
{
if (value != markedIndex)
{
markedIndex = value;
Invalidate();
}
}
}
// Read-only: just return the marked Item, set it using the Index only
public object MarkedItem
{
get { return Items[markedIndex]; }
}
#endregion
#region <Overriden Events>
protected override void OnDrawItem(DrawItemEventArgs e)
{
if (Items.Count == 0) return;
// Draw Selection:
if (e.State.HasFlag(DrawItemState.Focus) || e.State.HasFlag(DrawItemState.Selected))
{
using (var brush = new SolidBrush(Color.FromArgb(255, 52, 52, 52)))
{
// Background Rectangle
e.Graphics.FillRectangle(brush, e.Bounds);
// Item Text : Marked Item
if (e.Index == markedIndex)
{
TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, Color.Red, flags);
}
// Other Items (Except Marked)
else
{
TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, Color.White, flags);
}
}
}
// Draw Unselected:
else
{
using (var brush = new SolidBrush(BackColor))
using (var markedBrush = new SolidBrush(Color.Khaki))
{
e.Graphics.FillRectangle(brush, e.Bounds);
TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, Color.White, flags);
}
// Draw (Unselected) Marked Item
if (markedIndex > -1 && e.Index == markedIndex)
{
TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, Color.Red, flags);
}
}
e.DrawFocusRectangle();
base.OnDrawItem(e);
}
// Set the Height of the Item (Width: only if needed).
// This is the Standard Value (Modify as required)
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
if (Items.Count > 0)
{
e.ItemHeight = Font.Height + 4; // 4 = Text vs Item Rectangle Margin
}
base.OnMeasureItem(e);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
}
protected override void OnDoubleClick(EventArgs e)
{
base.OnDoubleClick(e);
SetMarkedItem();
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
base.OnMouseDoubleClick(e);
if (e.Button == MouseButtons.Left)
{
SetMarkedItem();
}
}
#endregion
#region <Methods>
/// <summary>
/// WndProc is Overridden in order to Intercept the LB_RESETCONTENT (sent when the ObjectCollection is cleared);<br/>
/// and the LB_DELETESTRING (sent when an Item is removed).
/// This is done to reset the marked Item when the list is cleared or the marked Item is removed (otherwise an Item will remain marked when it shouldn't).
/// </summary>
/// <param name="m"></param>
//protected override void WndProc(ref Message m)
//{
// switch (m.Msg)
// {
// // List Cleared
// case LB_RESETCONTENT:
// markedIndex = -1;
// break;
// // Item Deleted
// case LB_DELETESTRING:
// if (markedIndex == m.WParam.ToInt32())
// {
// markedIndex = -1;
// }
// break;
// }
//}
private void SetMarkedItem() // Current Block
{
markedIndex = SelectedIndex;
}
// Previous Code Line (By Jimmy; using Ternary Operator) <----------------------------------------
//private void SetMarkedItem() => MarkedIndex = markedIndex == SelectedIndex ? -1 : SelectedIndex;
#endregion
}
}
Helpful Related Content
How to add multiline Text to a ListBox item
Upvotes: 1
Views: 653
Reputation: 32278
This sample class contains the adjustments needed to make the List work as the standard ListBox, but with the enhancements described in the question.
See also the comments in code.
Graphics.DrawString()
: this will give a more natural aspect to the rendered list items. No need to use anti-aliasing.ListBox.Font.Height + 4
(pretty standard); modify as needed.OnDrawItem()
is corrected to handle both the custom Selection colors and the marked Item's colors. Note that this method is called once per Item, so you don't have to loop the entire collection each time, just paint the current Item with correct colors.SetMarkedItem()
is modified to toggle the state of a marked Item, in case you double-click it twice.WndProc
is overridden to intercept LB_RESETCONTENT
(sent when the ObjectCollection
is cleared) and LB_DELETESTRING
(sent when an Item is removed). This is done to reset the marked Item when the list is cleared or the marked Item is removed (otherwise an Item will remain marked when it shouldn't).Invalidate()
(or any other method - or a Property setter) for no reason.public class MyPlaylist : ListBox {
private const int LB_DELETESTRING = 0x0182;
private const int LB_RESETCONTENT = 0x0184;
TextFormatFlags flags = TextFormatFlags.PreserveGraphicsClipping |
TextFormatFlags.LeftAndRightPadding |
TextFormatFlags.VerticalCenter;
public MyPlaylist()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
DrawMode = DrawMode.OwnerDrawVariable;
}
protected override void WndProc(ref Message m)
{
switch (m.Msg) {
// List cleared
case LB_RESETCONTENT:
markedIndex = -1;
break;
// Item deleted
case LB_DELETESTRING:
if (markedIndex == m.WParam.ToInt32()) {
markedIndex = -1;
}
break;
}
base.WndProc(ref m);
}
private int markedIndex = -1;
public int MarkedIndex {
get => markedIndex;
set {
if (value != markedIndex) {
markedIndex = value;
Invalidate();
}
}
}
// Read-only: just return the marked Item, set it using the Index only
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public object MarkedItem {
get => Items[markedIndex];
}
// Always verify that the new value is different from the old one
private Color markedItemForeColor = Color.Orange;
public Color MarkedItemForeColor {
get => markedItemForeColor;
set {
if (value != markedItemForeColor) {
markedItemForeColor = value;
Invalidate();
}
}
}
private Color markedItemBackColor = Color.DimGray;
public Color MarkedItemBackColor {
get => markedItemBackColor;
set {
if (value != markedItemBackColor) {
markedItemBackColor = value;
Invalidate();
}
}
}
private Color selectionBackColor = Color.DeepSkyBlue;
public Color SelectionBackColor {
get => selectionBackColor;
set {
if (value != selectionBackColor) {
selectionBackColor = value;
Invalidate();
}
}
}
private Color selectionForeColor = Color.White;
public Color SelectionForeColor {
get => selectionForeColor;
set {
if (value != selectionForeColor) {
selectionForeColor = value;
Invalidate();
}
}
}
// Use TextRenderer to draw the Items - no anti-aliasing needed
protected override void OnDrawItem(DrawItemEventArgs e)
{
if (Items.Count == 0) return;
if (e.State.HasFlag(DrawItemState.Focus) || e.State.HasFlag(DrawItemState.Selected)) {
using (var brush = new SolidBrush(selectionBackColor)) {
e.Graphics.FillRectangle(brush, e.Bounds);
}
TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, selectionForeColor, flags);
}
else {
var color = markedIndex != -1 && markedIndex == e.Index ? markedItemBackColor : BackColor;
using (var brush = new SolidBrush(color)) {
e.Graphics.FillRectangle(brush, e.Bounds);
}
var foreColor = markedIndex == e.Index ? markedItemForeColor : ForeColor;
TextRenderer.DrawText(e.Graphics, GetItemText(Items[e.Index]), Font, e.Bounds, foreColor, flags);
}
e.DrawFocusRectangle();
base.OnDrawItem(e);
}
// Set the Height (the Width only if needed) of the Item
// This is the standard value, modify as required
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
if (Items.Count > 0) {
e.ItemHeight = Font.Height + 4;
}
base.OnMeasureItem(e);
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) {
SetMarkedItem();
}
base.OnMouseDoubleClick(e);
}
private void SetMarkedItem() => MarkedIndex = markedIndex == SelectedIndex ? -1 : SelectedIndex;
}
Upvotes: 1