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:
- Not Painting upon Load
- String is Drawn with "Weird Characters"
- When Selecting an Item; I'm drawing the "Marked" Item.
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 onm ouseDoubleClick(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:
- I've Commented the Pinvoke LB_ Enums and WndProc as I was not able to make it work.
- In order to keep simplicity: I removed the ternary operators (however I loved the way Jimi's code would alternate the colors with them).
- SetMarker() was reverted to previous version. The Marked Item was never Drawn that way.
- Custom Properties were not providing their value to the Brushes; therefore they were temporarily removed.
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 onm ouseDoubleClick(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
CodePudding user response:
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.
- TextRenderer.DrawText() replaces
Graphics.DrawString()
: this will give a more natural aspect to the rendered list items. No need to use anti-aliasing. - OnMeasureItem is also overridden, to provide a custom Height (optionally the Width - when strictly required) for the Items. It's set to
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 interceptLB_RESETCONTENT
(sent when theObjectCollection
is cleared) andLB_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).- When setting a Property value, always verify that the new value is not equal to the old one, before you call
Invalidate()
(or any other method - or a Property setter) for no reason. - A few minor changes, see the code.
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
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 onm ouseDoubleClick(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) {
SetMarkedItem();
}
base.OnMouseDoubleClick(e);
}
private void SetMarkedItem() => MarkedIndex = markedIndex == SelectedIndex ? -1 : SelectedIndex;
}