Files
ObjectListView/Rendering/Renderers.cs
2017-02-04 13:47:58 +01:00

3414 lines
135 KiB
C#

/*
* Renderers - A collection of useful renderers that are used to owner draw a cell in an ObjectListView
*
* Author: Phillip Piper
* Date: 27/09/2008 9:15 AM
*
* Change log:
* v2.8
* 2014-09-26 JPP - Dispose of animation timer in a more robust fashion.
* 2014-05-20 JPP - Handle rendering disabled rows
* v2.7
* 2013-04-29 JPP - Fixed bug where Images were not vertically aligned
* v2.6
* 2012-10-26 JPP - Hit detection will no longer report check box hits on columns without checkboxes.
* 2012-07-13 JPP - [Breaking change] Added preferedSize parameter to IRenderer.GetEditRectangle().
* v2.5.1
* 2012-07-14 JPP - Added CellPadding to various places. Replaced DescribedTaskRenderer.CellPadding.
* 2012-07-11 JPP - Added CellVerticalAlignment to various places allow cell contents to be vertically
* aligned (rather than always being centered).
* v2.5
* 2010-08-24 JPP - CheckBoxRenderer handles hot boxes and correctly vertically centers the box.
* 2010-06-23 JPP - Major rework of HighlightTextRenderer. Now uses TextMatchFilter directly.
* Draw highlighting underneath text to improve legibility. Works with new
* TextMatchFilter capabilities.
* v2.4
* 2009-10-30 JPP - Plugged possible resource leak by using using() with CreateGraphics()
* v2.3
* 2009-09-28 JPP - Added DescribedTaskRenderer
* 2009-09-01 JPP - Correctly handle an ImageRenderer's handling of an aspect that holds
* the image to be displayed at Byte[].
* 2009-08-29 JPP - Fixed bug where some of a cell's background was not erased.
* 2009-08-15 JPP - Correctly MeasureText() using the appropriate graphic context
* - Handle translucent selection setting
* v2.2.1
* 2009-07-24 JPP - Try to honour CanWrap setting when GDI rendering text.
* 2009-07-11 JPP - Correctly calculate edit rectangle for subitems of a tree view
* (previously subitems were indented in the same way as the primary column)
* v2.2
* 2009-06-06 JPP - Tweaked text rendering so that column 0 isn't ellipsed unnecessarily.
* 2009-05-05 JPP - Added Unfocused foreground and background colors
* (thanks to Christophe Hosten)
* 2009-04-21 JPP - Fixed off-by-1 error when calculating text widths. This caused
* middle and right aligned columns to always wrap one character
* when printed using ListViewPrinter (SF#2776634).
* 2009-04-11 JPP - Correctly renderer checkboxes when RowHeight is non-standard
* 2009-04-06 JPP - Allow for item indent when calculating edit rectangle
* v2.1
* 2009-02-24 JPP - Work properly with ListViewPrinter again
* 2009-01-26 JPP - AUSTRALIA DAY (why aren't I on holidays!)
* - Major overhaul of renderers. Now uses IRenderer interface.
* - ImagesRenderer and FlagsRenderer<T> are now defunct.
* The names are retained for backward compatibility.
* 2009-01-23 JPP - Align bitmap AND text according to column alignment (previously
* only text was aligned and bitmap was always to the left).
* 2009-01-21 JPP - Changed to use TextRenderer rather than native GDI routines.
* 2009-01-20 JPP - Draw images directly from image list if possible. 30% faster!
* - Tweaked some spacings to look more like native ListView
* - Text highlight for non FullRowSelect is now the right color
* when the control doesn't have focus.
* - Commented out experimental animations. Still needs work.
* 2009-01-19 JPP - Changed to draw text using GDI routines. Looks more like
* native control this way. Set UseGdiTextRendering to false to
* revert to previous behavior.
* 2009-01-15 JPP - Draw background correctly when control is disabled
* - Render checkboxes using CheckBoxRenderer
* v2.0.1
* 2008-12-29 JPP - Render text correctly when HideSelection is true.
* 2008-12-26 JPP - BaseRenderer now works correctly in all Views
* 2008-12-23 JPP - Fixed two small bugs in BarRenderer
* v2.0
* 2008-10-26 JPP - Don't owner draw when in Design mode
* 2008-09-27 JPP - Separated from ObjectListView.cs
*
* Copyright (C) 2006-2014 Phillip Piper
*
* TO DO:
* - Hit detection on renderers doesn't change the controls standard selection behavior
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;
using Timer = System.Threading.Timer;
namespace BrightIdeasSoftware
{
/// <summary>
/// Renderers are the mechanism used for owner drawing cells. As such, they can also handle
/// hit detection and positioning of cell editing rectangles.
/// </summary>
public interface IRenderer
{
/// <summary>
/// Render the whole item within an ObjectListView. This is only used in non-Details views.
/// </summary>
/// <param name="e">The event</param>
/// <param name="g">A Graphics for rendering</param>
/// <param name="itemBounds">The bounds of the item</param>
/// <param name="rowObject">The model object to be drawn</param>
/// <returns>Return true to indicate that the event was handled and no further processing is needed.</returns>
bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, Object rowObject);
/// <summary>
/// Render one cell within an ObjectListView when it is in Details mode.
/// </summary>
/// <param name="e">The event</param>
/// <param name="g">A Graphics for rendering</param>
/// <param name="cellBounds">The bounds of the cell</param>
/// <param name="rowObject">The model object to be drawn</param>
/// <returns>Return true to indicate that the event was handled and no further processing is needed.</returns>
bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, Object rowObject);
/// <summary>
/// What is under the given point?
/// </summary>
/// <param name="hti"></param>
/// <param name="x">x co-ordinate</param>
/// <param name="y">y co-ordinate</param>
/// <remarks>This method should only alter HitTestLocation and/or UserData.</remarks>
void HitTest(OlvListViewHitTestInfo hti, int x, int y);
/// <summary>
/// When the value in the given cell is to be edited, where should the edit rectangle be placed?
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="item"></param>
/// <param name="subItemIndex"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize);
}
/// <summary>
/// An AbstractRenderer is a do-nothing implementation of the IRenderer interface.
/// </summary>
[Browsable(true),
ToolboxItem(false)]
public class AbstractRenderer : Component, IRenderer
{
#region IRenderer Members
/// <summary>
/// Render the whole item within an ObjectListView. This is only used in non-Details views.
/// </summary>
/// <param name="e">The event</param>
/// <param name="g">A Graphics for rendering</param>
/// <param name="itemBounds">The bounds of the item</param>
/// <param name="rowObject">The model object to be drawn</param>
/// <returns>Return true to indicate that the event was handled and no further processing is needed.</returns>
public virtual bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, object rowObject) {
return true;
}
/// <summary>
/// Render one cell within an ObjectListView when it is in Details mode.
/// </summary>
/// <param name="e">The event</param>
/// <param name="g">A Graphics for rendering</param>
/// <param name="cellBounds">The bounds of the cell</param>
/// <param name="rowObject">The model object to be drawn</param>
/// <returns>Return true to indicate that the event was handled and no further processing is needed.</returns>
public virtual bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) {
return false;
}
/// <summary>
/// What is under the given point?
/// </summary>
/// <param name="hti"></param>
/// <param name="x">x co-ordinate</param>
/// <param name="y">y co-ordinate</param>
/// <remarks>This method should only alter HitTestLocation and/or UserData.</remarks>
public virtual void HitTest(OlvListViewHitTestInfo hti, int x, int y) {
}
/// <summary>
/// When the value in the given cell is to be edited, where should the edit rectangle be placed?
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="item"></param>
/// <param name="subItemIndex"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
public virtual Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) {
return cellBounds;
}
#endregion
}
/// <summary>
/// This class provides compatibility for v1 RendererDelegates
/// </summary>
[ToolboxItem(false)]
internal class Version1Renderer : AbstractRenderer
{
public Version1Renderer(RenderDelegate renderDelegate) {
this.RenderDelegate = renderDelegate;
}
/// <summary>
/// The renderer delegate that this renderer wraps
/// </summary>
public RenderDelegate RenderDelegate;
#region IRenderer Members
public override bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) {
if (this.RenderDelegate == null)
return base.RenderSubItem(e, g, cellBounds, rowObject);
else
return this.RenderDelegate(e, g, cellBounds, rowObject);
}
#endregion
}
/// <summary>
/// A BaseRenderer provides useful base level functionality for any custom renderer.
/// </summary>
/// <remarks>
/// <para>Subclasses will normally override the Render or OptionalRender method, and use the other
/// methods as helper functions.</para>
/// </remarks>
[Browsable(true),
ToolboxItem(true)]
public class BaseRenderer : AbstractRenderer
{
#region Configuration Properties
/// <summary>
/// Can the renderer wrap lines that do not fit completely within the cell?
/// </summary>
/// <remarks>Wrapping text doesn't work with the GDI renderer.</remarks>
[Category("Appearance"),
Description("Can the renderer wrap text that does not fit completely within the cell"),
DefaultValue(false)]
public bool CanWrap {
get { return canWrap; }
set {
canWrap = value;
if (canWrap)
this.UseGdiTextRendering = false;
}
}
private bool canWrap;
/// <summary>
/// Gets or sets how many pixels will be left blank around this cell
/// </summary>
/// <remarks>
/// <para>
/// This setting only takes effect when the control is owner drawn.
/// </para>
/// <para><see cref="ObjectListView.CellPadding"/> for more details.</para>
/// </remarks>
[Category("ObjectListView"),
Description("The number of pixels that renderer will leave empty around the edge of the cell"),
DefaultValue(null)]
public Rectangle? CellPadding {
get { return this.cellPadding; }
set { this.cellPadding = value; }
}
private Rectangle? cellPadding;
/// <summary>
/// Gets or sets how cells drawn by this renderer will be vertically aligned.
/// </summary>
/// <remarks>
/// <para>
/// If this is not set, the value from the column or control itself will be used.
/// </para>
/// </remarks>
[Category("ObjectListView"),
Description("How will cell values be vertically aligned?"),
DefaultValue(null)]
public virtual StringAlignment? CellVerticalAlignment {
get { return this.cellVerticalAlignment; }
set { this.cellVerticalAlignment = value; }
}
private StringAlignment? cellVerticalAlignment;
/// <summary>
/// Gets the optional padding that this renderer should apply before drawing.
/// This property considers all possible sources of padding
/// </summary>
[Browsable(false)]
protected virtual Rectangle? EffectiveCellPadding {
get {
if (this.cellPadding.HasValue)
return this.cellPadding.Value;
if (this.OLVSubItem != null && this.OLVSubItem.CellPadding.HasValue)
return this.OLVSubItem.CellPadding.Value;
if (this.ListItem != null && this.ListItem.CellPadding.HasValue)
return this.ListItem.CellPadding.Value;
if (this.Column != null && this.Column.CellPadding.HasValue)
return this.Column.CellPadding.Value;
if (this.ListView != null && this.ListView.CellPadding.HasValue)
return this.ListView.CellPadding.Value;
return null;
}
}
/// <summary>
/// Gets the vertical cell alignment that should govern the rendering.
/// This property considers all possible sources.
/// </summary>
[Browsable(false)]
protected virtual StringAlignment EffectiveCellVerticalAlignment {
get {
if (this.cellVerticalAlignment.HasValue)
return this.cellVerticalAlignment.Value;
if (this.OLVSubItem != null && this.OLVSubItem.CellVerticalAlignment.HasValue)
return this.OLVSubItem.CellVerticalAlignment.Value;
if (this.ListItem != null && this.ListItem.CellVerticalAlignment.HasValue)
return this.ListItem.CellVerticalAlignment.Value;
if (this.Column != null && this.Column.CellVerticalAlignment.HasValue)
return this.Column.CellVerticalAlignment.Value;
if (this.ListView != null)
return this.ListView.CellVerticalAlignment;
return StringAlignment.Center;
}
}
/// <summary>
/// Gets or sets the image list from which keyed images will be fetched
/// </summary>
[Category("Appearance"),
Description("The image list from which keyed images will be fetched for drawing."),
DefaultValue(null)]
public ImageList ImageList {
get { return imageList; }
set { imageList = value; }
}
private ImageList imageList;
/// <summary>
/// When rendering multiple images, how many pixels should be between each image?
/// </summary>
[Category("Appearance"),
Description("When rendering multiple images, how many pixels should be between each image?"),
DefaultValue(1)]
public int Spacing {
get { return spacing; }
set { spacing = value; }
}
private int spacing = 1;
/// <summary>
/// Should text be rendered using GDI routines? This makes the text look more
/// like a native List view control.
/// </summary>
[Category("Appearance"),
Description("Should text be rendered using GDI routines?"),
DefaultValue(true)]
public bool UseGdiTextRendering {
get {
// Can't use GDI routines on a GDI+ printer context
return !this.IsPrinting && useGdiTextRendering;
}
set { useGdiTextRendering = value; }
}
private bool useGdiTextRendering = true;
#endregion
#region State Properties
/// <summary>
/// Get or set the aspect of the model object that this renderer should draw
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Object Aspect {
get {
if (aspect == null)
aspect = column.GetValue(this.rowObject);
return aspect;
}
set { aspect = value; }
}
private Object aspect;
/// <summary>
/// What are the bounds of the cell that is being drawn?
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Rectangle Bounds {
get { return bounds; }
set { bounds = value; }
}
private Rectangle bounds;
/// <summary>
/// Get or set the OLVColumn that this renderer will draw
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public OLVColumn Column {
get { return column; }
set { column = value; }
}
private OLVColumn column;
/// <summary>
/// Get/set the event that caused this renderer to be called
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public DrawListViewItemEventArgs DrawItemEvent {
get { return drawItemEventArgs; }
set { drawItemEventArgs = value; }
}
private DrawListViewItemEventArgs drawItemEventArgs;
/// <summary>
/// Get/set the event that caused this renderer to be called
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public DrawListViewSubItemEventArgs Event {
get { return eventArgs; }
set { eventArgs = value; }
}
private DrawListViewSubItemEventArgs eventArgs;
/// <summary>
/// Return the font to be used for text in this cell
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Font Font {
get {
if (this.font != null || this.ListItem == null)
return this.font;
if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems)
return this.ListItem.Font;
return this.SubItem.Font;
}
set {
this.font = value;
}
}
private Font font;
/// <summary>
/// Gets the image list from which keyed images will be fetched
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ImageList ImageListOrDefault {
get { return this.ImageList ?? this.ListView.SmallImageList; }
}
/// <summary>
/// Should this renderer fill in the background before drawing?
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool IsDrawBackground {
get { return !this.IsPrinting; }
}
/// <summary>
/// Cache whether or not our item is selected
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool IsItemSelected {
get { return isItemSelected; }
set { isItemSelected = value; }
}
private bool isItemSelected;
/// <summary>
/// Is this renderer being used on a printer context?
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool IsPrinting {
get { return isPrinting; }
set { isPrinting = value; }
}
private bool isPrinting;
/// <summary>
/// Get or set the listitem that this renderer will be drawing
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public OLVListItem ListItem {
get { return listItem; }
set { listItem = value; }
}
private OLVListItem listItem;
/// <summary>
/// Get/set the listview for which the drawing is to be done
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ObjectListView ListView {
get { return objectListView; }
set { objectListView = value; }
}
private ObjectListView objectListView;
/// <summary>
/// Get the specialized OLVSubItem that this renderer is drawing
/// </summary>
/// <remarks>This returns null for column 0.</remarks>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public OLVListSubItem OLVSubItem {
get { return listSubItem as OLVListSubItem; }
}
/// <summary>
/// Get or set the model object that this renderer should draw
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Object RowObject {
get { return rowObject; }
set { rowObject = value; }
}
private Object rowObject;
/// <summary>
/// Get or set the list subitem that this renderer will be drawing
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public OLVListSubItem SubItem {
get { return listSubItem; }
set { listSubItem = value; }
}
private OLVListSubItem listSubItem;
/// <summary>
/// The brush that will be used to paint the text
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Brush TextBrush {
get {
if (textBrush == null)
return new SolidBrush(this.GetForegroundColor());
else
return this.textBrush;
}
set { textBrush = value; }
}
private Brush textBrush;
/// <summary>
/// Will this renderer use the custom images from the parent ObjectListView
/// to draw the checkbox images.
/// </summary>
/// <remarks>
/// <para>
/// If this is true, the renderer will use the images from the
/// StateImageList to represent checkboxes. 0 - unchecked, 1 - checked, 2 - indeterminate.
/// </para>
/// <para>If this is false (the default), then the renderer will use .NET's standard
/// CheckBoxRenderer.</para>
/// </remarks>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool UseCustomCheckboxImages {
get { return useCustomCheckboxImages; }
set { useCustomCheckboxImages = value; }
}
private bool useCustomCheckboxImages;
private void ClearState() {
this.Event = null;
this.DrawItemEvent = null;
this.Aspect = null;
this.Font = null;
this.TextBrush = null;
}
#endregion
#region Utilities
/// <summary>
/// Align the second rectangle with the first rectangle,
/// according to the alignment of the column
/// </summary>
/// <param name="outer">The cell's bounds</param>
/// <param name="inner">The rectangle to be aligned within the bounds</param>
/// <returns>An aligned rectangle</returns>
protected virtual Rectangle AlignRectangle(Rectangle outer, Rectangle inner) {
Rectangle r = new Rectangle(outer.Location, inner.Size);
// Align horizontally depending on the column alignment
if (inner.Width < outer.Width) {
r.X = AlignHorizontally(outer, inner);
}
// Align vertically too
if (inner.Height < outer.Height) {
r.Y = AlignVertically(outer, inner);
}
return r;
}
/// <summary>
/// Calculate the left edge of the rectangle that aligns the outer rectangle with the inner one
/// according to this renderer's horizontal alignement
/// </summary>
/// <param name="outer"></param>
/// <param name="inner"></param>
/// <returns></returns>
protected int AlignHorizontally(Rectangle outer, Rectangle inner) {
HorizontalAlignment alignment = this.Column == null ? HorizontalAlignment.Left : this.Column.TextAlign;
switch (alignment) {
case HorizontalAlignment.Left:
return outer.Left + 1;
case HorizontalAlignment.Center:
return outer.Left + ((outer.Width - inner.Width)/2);
case HorizontalAlignment.Right:
return outer.Right - inner.Width - 1;
default:
throw new ArgumentOutOfRangeException();
}
}
/// <summary>
/// Calculate the top of the rectangle that aligns the outer rectangle with the inner rectangle
/// according to this renders vertical alignment
/// </summary>
/// <param name="outer"></param>
/// <param name="inner"></param>
/// <returns></returns>
protected int AlignVertically(Rectangle outer, Rectangle inner) {
return AlignVertically(outer, inner.Height);
}
/// <summary>
/// Calculate the top of the rectangle that aligns the outer rectangle with a rectangle of the given height
/// according to this renderer's vertical alignment
/// </summary>
/// <param name="outer"></param>
/// <param name="innerHeight"></param>
/// <returns></returns>
protected int AlignVertically(Rectangle outer, int innerHeight) {
switch (this.EffectiveCellVerticalAlignment) {
case StringAlignment.Near:
return outer.Top + 1;
case StringAlignment.Center:
return outer.Top + ((outer.Height - innerHeight) / 2);
case StringAlignment.Far:
return outer.Bottom - innerHeight - 1;
default:
throw new ArgumentOutOfRangeException();
}
}
/// <summary>
/// Calculate the space that our rendering will occupy and then align that space
/// with the given rectangle, according to the Column alignment
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <returns></returns>
protected virtual Rectangle CalculateAlignedRectangle(Graphics g, Rectangle r) {
if (this.Column == null || this.Column.TextAlign == HorizontalAlignment.Left)
return r;
int width = this.CalculateCheckBoxWidth(g);
width += this.CalculateImageWidth(g, this.GetImageSelector());
width += this.CalculateTextWidth(g, this.GetText());
// If the combined width is greater than the whole cell,
// we just use the cell itself
if (width >= r.Width)
return r;
return this.AlignRectangle(r, new Rectangle(0, 0, width, r.Height));
}
/// <summary>
/// Calculate the bounds of a checkbox given the cell bounds
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <returns></returns>
protected Rectangle CalculateCheckBoxBounds(Graphics g, Rectangle cellBounds) {
Size checkBoxSize = (UseCustomCheckboxImages && this.ListView.StateImageList != null)
? this.ListView.StateImageList.ImageSize
: CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.CheckedNormal);
return this.AlignRectangle(cellBounds,
new Rectangle(0, 0, checkBoxSize.Width, checkBoxSize.Height));
}
/// <summary>
/// How much space will the check box for this cell occupy?
/// </summary>
/// <remarks>Only column 0 can have check boxes. Sub item checkboxes are
/// treated as images</remarks>
/// <param name="g"></param>
/// <returns></returns>
protected virtual int CalculateCheckBoxWidth(Graphics g) {
if (!this.ListView.CheckBoxes || !this.ColumnIsPrimary)
return 0;
if (UseCustomCheckboxImages && this.ListView.StateImageList != null)
return this.ListView.StateImageList.ImageSize.Width;
return CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.UncheckedNormal).Width + 6;
}
/// <summary>
/// How much horizontal space will the image of this cell occupy?
/// </summary>
/// <param name="g"></param>
/// <param name="imageSelector"></param>
/// <returns></returns>
protected virtual int CalculateImageWidth(Graphics g, object imageSelector) {
if (imageSelector == null || imageSelector == System.DBNull.Value)
return 0;
// Draw from the image list (most common case)
ImageList il = this.ImageListOrDefault;
if (il != null) {
int selectorAsInt = -1;
if (imageSelector is Int32)
selectorAsInt = (Int32)imageSelector;
else {
String selectorAsString = imageSelector as String;
if (selectorAsString != null)
selectorAsInt = il.Images.IndexOfKey(selectorAsString);
}
if (selectorAsInt >= 0)
return il.ImageSize.Width;
}
// Is the selector actually an image?
Image image = imageSelector as Image;
if (image != null)
return image.Width;
return 0;
}
/// <summary>
/// How much horizontal space will the text of this cell occupy?
/// </summary>
/// <param name="g"></param>
/// <param name="txt"></param>
/// <returns></returns>
protected virtual int CalculateTextWidth(Graphics g, string txt) {
if (String.IsNullOrEmpty(txt))
return 0;
if (this.UseGdiTextRendering) {
Size proposedSize = new Size(int.MaxValue, int.MaxValue);
return TextRenderer.MeasureText(g, txt, this.Font, proposedSize, TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix).Width;
} else {
using (StringFormat fmt = new StringFormat()) {
fmt.Trimming = StringTrimming.EllipsisCharacter;
return 1 + (int)g.MeasureString(txt, this.Font, int.MaxValue, fmt).Width;
}
}
}
/// <summary>
/// Return the Color that is the background color for this item's cell
/// </summary>
/// <returns>The background color of the subitem</returns>
public virtual Color GetBackgroundColor() {
if (!this.ListView.Enabled)
return SystemColors.Control;
if (this.IsItemSelected && !this.ListView.UseTranslucentSelection && this.ListView.FullRowSelect) {
if (this.ListView.Focused)
return this.ListView.HighlightBackgroundColorOrDefault;
if (!this.ListView.HideSelection)
return this.ListView.UnfocusedHighlightBackgroundColorOrDefault;
}
if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems)
return this.ListItem.BackColor;
return this.SubItem.BackColor;
}
/// <summary>
/// Return the color to be used for text in this cell
/// </summary>
/// <returns>The text color of the subitem</returns>
public virtual Color GetForegroundColor() {
if (this.IsItemSelected && !this.ListView.UseTranslucentSelection &&
(this.ColumnIsPrimary || this.ListView.FullRowSelect)) {
if (this.ListView.Focused)
return this.ListView.HighlightForegroundColorOrDefault;
else if (!this.ListView.HideSelection)
return this.ListView.UnfocusedHighlightForegroundColorOrDefault;
}
if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems)
return this.ListItem.ForeColor;
else
return this.SubItem.ForeColor;
}
/// <summary>
/// Return the image that should be drawn against this subitem
/// </summary>
/// <returns>An Image or null if no image should be drawn.</returns>
protected virtual Image GetImage() {
return this.GetImage(this.GetImageSelector());
}
/// <summary>
/// Return the actual image that should be drawn when keyed by the given image selector.
/// An image selector can be: <list type="bullet">
/// <item><description>an int, giving the index into the image list</description></item>
/// <item><description>a string, giving the image key into the image list</description></item>
/// <item><description>an Image, being the image itself</description></item>
/// </list>
/// </summary>
/// <param name="imageSelector">The value that indicates the image to be used</param>
/// <returns>An Image or null</returns>
protected virtual Image GetImage(Object imageSelector) {
if (imageSelector == null || imageSelector == System.DBNull.Value)
return null;
ImageList il = this.ImageListOrDefault;
if (il != null) {
if (imageSelector is Int32) {
Int32 index = (Int32)imageSelector;
if (index < 0 || index >= il.Images.Count)
return null;
return il.Images[index];
}
String str = imageSelector as String;
if (str != null) {
if (il.Images.ContainsKey(str))
return il.Images[str];
return null;
}
}
return imageSelector as Image;
}
/// <summary>
/// </summary>
protected virtual Object GetImageSelector() {
return this.ColumnIsPrimary ? this.ListItem.ImageSelector : this.OLVSubItem.ImageSelector;
}
/// <summary>
/// Return the string that should be drawn within this
/// </summary>
/// <returns></returns>
protected virtual string GetText() {
return this.SubItem == null ? this.ListItem.Text : this.SubItem.Text;
}
/// <summary>
/// Return the Color that is the background color for this item's text
/// </summary>
/// <returns>The background color of the subitem's text</returns>
protected virtual Color GetTextBackgroundColor() {
//TODO: Refactor with GetBackgroundColor() - they are almost identical
if (this.IsItemSelected && !this.ListView.UseTranslucentSelection
&& (this.ColumnIsPrimary || this.ListView.FullRowSelect)) {
if (this.ListView.Focused)
return this.ListView.HighlightBackgroundColorOrDefault;
else
if (!this.ListView.HideSelection)
return this.ListView.UnfocusedHighlightBackgroundColorOrDefault;
}
if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems)
return this.ListItem.BackColor;
else
return this.SubItem.BackColor;
}
#endregion
#region IRenderer members
/// <summary>
/// Render the whole item in a non-details view.
/// </summary>
/// <param name="e"></param>
/// <param name="g"></param>
/// <param name="itemBounds"></param>
/// <param name="rowObject"></param>
/// <returns></returns>
public override bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, object rowObject) {
this.ClearState();
this.DrawItemEvent = e;
this.ListItem = (OLVListItem)e.Item;
this.SubItem = null;
this.ListView = (ObjectListView)this.ListItem.ListView;
this.Column = this.ListView.GetColumn(0);
this.RowObject = rowObject;
this.Bounds = itemBounds;
this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled;
return this.OptionalRender(g, itemBounds);
}
/// <summary>
/// Render one cell
/// </summary>
/// <param name="e"></param>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="rowObject"></param>
/// <returns></returns>
public override bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) {
this.ClearState();
this.Event = e;
this.ListItem = (OLVListItem)e.Item;
this.SubItem = (OLVListSubItem)e.SubItem;
this.ListView = (ObjectListView)this.ListItem.ListView;
this.Column = (OLVColumn)e.Header;
this.RowObject = rowObject;
this.Bounds = cellBounds;
this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled;
return this.OptionalRender(g, cellBounds);
}
/// <summary>
/// Calculate which part of this cell was hit
/// </summary>
/// <param name="hti"></param>
/// <param name="x"></param>
/// <param name="y"></param>
public override void HitTest(OlvListViewHitTestInfo hti, int x, int y) {
this.ClearState();
this.ListView = hti.ListView;
this.ListItem = hti.Item;
this.SubItem = hti.SubItem;
this.Column = hti.Column;
this.RowObject = hti.RowObject;
this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled;
if (this.SubItem == null)
this.Bounds = this.ListItem.Bounds;
else
this.Bounds = this.ListItem.GetSubItemBounds(this.Column.Index);
using (Graphics g = this.ListView.CreateGraphics()) {
this.HandleHitTest(g, hti, x, y);
}
}
/// <summary>
/// Calculate the edit rectangle
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="item"></param>
/// <param name="subItemIndex"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
public override Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) {
this.ClearState();
this.ListView = (ObjectListView)item.ListView;
this.ListItem = item;
this.SubItem = item.GetSubItem(subItemIndex);
this.Column = this.ListView.GetColumn(subItemIndex);
this.RowObject = item.RowObject;
this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled;
this.Bounds = cellBounds;
return this.HandleGetEditRectangle(g, cellBounds, item, subItemIndex, preferredSize);
}
#endregion
#region IRenderer implementation
// Subclasses will probably want to override these methods rather than the IRenderer
// interface methods.
/// <summary>
/// Draw our data into the given rectangle using the given graphics context.
/// </summary>
/// <remarks>
/// <para>Subclasses should override this method.</para></remarks>
/// <param name="g">The graphics context that should be used for drawing</param>
/// <param name="r">The bounds of the subitem cell</param>
/// <returns>Returns whether the renderering has already taken place.
/// If this returns false, the default processing will take over.
/// </returns>
public virtual bool OptionalRender(Graphics g, Rectangle r) {
if (this.ListView.View != View.Details)
return false;
this.Render(g, r);
return true;
}
/// <summary>
/// Draw our data into the given rectangle using the given graphics context.
/// </summary>
/// <remarks>
/// <para>Subclasses should override this method if they never want
/// to fall back on the default processing</para></remarks>
/// <param name="g">The graphics context that should be used for drawing</param>
/// <param name="r">The bounds of the subitem cell</param>
public virtual void Render(Graphics g, Rectangle r) {
this.StandardRender(g, r);
}
/// <summary>
/// Do the actual work of hit testing. Subclasses should override this rather than HitTest()
/// </summary>
/// <param name="g"></param>
/// <param name="hti"></param>
/// <param name="x"></param>
/// <param name="y"></param>
protected virtual void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) {
Rectangle r = this.CalculateAlignedRectangle(g, this.Bounds);
this.StandardHitTest(g, hti, r, x, y);
}
/// <summary>
/// Handle a HitTest request after all state information has been initialized
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="item"></param>
/// <param name="subItemIndex"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
protected virtual Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) {
// MAINTAINER NOTE: This type testing is wrong (design-wise). The base class should return cell bounds,
// and a more specialized class should return StandardGetEditRectangle(). But BaseRenderer is used directly
// to draw most normal cells, as well as being directly subclassed for user implemented renderers. And this
// method needs to return different bounds in each of those cases. We should have a StandardRenderer and make
// BaseRenderer into an ABC -- but that would break too much existing code. And so we have this hack :(
// If we are a standard renderer, return the position of the text, otherwise, use the whole cell.
if (this.GetType() == typeof(BaseRenderer))
return this.StandardGetEditRectangle(g, cellBounds, preferredSize);
else
return cellBounds;
}
#endregion
#region Standard IRenderer implementations
/// <summary>
/// Draw the standard "[checkbox] [image] [text]" cell after the state properties have been initialized.
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
protected void StandardRender(Graphics g, Rectangle r) {
this.DrawBackground(g, r);
// Adjust the first columns rectangle to match the padding used by the native mode of the ListView
if (this.ColumnIsPrimary) {
r.X += 3;
r.Width -= 1;
}
r = this.ApplyCellPadding(r);
this.DrawAlignedImageAndText(g, r);
// Show where the bounds of the cell padding are (debugging)
if (ObjectListView.ShowCellPaddingBounds)
g.DrawRectangle(Pens.Purple, r);
}
/// <summary>
/// Change the bounds of the given rectangle to take any cell padding into account
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public virtual Rectangle ApplyCellPadding(Rectangle r) {
Rectangle? padding = this.EffectiveCellPadding;
if (!padding.HasValue)
return r;
// The two subtractions below look wrong, but are correct!
Rectangle paddingRectangle = padding.Value;
r.Width -= paddingRectangle.Right;
r.Height -= paddingRectangle.Bottom;
r.Offset(paddingRectangle.Location);
return r;
}
/// <summary>
/// Perform normal hit testing relative to the given bounds
/// </summary>
/// <param name="g"></param>
/// <param name="hti"></param>
/// <param name="bounds"></param>
/// <param name="x"></param>
/// <param name="y"></param>
protected void StandardHitTest(Graphics g, OlvListViewHitTestInfo hti, Rectangle bounds, int x, int y) {
Rectangle r = bounds;
// Match tweaking from renderer
if (this.ColumnIsPrimary && !(this is TreeListView.TreeRenderer)) {
r.X += 3;
r.Width -= 1;
}
r = ApplyCellPadding(r);
int width = 0;
// Did they hit a check box on the primary column?
if (this.ColumnIsPrimary && this.ListView.CheckBoxes) {
Rectangle r2 = this.CalculateCheckBoxBounds(g, r);
Rectangle r3 = r2;
r3.Inflate(2, 2); // slightly larger hit area
//g.DrawRectangle(Pens.DarkGreen, r3);
if (r3.Contains(x, y)) {
hti.HitTestLocation = HitTestLocation.CheckBox;
return;
}
width = r3.Width;
}
// Did they hit the image? If they hit the image of a
// non-primary column that has a checkbox, it counts as a
// checkbox hit
r.X += width;
r.Width -= width;
width = this.CalculateImageWidth(g, this.GetImageSelector());
Rectangle rTwo = r;
rTwo.Width = width;
//g.DrawRectangle(Pens.Red, rTwo);
if (rTwo.Contains(x, y)) {
if (this.Column != null && (this.Column.Index > 0 && this.Column.CheckBoxes))
hti.HitTestLocation = HitTestLocation.CheckBox;
else
hti.HitTestLocation = HitTestLocation.Image;
return;
}
// Did they hit the text?
r.X += width;
r.Width -= width;
width = this.CalculateTextWidth(g, this.GetText());
rTwo = r;
rTwo.Width = width;
//g.DrawRectangle(Pens.Blue, rTwo);
if (rTwo.Contains(x, y)) {
hti.HitTestLocation = HitTestLocation.Text;
return;
}
hti.HitTestLocation = HitTestLocation.InCell;
}
/// <summary>
/// This method calculates the bounds of the text within a standard layout
/// (i.e. optional checkbox, optional image, text)
/// </summary>
/// <remarks>This method only works correctly if the state of the renderer
/// has been fully initialized (see BaseRenderer.GetEditRectangle)</remarks>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
protected Rectangle StandardGetEditRectangle(Graphics g, Rectangle cellBounds, Size preferredSize) {
Rectangle r = this.CalculateAlignedRectangle(g, cellBounds);
r = CalculatePaddedAlignedBounds(g, r, preferredSize);
int width = this.CalculateCheckBoxWidth(g);
width += this.CalculateImageWidth(g, this.GetImageSelector());
// Indent the primary column by the required amount
if (this.ColumnIsPrimary && this.ListItem.IndentCount > 0) {
int indentWidth = this.ListView.SmallImageSize.Width;
width += (indentWidth*this.ListItem.IndentCount);
}
// If there was either a check box or an image,
// take the check box and the image out of the rectangle, but ensure that
// there is minimum width to the editor
if (width > 0) {
r.X += width;
r.Width = Math.Max(r.Width - width, 40);
}
return r;
}
/// <summary>
/// Apply any padding to the given bounds, and then align a rectangle of the given
/// size within that padded area.
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="preferredSize"></param>
/// <returns></returns>
protected Rectangle CalculatePaddedAlignedBounds(Graphics g, Rectangle cellBounds, Size preferredSize) {
Rectangle r = ApplyCellPadding(cellBounds);
r = this.AlignRectangle(r, new Rectangle(0, 0, r.Width, preferredSize.Height));
return r;
}
#endregion
#region Drawing routines
/// <summary>
/// Draw the given image aligned horizontally within the column.
/// </summary>
/// <remarks>
/// Over tall images are scaled to fit. Over-wide images are
/// truncated. This is by design!
/// </remarks>
/// <param name="g">Graphics context to use for drawing</param>
/// <param name="r">Bounds of the cell</param>
/// <param name="image">The image to be drawn</param>
protected virtual void DrawAlignedImage(Graphics g, Rectangle r, Image image) {
if (image == null)
return;
// By default, the image goes in the top left of the rectangle
Rectangle imageBounds = new Rectangle(r.Location, image.Size);
// If the image is too tall to be drawn in the space provided, proportionally scale it down.
// Too wide images are not scaled.
if (image.Height > r.Height) {
float scaleRatio = (float)r.Height / (float)image.Height;
imageBounds.Width = (int)((float)image.Width * scaleRatio);
imageBounds.Height = r.Height - 1;
}
// Align and draw our (possibly scaled) image
Rectangle alignRectangle = this.AlignRectangle(r, imageBounds);
if (this.ListItem.Enabled)
g.DrawImage(image, alignRectangle);
else
ControlPaint.DrawImageDisabled(g, image, alignRectangle.X, alignRectangle.Y, GetBackgroundColor());
}
/// <summary>
/// Draw our subitems image and text
/// </summary>
/// <param name="g">Graphics context to use for drawing</param>
/// <param name="r">Bounds of the cell</param>
protected virtual void DrawAlignedImageAndText(Graphics g, Rectangle r) {
this.DrawImageAndText(g, this.CalculateAlignedRectangle(g, r));
}
/// <summary>
/// Fill in the background of this cell
/// </summary>
/// <param name="g">Graphics context to use for drawing</param>
/// <param name="r">Bounds of the cell</param>
protected virtual void DrawBackground(Graphics g, Rectangle r) {
if (!this.IsDrawBackground)
return;
Color backgroundColor = this.GetBackgroundColor();
using (Brush brush = new SolidBrush(backgroundColor)) {
g.FillRectangle(brush, r.X - 1, r.Y - 1, r.Width + 2, r.Height + 2);
}
}
/// <summary>
/// Draw the check box of this row
/// </summary>
/// <param name="g">Graphics context to use for drawing</param>
/// <param name="r">Bounds of the cell</param>
protected virtual int DrawCheckBox(Graphics g, Rectangle r) {
// TODO: Unify this with CheckStateRenderer
if (this.IsPrinting || this.UseCustomCheckboxImages) {
int imageIndex = this.ListItem.StateImageIndex;
if (this.ListView.StateImageList == null || imageIndex < 0 || imageIndex >= this.ListView.StateImageList.Images.Count)
return 0;
return this.DrawImage(g, r, this.ListView.StateImageList.Images[imageIndex]) + 4;
}
// The odd constants are to match checkbox placement in native mode (on XP at least)
r = this.CalculateCheckBoxBounds(g, r);
CheckBoxState boxState = this.GetCheckBoxState(this.ListItem.CheckState);
CheckBoxRenderer.DrawCheckBox(g, r.Location, boxState);
return CheckBoxRenderer.GetGlyphSize(g, boxState).Width + 6;
}
/// <summary>
/// Calculate the renderer checkboxstate we need to correctly draw the given state
/// </summary>
/// <param name="checkState"></param>
/// <returns></returns>
protected virtual CheckBoxState GetCheckBoxState(CheckState checkState) {
// Should the checkbox be drawn as disabled?
if (this.IsCheckBoxDisabled) {
switch (checkState) {
case CheckState.Checked: return CheckBoxState.CheckedDisabled;
case CheckState.Unchecked: return CheckBoxState.UncheckedDisabled;
default: return CheckBoxState.MixedDisabled;
}
}
// Is the cursor currently over this checkbox?
if (this.IsItemHot) {
switch (checkState) {
case CheckState.Checked: return CheckBoxState.CheckedHot;
case CheckState.Unchecked: return CheckBoxState.UncheckedHot;
default: return CheckBoxState.MixedHot;
}
}
// Not hot and not disabled -- just draw it normally
switch (checkState) {
case CheckState.Checked: return CheckBoxState.CheckedNormal;
case CheckState.Unchecked: return CheckBoxState.UncheckedNormal;
default: return CheckBoxState.MixedNormal;
}
}
/// <summary>
/// Should this checkbox be drawn as disabled?
/// </summary>
protected virtual bool IsCheckBoxDisabled {
get {
if (this.ListItem != null && !this.ListItem.Enabled)
return true;
if (!this.ListView.RenderNonEditableCheckboxesAsDisabled)
return false;
return (this.ListView.CellEditActivation == ObjectListView.CellEditActivateMode.None ||
(this.Column != null && !this.Column.IsEditable));
}
}
/// <summary>
/// Is the current item hot (i.e. under the mouse)?
/// </summary>
protected bool IsItemHot {
get {
return this.ListView != null &&
this.ListItem != null &&
this.ListView.HotRowIndex == this.ListItem.Index &&
this.ListView.HotColumnIndex == (this.Column == null ? 0 : this.Column.Index) &&
this.ListView.HotCellHitLocation == HitTestLocation.CheckBox;
}
}
/// <summary>
/// Draw the given text and optional image in the "normal" fashion
/// </summary>
/// <param name="g">Graphics context to use for drawing</param>
/// <param name="r">Bounds of the cell</param>
/// <param name="imageSelector">The optional image to be drawn</param>
protected virtual int DrawImage(Graphics g, Rectangle r, Object imageSelector) {
if (imageSelector == null || imageSelector == System.DBNull.Value)
return 0;
// Draw from the image list (most common case)
ImageList il = this.ListView.SmallImageList;
if (il != null) {
int selectorAsInt = -1;
if (imageSelector is Int32) {
selectorAsInt = (Int32)imageSelector;
if (selectorAsInt >= il.Images.Count)
selectorAsInt = -1;
} else {
String selectorAsString = imageSelector as String;
if (selectorAsString != null)
selectorAsInt = il.Images.IndexOfKey(selectorAsString);
}
if (selectorAsInt >= 0) {
if (this.IsPrinting) {
// For some reason, printing from an image list doesn't work onto a printer context
// So get the image from the list and fall through to the "print an image" case
imageSelector = il.Images[selectorAsInt];
} else {
if (il.ImageSize.Height < r.Height)
r.Y = this.AlignVertically(r, new Rectangle(Point.Empty, il.ImageSize));
// If we are not printing, it's probable that the given Graphics object is double buffered using a BufferedGraphics object.
// But the ImageList.Draw method doesn't honor the Translation matrix that's probably in effect on the buffered
// graphics. So we have to calculate our drawing rectangle, relative to the cells natural boundaries.
// This effectively simulates the Translation matrix.
Rectangle r2 = new Rectangle(r.X - this.Bounds.X, r.Y - this.Bounds.Y, r.Width, r.Height);
//il.Draw(g, r2.Location, selectorAsInt);
// Use this call instead of the above if you want to images to appear blended when selected
NativeMethods.DrawImageList(g, il, selectorAsInt, r2.X, r2.Y, this.IsItemSelected, !this.ListItem.Enabled);
return il.ImageSize.Width;
}
}
}
// Is the selector actually an image?
Image image = imageSelector as Image;
if (image != null) {
if (image.Size.Height < r.Height)
r.Y = this.AlignVertically(r, new Rectangle(Point.Empty, image.Size));
if (this.ListItem.Enabled)
g.DrawImageUnscaled(image, r.X, r.Y);
else
ControlPaint.DrawImageDisabled(g, image, r.X, r.Y, GetBackgroundColor());
return image.Width;
}
return 0;
}
/// <summary>
/// Draw our subitems image and text
/// </summary>
/// <param name="g">Graphics context to use for drawing</param>
/// <param name="r">Bounds of the cell</param>
protected virtual void DrawImageAndText(Graphics g, Rectangle r) {
int offset = 0;
if (this.ListView.CheckBoxes && this.ColumnIsPrimary) {
offset = this.DrawCheckBox(g, r);
r.X += offset;
r.Width -= offset;
}
offset = this.DrawImage(g, r, this.GetImageSelector());
r.X += offset;
r.Width -= offset;
this.DrawText(g, r, this.GetText());
}
/// <summary>
/// Draw the given collection of image selectors
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="imageSelectors"></param>
protected virtual int DrawImages(Graphics g, Rectangle r, ICollection imageSelectors) {
// Collect the non-null images
List<Image> images = new List<Image>();
foreach (Object selector in imageSelectors) {
Image image = this.GetImage(selector);
if (image != null)
images.Add(image);
}
// Figure out how much space they will occupy
int width = 0;
int height = 0;
foreach (Image image in images) {
width += (image.Width + this.Spacing);
height = Math.Max(height, image.Height);
}
// Align the collection of images within the cell
Rectangle r2 = this.AlignRectangle(r, new Rectangle(0, 0, width, height));
// Finally, draw all the images in their correct location
Color backgroundColor = GetBackgroundColor();
Point pt = r2.Location;
foreach (Image image in images) {
if (this.ListItem.Enabled)
g.DrawImage(image, pt);
else
ControlPaint.DrawImageDisabled(g, image, pt.X, pt.Y, backgroundColor);
pt.X += (image.Width + this.Spacing);
}
// Return the width that the images occupy
return width;
}
/// <summary>
/// Draw the given text and optional image in the "normal" fashion
/// </summary>
/// <param name="g">Graphics context to use for drawing</param>
/// <param name="r">Bounds of the cell</param>
/// <param name="txt">The string to be drawn</param>
public virtual void DrawText(Graphics g, Rectangle r, String txt) {
if (String.IsNullOrEmpty(txt))
return;
if (this.UseGdiTextRendering)
this.DrawTextGdi(g, r, txt);
else
this.DrawTextGdiPlus(g, r, txt);
}
/// <summary>
/// Print the given text in the given rectangle using only GDI routines
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="txt"></param>
/// <remarks>
/// The native list control uses GDI routines to do its drawing, so using them
/// here makes the owner drawn mode looks more natural.
/// <para>This method doesn't honour the CanWrap setting on the renderer. All
/// text is single line</para>
/// </remarks>
protected virtual void DrawTextGdi(Graphics g, Rectangle r, String txt) {
Color backColor = Color.Transparent;
if (this.IsDrawBackground && this.IsItemSelected && ColumnIsPrimary && !this.ListView.FullRowSelect)
backColor = this.GetTextBackgroundColor();
TextFormatFlags flags = TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix |
TextFormatFlags.PreserveGraphicsTranslateTransform |
this.CellVerticalAlignmentAsTextFormatFlag;
// I think there is a bug in the TextRenderer. Setting or not setting SingleLine doesn't make
// any difference -- it is always single line.
if (!this.CanWrap)
flags |= TextFormatFlags.SingleLine;
TextRenderer.DrawText(g, txt, this.Font, r, this.GetForegroundColor(), backColor, flags);
}
private bool ColumnIsPrimary {
get { return this.Column != null && this.Column.Index == 0; }
}
/// <summary>
/// Gets the cell's vertical alignment as a TextFormatFlag
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
protected TextFormatFlags CellVerticalAlignmentAsTextFormatFlag {
get {
switch (this.EffectiveCellVerticalAlignment) {
case StringAlignment.Near:
return TextFormatFlags.Top;
case StringAlignment.Center:
return TextFormatFlags.VerticalCenter;
case StringAlignment.Far:
return TextFormatFlags.Bottom;
default:
throw new ArgumentOutOfRangeException();
}
}
}
/// <summary>
/// Gets the StringFormat needed when drawing text using GDI+
/// </summary>
protected virtual StringFormat StringFormatForGdiPlus {
get {
StringFormat fmt = new StringFormat();
fmt.LineAlignment = this.EffectiveCellVerticalAlignment;
fmt.Trimming = StringTrimming.EllipsisCharacter;
fmt.Alignment = this.Column == null ? StringAlignment.Near : this.Column.TextStringAlign;
if (!this.CanWrap)
fmt.FormatFlags = StringFormatFlags.NoWrap;
return fmt;
}
}
/// <summary>
/// Print the given text in the given rectangle using normal GDI+ .NET methods
/// </summary>
/// <remarks>Printing to a printer dc has to be done using this method.</remarks>
protected virtual void DrawTextGdiPlus(Graphics g, Rectangle r, String txt) {
using (StringFormat fmt = this.StringFormatForGdiPlus) {
// Draw the background of the text as selected, if it's the primary column
// and it's selected and it's not in FullRowSelect mode.
Font f = this.Font;
if (this.IsDrawBackground && this.IsItemSelected && this.ColumnIsPrimary && !this.ListView.FullRowSelect) {
SizeF size = g.MeasureString(txt, f, r.Width, fmt);
Rectangle r2 = r;
r2.Width = (int)size.Width + 1;
using (Brush brush = new SolidBrush(this.ListView.HighlightBackgroundColorOrDefault)) {
g.FillRectangle(brush, r2);
}
}
RectangleF rf = r;
g.DrawString(txt, f, this.TextBrush, rf, fmt);
}
// We should put a focus rectange around the column 0 text if it's selected --
// but we don't because:
// - I really dislike this UI convention
// - we are using buffered graphics, so the DrawFocusRecatangle method of the event doesn't work
//if (this.ColumnIsPrimary) {
// Size size = TextRenderer.MeasureText(this.SubItem.Text, this.ListView.ListFont);
// if (r.Width > size.Width)
// r.Width = size.Width;
// this.Event.DrawFocusRectangle(r);
//}
}
#endregion
}
/// <summary>
/// This renderer highlights substrings that match a given text filter.
/// </summary>
public class HighlightTextRenderer : BaseRenderer
{
#region Life and death
/// <summary>
/// Create a HighlightTextRenderer
/// </summary>
public HighlightTextRenderer() {
this.FramePen = Pens.DarkGreen;
this.FillBrush = Brushes.Yellow;
}
/// <summary>
/// Create a HighlightTextRenderer
/// </summary>
/// <param name="filter"></param>
public HighlightTextRenderer(TextMatchFilter filter)
: this() {
this.Filter = filter;
}
/// <summary>
/// Create a HighlightTextRenderer
/// </summary>
/// <param name="text"></param>
[Obsolete("Use HighlightTextRenderer(TextMatchFilter) instead", true)]
public HighlightTextRenderer(string text) {
}
#endregion
#region Configuration properties
/// <summary>
/// Gets or set how rounded will be the corners of the text match frame
/// </summary>
[Category("Appearance"),
DefaultValue(3.0f),
Description("How rounded will be the corners of the text match frame?")]
public float CornerRoundness {
get { return cornerRoundness; }
set { cornerRoundness = value; }
}
private float cornerRoundness = 3.0f;
/// <summary>
/// Gets or set the brush will be used to paint behind the matched substrings.
/// Set this to null to not fill the frame.
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Brush FillBrush {
get { return fillBrush; }
set { fillBrush = value; }
}
private Brush fillBrush;
/// <summary>
/// Gets or sets the filter that is filtering the ObjectListView and for
/// which this renderer should highlight text
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public TextMatchFilter Filter {
get { return filter; }
set { filter = value; }
}
private TextMatchFilter filter;
/// <summary>
/// Gets or set the pen will be used to frame the matched substrings.
/// Set this to null to not draw a frame.
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Pen FramePen {
get { return framePen; }
set { framePen = value; }
}
private Pen framePen;
/// <summary>
/// Gets or sets whether the frame around a text match will have rounded corners
/// </summary>
[Category("Appearance"),
DefaultValue(true),
Description("Will the frame around a text match will have rounded corners?")]
public bool UseRoundedRectangle {
get { return useRoundedRectangle; }
set { useRoundedRectangle = value; }
}
private bool useRoundedRectangle = true;
#endregion
#region Compatibility properties
/// <summary>
/// Gets or set the text that will be highlighted
/// </summary>
[Obsolete("Set the Filter directly rather than just the text", true)]
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string TextToHighlight {
get { return String.Empty; }
set { }
}
/// <summary>
/// Gets or sets the manner in which substring will be compared.
/// </summary>
/// <remarks>
/// Use this to control if substring matches are case sensitive or insensitive.</remarks>
[Obsolete("Set the Filter directly rather than just this setting", true)]
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public StringComparison StringComparison {
get { return StringComparison.CurrentCultureIgnoreCase; }
set { }
}
#endregion
#region IRenderer interface overrides
/// <summary>
/// Handle a HitTest request after all state information has been initialized
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="item"></param>
/// <param name="subItemIndex"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) {
return this.StandardGetEditRectangle(g, cellBounds, preferredSize);
}
#endregion
#region Rendering
// This class has two implement two highlighting schemes: one for GDI, another for GDI+.
// Naturally, GDI+ makes the task easier, but we have to provide something for GDI
// since that it is what is normally used.
/// <summary>
/// Draw text using GDI
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="txt"></param>
protected override void DrawTextGdi(Graphics g, Rectangle r, string txt) {
if (this.ShouldDrawHighlighting)
this.DrawGdiTextHighlighting(g, r, txt);
base.DrawTextGdi(g, r, txt);
}
/// <summary>
/// Draw the highlighted text using GDI
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="txt"></param>
protected virtual void DrawGdiTextHighlighting(Graphics g, Rectangle r, string txt) {
TextFormatFlags flags = TextFormatFlags.NoPrefix |
TextFormatFlags.VerticalCenter | TextFormatFlags.PreserveGraphicsTranslateTransform;
// TextRenderer puts horizontal padding around the strings, so we need to take
// that into account when measuring strings
int paddingAdjustment = 6;
// Cache the font
Font f = this.Font;
foreach (CharacterRange range in this.Filter.FindAllMatchedRanges(txt)) {
// Measure the text that comes before our substring
Size precedingTextSize = Size.Empty;
if (range.First > 0) {
string precedingText = txt.Substring(0, range.First);
precedingTextSize = TextRenderer.MeasureText(g, precedingText, f, r.Size, flags);
precedingTextSize.Width -= paddingAdjustment;
}
// Measure the length of our substring (may be different each time due to case differences)
string highlightText = txt.Substring(range.First, range.Length);
Size textToHighlightSize = TextRenderer.MeasureText(g, highlightText, f, r.Size, flags);
textToHighlightSize.Width -= paddingAdjustment;
float textToHighlightLeft = r.X + precedingTextSize.Width + 1;
float textToHighlightTop = this.AlignVertically(r, textToHighlightSize.Height);
// Draw a filled frame around our substring
this.DrawSubstringFrame(g, textToHighlightLeft, textToHighlightTop, textToHighlightSize.Width, textToHighlightSize.Height);
}
}
/// <summary>
/// Draw an indication around the given frame that shows a text match
/// </summary>
/// <param name="g"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
protected virtual void DrawSubstringFrame(Graphics g, float x, float y, float width, float height) {
if (this.UseRoundedRectangle) {
using (GraphicsPath path = this.GetRoundedRect(x, y, width, height, 3.0f)) {
if (this.FillBrush != null)
g.FillPath(this.FillBrush, path);
if (this.FramePen != null)
g.DrawPath(this.FramePen, path);
}
} else {
if (this.FillBrush != null)
g.FillRectangle(this.FillBrush, x, y, width, height);
if (this.FramePen != null)
g.DrawRectangle(this.FramePen, x, y, width, height);
}
}
/// <summary>
/// Draw the text using GDI+
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="txt"></param>
protected override void DrawTextGdiPlus(Graphics g, Rectangle r, string txt) {
if (this.ShouldDrawHighlighting)
this.DrawGdiPlusTextHighlighting(g, r, txt);
base.DrawTextGdiPlus(g, r, txt);
}
/// <summary>
/// Draw the highlighted text using GDI+
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="txt"></param>
protected virtual void DrawGdiPlusTextHighlighting(Graphics g, Rectangle r, string txt) {
// Find the substrings we want to highlight
List<CharacterRange> ranges = new List<CharacterRange>(this.Filter.FindAllMatchedRanges(txt));
if (ranges.Count == 0)
return;
using (StringFormat fmt = this.StringFormatForGdiPlus) {
RectangleF rf = r;
fmt.SetMeasurableCharacterRanges(ranges.ToArray());
Region[] stringRegions = g.MeasureCharacterRanges(txt, this.Font, rf, fmt);
foreach (Region region in stringRegions) {
RectangleF bounds = region.GetBounds(g);
this.DrawSubstringFrame(g, bounds.X - 1, bounds.Y - 1, bounds.Width + 2, bounds.Height);
}
}
}
#endregion
#region Utilities
/// <summary>
/// Gets whether the renderer should actually draw highlighting
/// </summary>
protected bool ShouldDrawHighlighting {
get {
return this.Column == null || (this.Column.Searchable && this.Filter != null && this.Filter.HasComponents);
}
}
/// <summary>
/// Return a GraphicPath that is a round cornered rectangle
/// </summary>
/// <returns>A round cornered rectagle path</returns>
/// <remarks>If I could rely on people using C# 3.0+, this should be
/// an extension method of GraphicsPath.</remarks>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="diameter"></param>
protected GraphicsPath GetRoundedRect(float x, float y, float width, float height, float diameter) {
return GetRoundedRect(new RectangleF(x, y, width, height), diameter);
}
/// <summary>
/// Return a GraphicPath that is a round cornered rectangle
/// </summary>
/// <param name="rect">The rectangle</param>
/// <param name="diameter">The diameter of the corners</param>
/// <returns>A round cornered rectagle path</returns>
/// <remarks>If I could rely on people using C# 3.0+, this should be
/// an extension method of GraphicsPath.</remarks>
protected GraphicsPath GetRoundedRect(RectangleF rect, float diameter) {
GraphicsPath path = new GraphicsPath();
if (diameter > 0) {
RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter);
path.AddArc(arc, 180, 90);
arc.X = rect.Right - diameter;
path.AddArc(arc, 270, 90);
arc.Y = rect.Bottom - diameter;
path.AddArc(arc, 0, 90);
arc.X = rect.Left;
path.AddArc(arc, 90, 90);
path.CloseFigure();
} else {
path.AddRectangle(rect);
}
return path;
}
#endregion
}
/// <summary>
/// This class maps a data value to an image that should be drawn for that value.
/// </summary>
/// <remarks><para>It is useful for drawing data that is represented as an enum or boolean.</para></remarks>
public class MappedImageRenderer : BaseRenderer
{
/// <summary>
/// Return a renderer that draw boolean values using the given images
/// </summary>
/// <param name="trueImage">Draw this when our data value is true</param>
/// <param name="falseImage">Draw this when our data value is false</param>
/// <returns>A Renderer</returns>
static public MappedImageRenderer Boolean(Object trueImage, Object falseImage) {
return new MappedImageRenderer(true, trueImage, false, falseImage);
}
/// <summary>
/// Return a renderer that draw tristate boolean values using the given images
/// </summary>
/// <param name="trueImage">Draw this when our data value is true</param>
/// <param name="falseImage">Draw this when our data value is false</param>
/// <param name="nullImage">Draw this when our data value is null</param>
/// <returns>A Renderer</returns>
static public MappedImageRenderer TriState(Object trueImage, Object falseImage, Object nullImage) {
return new MappedImageRenderer(new Object[] { true, trueImage, false, falseImage, null, nullImage });
}
/// <summary>
/// Make a new empty renderer
/// </summary>
public MappedImageRenderer() {
map = new System.Collections.Hashtable();
}
/// <summary>
/// Make a new renderer that will show the given image when the given key is the aspect value
/// </summary>
/// <param name="key">The data value to be matched</param>
/// <param name="image">The image to be shown when the key is matched</param>
public MappedImageRenderer(Object key, Object image)
: this() {
this.Add(key, image);
}
/// <summary>
/// Make a new renderer that will show the given images when it receives the given keys
/// </summary>
/// <param name="key1"></param>
/// <param name="image1"></param>
/// <param name="key2"></param>
/// <param name="image2"></param>
public MappedImageRenderer(Object key1, Object image1, Object key2, Object image2)
: this() {
this.Add(key1, image1);
this.Add(key2, image2);
}
/// <summary>
/// Build a renderer from the given array of keys and their matching images
/// </summary>
/// <param name="keysAndImages">An array of key/image pairs</param>
public MappedImageRenderer(Object[] keysAndImages)
: this() {
if ((keysAndImages.GetLength(0) % 2) != 0)
throw new ArgumentException("Array must have key/image pairs");
for (int i = 0; i < keysAndImages.GetLength(0); i += 2)
this.Add(keysAndImages[i], keysAndImages[i + 1]);
}
/// <summary>
/// Register the image that should be drawn when our Aspect has the data value.
/// </summary>
/// <param name="value">Value that the Aspect must match</param>
/// <param name="image">An ImageSelector -- an int, string or image</param>
public void Add(Object value, Object image) {
if (value == null)
this.nullImage = image;
else
map[value] = image;
}
/// <summary>
/// Render our value
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
public override void Render(Graphics g, Rectangle r) {
this.DrawBackground(g, r);
r = this.ApplyCellPadding(r);
ICollection aspectAsCollection = this.Aspect as ICollection;
if (aspectAsCollection == null)
this.RenderOne(g, r, this.Aspect);
else
this.RenderCollection(g, r, aspectAsCollection);
}
/// <summary>
/// Draw a collection of images
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="imageSelectors"></param>
protected void RenderCollection(Graphics g, Rectangle r, ICollection imageSelectors) {
ArrayList images = new ArrayList();
Image image = null;
foreach (Object selector in imageSelectors) {
if (selector == null)
image = this.GetImage(this.nullImage);
else if (map.ContainsKey(selector))
image = this.GetImage(map[selector]);
else
image = null;
if (image != null)
images.Add(image);
}
this.DrawImages(g, r, images);
}
/// <summary>
/// Draw one image
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="selector"></param>
protected void RenderOne(Graphics g, Rectangle r, Object selector) {
Image image = null;
if (selector == null)
image = this.GetImage(this.nullImage);
else
if (map.ContainsKey(selector))
image = this.GetImage(map[selector]);
if (image != null)
this.DrawAlignedImage(g, r, image);
}
#region Private variables
private Hashtable map; // Track the association between values and images
private Object nullImage; // image to be drawn for null values (since null can't be a key)
#endregion
}
/// <summary>
/// This renderer draws just a checkbox to match the check state of our model object.
/// </summary>
public class CheckStateRenderer : BaseRenderer
{
/// <summary>
/// Draw our cell
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
public override void Render(Graphics g, Rectangle r) {
this.DrawBackground(g, r);
if (this.Column == null)
return;
r = this.ApplyCellPadding(r);
CheckState state = this.Column.GetCheckState(this.RowObject);
if (this.IsPrinting) {
// Renderers don't work onto printer DCs, so we have to draw the image ourselves
string key = ObjectListView.CHECKED_KEY;
if (state == CheckState.Unchecked)
key = ObjectListView.UNCHECKED_KEY;
if (state == CheckState.Indeterminate)
key = ObjectListView.INDETERMINATE_KEY;
this.DrawAlignedImage(g, r, this.ListView.SmallImageList.Images[key]);
} else {
r = this.CalculateCheckBoxBounds(g, r);
CheckBoxRenderer.DrawCheckBox(g, r.Location, this.GetCheckBoxState(state));
}
}
/// <summary>
/// Handle the GetEditRectangle request
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="item"></param>
/// <param name="subItemIndex"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) {
return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize);
}
/// <summary>
/// Handle the HitTest request
/// </summary>
/// <param name="g"></param>
/// <param name="hti"></param>
/// <param name="x"></param>
/// <param name="y"></param>
protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) {
Rectangle r = this.CalculateCheckBoxBounds(g, this.Bounds);
if (r.Contains(x, y))
hti.HitTestLocation = HitTestLocation.CheckBox;
}
}
/// <summary>
/// Render an image that comes from our data source.
/// </summary>
/// <remarks>The image can be sourced from:
/// <list type="bullet">
/// <item><description>a byte-array (normally when the image to be shown is
/// stored as a value in a database)</description></item>
/// <item><description>an int, which is treated as an index into the image list</description></item>
/// <item><description>a string, which is treated first as a file name, and failing that as an index into the image list</description></item>
/// <item><description>an ICollection of ints or strings, which will be drawn as consecutive images</description></item>
/// </list>
/// <para>If an image is an animated GIF, it's state is stored in the SubItem object.</para>
/// <para>By default, the image renderer does not render animations (it begins life with animations paused).
/// To enable animations, you must call Unpause().</para>
/// <para>In the current implementation (2009-09), each column showing animated gifs must have a
/// different instance of ImageRenderer assigned to it. You cannot share the same instance of
/// an image renderer between two animated gif columns. If you do, only the last column will be
/// animated.</para>
/// </remarks>
public class ImageRenderer : BaseRenderer
{
/// <summary>
/// Make an empty image renderer
/// </summary>
public ImageRenderer() {
this.stopwatch = new Stopwatch();
}
/// <summary>
/// Make an empty image renderer that begins life ready for animations
/// </summary>
public ImageRenderer(bool startAnimations)
: this() {
this.Paused = !startAnimations;
}
/// <summary>
/// Finalizer
/// </summary>
protected override void Dispose(bool disposing) {
Paused = true;
base.Dispose(disposing);
}
#region Properties
/// <summary>
/// Should the animations in this renderer be paused?
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool Paused {
get { return isPaused; }
set {
if (this.isPaused == value)
return;
this.isPaused = value;
if (this.isPaused) {
this.StopTickler();
this.stopwatch.Stop();
} else {
this.Tickler.Change(1, Timeout.Infinite);
this.stopwatch.Start();
}
}
}
private bool isPaused = true;
private void StopTickler() {
if (this.tickler == null)
return;
this.tickler.Dispose();
this.tickler = null;
}
/// <summary>
/// Gets a timer that can be used to trigger redraws on animations
/// </summary>
protected Timer Tickler {
get {
if (this.tickler == null)
this.tickler = new System.Threading.Timer(new TimerCallback(this.OnTimer), null, Timeout.Infinite, Timeout.Infinite);
return this.tickler;
}
}
#endregion
#region Commands
/// <summary>
/// Pause any animations
/// </summary>
public void Pause() {
this.Paused = true;
}
/// <summary>
/// Unpause any animations
/// </summary>
public void Unpause() {
this.Paused = false;
}
#endregion
#region Drawing
/// <summary>
/// Draw our image
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
public override void Render(Graphics g, Rectangle r) {
this.DrawBackground(g, r);
if (this.Aspect == null || this.Aspect == System.DBNull.Value)
return;
r = this.ApplyCellPadding(r);
if (this.Aspect is System.Byte[]) {
this.DrawAlignedImage(g, r, this.GetImageFromAspect());
} else {
ICollection imageSelectors = this.Aspect as ICollection;
if (imageSelectors == null)
this.DrawAlignedImage(g, r, this.GetImageFromAspect());
else
this.DrawImages(g, r, imageSelectors);
}
}
/// <summary>
/// Translate our Aspect into an image.
/// </summary>
/// <remarks>The strategy is:<list type="bullet">
/// <item><description>If its a byte array, we treat it as an in-memory image</description></item>
/// <item><description>If it's an int, we use that as an index into our image list</description></item>
/// <item><description>If it's a string, we try to load a file by that name. If we can't,
/// we use the string as an index into our image list.</description></item>
///</list></remarks>
/// <returns>An image</returns>
protected Image GetImageFromAspect() {
// If we've already figured out the image, don't do it again
if (this.OLVSubItem != null && this.OLVSubItem.ImageSelector is Image) {
if (this.OLVSubItem.AnimationState == null)
return (Image)this.OLVSubItem.ImageSelector;
else
return this.OLVSubItem.AnimationState.image;
}
// Try to convert our Aspect into an Image
// If its a byte array, we treat it as an in-memory image
// If it's an int, we use that as an index into our image list
// If it's a string, we try to find a file by that name.
// If we can't, we use the string as an index into our image list.
Image image = null;
if (this.Aspect is System.Byte[]) {
using (MemoryStream stream = new MemoryStream((System.Byte[])this.Aspect)) {
try {
image = Image.FromStream(stream);
}
catch (ArgumentException) {
// ignore
}
}
} else if (this.Aspect is Int32) {
image = this.GetImage(this.Aspect);
} else {
String str = this.Aspect as String;
if (!String.IsNullOrEmpty(str)) {
try {
image = Image.FromFile(str);
}
catch (FileNotFoundException) {
image = this.GetImage(this.Aspect);
}
catch (OutOfMemoryException) {
image = this.GetImage(this.Aspect);
}
}
}
// If this image is an animation, initialize the animation process
if (this.OLVSubItem != null && AnimationState.IsAnimation(image)) {
this.OLVSubItem.AnimationState = new AnimationState(image);
}
// Cache the image so we don't repeat this dreary process
if (this.OLVSubItem != null)
this.OLVSubItem.ImageSelector = image;
return image;
}
#endregion
#region Events
/// <summary>
/// This is the method that is invoked by the timer. It basically switches control to the listview thread.
/// </summary>
/// <param name="state">not used</param>
public void OnTimer(Object state) {
if (this.ListView == null || this.Paused)
return;
if (this.ListView.InvokeRequired)
this.ListView.Invoke((MethodInvoker)delegate { this.OnTimer(state); });
else
this.OnTimerInThread();
}
/// <summary>
/// This is the OnTimer callback, but invoked in the same thread as the creator of the ListView.
/// This method can use all of ListViews methods without creating a CrossThread exception.
/// </summary>
protected void OnTimerInThread() {
// MAINTAINER NOTE: This method must renew the tickler. If it doesn't the animations will stop.
// If this listview has been destroyed, we can't do anything, so we return without
// renewing the tickler, effectively killing all animations on this renderer
if (this.ListView == null || this.Paused || this.ListView.IsDisposed)
return;
// If we're not in Detail view or our column has been removed from the list,
// we can't do anything at the moment, but we still renew the tickler because the view may change later.
if (this.ListView.View != System.Windows.Forms.View.Details || this.Column == null || this.Column.Index < 0) {
this.Tickler.Change(1000, Timeout.Infinite);
return;
}
long elapsedMilliseconds = this.stopwatch.ElapsedMilliseconds;
int subItemIndex = this.Column.Index;
long nextCheckAt = elapsedMilliseconds + 1000; // wait at most one second before checking again
Rectangle updateRect = new Rectangle(); // what part of the view must be updated to draw the changed gifs?
// Run through all the subitems in the view for our column, and for each one that
// has an animation attached to it, see if the frame needs updating.
for (int i=0; i<this.ListView.GetItemCount(); i++) {
OLVListItem lvi = this.ListView.GetItem(i);
// Get the animation state from the subitem. If there isn't an animation state, skip this row.
OLVListSubItem lvsi = lvi.GetSubItem(subItemIndex);
AnimationState state = lvsi.AnimationState;
if (state == null || !state.IsValid)
continue;
// Has this frame of the animation expired?
if (elapsedMilliseconds >= state.currentFrameExpiresAt) {
state.AdvanceFrame(elapsedMilliseconds);
// Track the area of the view that needs to be redrawn to show the changed images
if (updateRect.IsEmpty)
updateRect = lvsi.Bounds;
else
updateRect = Rectangle.Union(updateRect, lvsi.Bounds);
}
// Remember the minimum time at which a frame is next due to change
nextCheckAt = Math.Min(nextCheckAt, state.currentFrameExpiresAt);
}
// Update the part of the listview where frames have changed
if (!updateRect.IsEmpty)
this.ListView.Invalidate(updateRect);
// Renew the tickler in time for the next frame change
this.Tickler.Change(nextCheckAt - elapsedMilliseconds, Timeout.Infinite);
}
#endregion
/// <summary>
/// Instances of this class kept track of the animation state of a single image.
/// </summary>
internal class AnimationState
{
const int PropertyTagTypeShort = 3;
const int PropertyTagTypeLong = 4;
const int PropertyTagFrameDelay = 0x5100;
const int PropertyTagLoopCount = 0x5101;
/// <summary>
/// Is the given image an animation
/// </summary>
/// <param name="image">The image to be tested</param>
/// <returns>Is the image an animation?</returns>
static public bool IsAnimation(Image image) {
if (image == null)
return false;
else
return (new List<Guid>(image.FrameDimensionsList)).Contains(FrameDimension.Time.Guid);
}
/// <summary>
/// Create an AnimationState in a quiet state
/// </summary>
public AnimationState() {
this.imageDuration = new List<int>();
}
/// <summary>
/// Create an animation state for the given image, which may or may not
/// be an animation
/// </summary>
/// <param name="image">The image to be rendered</param>
public AnimationState(Image image)
: this() {
if (!AnimationState.IsAnimation(image))
return;
// How many frames in the animation?
this.image = image;
this.frameCount = this.image.GetFrameCount(FrameDimension.Time);
// Find the delay between each frame.
// The delays are stored an array of 4-byte ints. Each int is the
// number of 1/100th of a second that should elapsed before the frame expires
foreach (PropertyItem pi in this.image.PropertyItems) {
if (pi.Id == PropertyTagFrameDelay) {
for (int i = 0; i < pi.Len; i += 4) {
//TODO: There must be a better way to convert 4-bytes to an int
int delay = (pi.Value[i + 3] << 24) + (pi.Value[i + 2] << 16) + (pi.Value[i + 1] << 8) + pi.Value[i];
this.imageDuration.Add(delay * 10); // store delays as milliseconds
}
break;
}
}
// There should be as many frame durations as frames
Debug.Assert(this.imageDuration.Count == this.frameCount, "There should be as many frame durations as there are frames.");
}
/// <summary>
/// Does this state represent a valid animation
/// </summary>
public bool IsValid {
get {
return (this.image != null && this.frameCount > 0);
}
}
/// <summary>
/// Advance our images current frame and calculate when it will expire
/// </summary>
public void AdvanceFrame(long millisecondsNow) {
this.currentFrame = (this.currentFrame + 1) % this.frameCount;
this.currentFrameExpiresAt = millisecondsNow + this.imageDuration[this.currentFrame];
this.image.SelectActiveFrame(FrameDimension.Time, this.currentFrame);
}
internal int currentFrame;
internal long currentFrameExpiresAt;
internal Image image;
internal List<int> imageDuration;
internal int frameCount;
}
#region Private variables
private System.Threading.Timer tickler; // timer used to tickle the animations
private Stopwatch stopwatch; // clock used to time the animation frame changes
#endregion
}
/// <summary>
/// Render our Aspect as a progress bar
/// </summary>
public class BarRenderer : BaseRenderer
{
#region Constructors
/// <summary>
/// Make a BarRenderer
/// </summary>
public BarRenderer()
: base() {
}
/// <summary>
/// Make a BarRenderer for the given range of data values
/// </summary>
public BarRenderer(int minimum, int maximum)
: this() {
this.MinimumValue = minimum;
this.MaximumValue = maximum;
}
/// <summary>
/// Make a BarRenderer using a custom bar scheme
/// </summary>
public BarRenderer(Pen pen, Brush brush)
: this() {
this.Pen = pen;
this.Brush = brush;
this.UseStandardBar = false;
}
/// <summary>
/// Make a BarRenderer using a custom bar scheme
/// </summary>
public BarRenderer(int minimum, int maximum, Pen pen, Brush brush)
: this(minimum, maximum) {
this.Pen = pen;
this.Brush = brush;
this.UseStandardBar = false;
}
/// <summary>
/// Make a BarRenderer that uses a horizontal gradient
/// </summary>
public BarRenderer(Pen pen, Color start, Color end)
: this() {
this.Pen = pen;
this.SetGradient(start, end);
}
/// <summary>
/// Make a BarRenderer that uses a horizontal gradient
/// </summary>
public BarRenderer(int minimum, int maximum, Pen pen, Color start, Color end)
: this(minimum, maximum) {
this.Pen = pen;
this.SetGradient(start, end);
}
#endregion
#region Configuration Properties
/// <summary>
/// Should this bar be drawn in the system style?
/// </summary>
[Category("ObjectListView"),
Description("Should this bar be drawn in the system style?"),
DefaultValue(true)]
public bool UseStandardBar {
get { return useStandardBar; }
set { useStandardBar = value; }
}
private bool useStandardBar = true;
/// <summary>
/// How many pixels in from our cell border will this bar be drawn
/// </summary>
[Category("ObjectListView"),
Description("How many pixels in from our cell border will this bar be drawn"),
DefaultValue(2)]
public int Padding {
get { return padding; }
set { padding = value; }
}
private int padding = 2;
/// <summary>
/// What color will be used to fill the interior of the control before the
/// progress bar is drawn?
/// </summary>
[Category("ObjectListView"),
Description("The color of the interior of the bar"),
DefaultValue(typeof(Color), "AliceBlue")]
public Color BackgroundColor {
get { return backgroundColor; }
set { backgroundColor = value; }
}
private Color backgroundColor = Color.AliceBlue;
/// <summary>
/// What color should the frame of the progress bar be?
/// </summary>
[Category("ObjectListView"),
Description("What color should the frame of the progress bar be"),
DefaultValue(typeof(Color), "Black")]
public Color FrameColor {
get { return frameColor; }
set { frameColor = value; }
}
private Color frameColor = Color.Black;
/// <summary>
/// How many pixels wide should the frame of the progress bar be?
/// </summary>
[Category("ObjectListView"),
Description("How many pixels wide should the frame of the progress bar be"),
DefaultValue(1.0f)]
public float FrameWidth {
get { return frameWidth; }
set { frameWidth = value; }
}
private float frameWidth = 1.0f;
/// <summary>
/// What color should the 'filled in' part of the progress bar be?
/// </summary>
/// <remarks>This is only used if GradientStartColor is Color.Empty</remarks>
[Category("ObjectListView"),
Description("What color should the 'filled in' part of the progress bar be"),
DefaultValue(typeof(Color), "BlueViolet")]
public Color FillColor {
get { return fillColor; }
set { fillColor = value; }
}
private Color fillColor = Color.BlueViolet;
/// <summary>
/// Use a gradient to fill the progress bar starting with this color
/// </summary>
[Category("ObjectListView"),
Description("Use a gradient to fill the progress bar starting with this color"),
DefaultValue(typeof(Color), "CornflowerBlue")]
public Color GradientStartColor {
get { return startColor; }
set {
startColor = value;
}
}
private Color startColor = Color.CornflowerBlue;
/// <summary>
/// Use a gradient to fill the progress bar ending with this color
/// </summary>
[Category("ObjectListView"),
Description("Use a gradient to fill the progress bar ending with this color"),
DefaultValue(typeof(Color), "DarkBlue")]
public Color GradientEndColor {
get { return endColor; }
set {
endColor = value;
}
}
private Color endColor = Color.DarkBlue;
/// <summary>
/// Regardless of how wide the column become the progress bar will never be wider than this
/// </summary>
[Category("Behavior"),
Description("The progress bar will never be wider than this"),
DefaultValue(100)]
public int MaximumWidth {
get { return maximumWidth; }
set { maximumWidth = value; }
}
private int maximumWidth = 100;
/// <summary>
/// Regardless of how high the cell is the progress bar will never be taller than this
/// </summary>
[Category("Behavior"),
Description("The progress bar will never be taller than this"),
DefaultValue(16)]
public int MaximumHeight {
get { return maximumHeight; }
set { maximumHeight = value; }
}
private int maximumHeight = 16;
/// <summary>
/// The minimum data value expected. Values less than this will given an empty bar
/// </summary>
[Category("Behavior"),
Description("The minimum data value expected. Values less than this will given an empty bar"),
DefaultValue(0.0)]
public double MinimumValue {
get { return minimumValue; }
set { minimumValue = value; }
}
private double minimumValue = 0.0;
/// <summary>
/// The maximum value for the range. Values greater than this will give a full bar
/// </summary>
[Category("Behavior"),
Description("The maximum value for the range. Values greater than this will give a full bar"),
DefaultValue(100.0)]
public double MaximumValue {
get { return maximumValue; }
set { maximumValue = value; }
}
private double maximumValue = 100.0;
#endregion
#region Public Properties (non-IDE)
/// <summary>
/// The Pen that will draw the frame surrounding this bar
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Pen Pen {
get {
if (this.pen == null && !this.FrameColor.IsEmpty)
return new Pen(this.FrameColor, this.FrameWidth);
else
return this.pen;
}
set {
this.pen = value;
}
}
private Pen pen;
/// <summary>
/// The brush that will be used to fill the bar
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Brush Brush {
get {
if (this.brush == null && !this.FillColor.IsEmpty)
return new SolidBrush(this.FillColor);
else
return this.brush;
}
set {
this.brush = value;
}
}
private Brush brush;
/// <summary>
/// The brush that will be used to fill the background of the bar
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Brush BackgroundBrush {
get {
if (this.backgroundBrush == null && !this.BackgroundColor.IsEmpty)
return new SolidBrush(this.BackgroundColor);
else
return this.backgroundBrush;
}
set {
this.backgroundBrush = value;
}
}
private Brush backgroundBrush;
#endregion
/// <summary>
/// Draw this progress bar using a gradient
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
public void SetGradient(Color start, Color end) {
this.GradientStartColor = start;
this.GradientEndColor = end;
}
/// <summary>
/// Draw our aspect
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
public override void Render(Graphics g, Rectangle r) {
this.DrawBackground(g, r);
r = this.ApplyCellPadding(r);
Rectangle frameRect = Rectangle.Inflate(r, 0 - this.Padding, 0 - this.Padding);
frameRect.Width = Math.Min(frameRect.Width, this.MaximumWidth);
frameRect.Height = Math.Min(frameRect.Height, this.MaximumHeight);
frameRect = this.AlignRectangle(r, frameRect);
// Convert our aspect to a numeric value
IConvertible convertable = this.Aspect as IConvertible;
if (convertable == null)
return;
double aspectValue = convertable.ToDouble(NumberFormatInfo.InvariantInfo);
Rectangle fillRect = Rectangle.Inflate(frameRect, -1, -1);
if (aspectValue <= this.MinimumValue)
fillRect.Width = 0;
else if (aspectValue < this.MaximumValue)
fillRect.Width = (int)(fillRect.Width * (aspectValue - this.MinimumValue) / this.MaximumValue);
// MS-themed progress bars don't work when printing
if (this.UseStandardBar && ProgressBarRenderer.IsSupported && !this.IsPrinting) {
ProgressBarRenderer.DrawHorizontalBar(g, frameRect);
ProgressBarRenderer.DrawHorizontalChunks(g, fillRect);
} else {
g.FillRectangle(this.BackgroundBrush, frameRect);
if (fillRect.Width > 0) {
// FillRectangle fills inside the given rectangle, so expand it a little
fillRect.Width++;
fillRect.Height++;
if (this.GradientStartColor == Color.Empty)
g.FillRectangle(this.Brush, fillRect);
else {
using (LinearGradientBrush gradient = new LinearGradientBrush(frameRect, this.GradientStartColor, this.GradientEndColor, LinearGradientMode.Horizontal)) {
g.FillRectangle(gradient, fillRect);
}
}
}
g.DrawRectangle(this.Pen, frameRect);
}
}
/// <summary>
/// Handle the GetEditRectangle request
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="item"></param>
/// <param name="subItemIndex"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) {
return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize);
}
}
/// <summary>
/// An ImagesRenderer draws zero or more images depending on the data returned by its Aspect.
/// </summary>
/// <remarks><para>This renderer's Aspect must return a ICollection of ints, strings or Images,
/// each of which will be drawn horizontally one after the other.</para>
/// <para>As of v2.1, this functionality has been absorbed into ImageRenderer and this is now an
/// empty shell, solely for backwards compatibility.</para>
/// </remarks>
[ToolboxItem(false)]
public class ImagesRenderer : ImageRenderer
{
}
/// <summary>
/// A MultiImageRenderer draws the same image a number of times based on our data value
/// </summary>
/// <remarks><para>The stars in the Rating column of iTunes is a good example of this type of renderer.</para></remarks>
public class MultiImageRenderer : BaseRenderer
{
/// <summary>
/// Make a quiet rendererer
/// </summary>
public MultiImageRenderer()
: base() {
}
/// <summary>
/// Make an image renderer that will draw the indicated image, at most maxImages times.
/// </summary>
/// <param name="imageSelector"></param>
/// <param name="maxImages"></param>
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
public MultiImageRenderer(Object imageSelector, int maxImages, int minValue, int maxValue)
: this() {
this.ImageSelector = imageSelector;
this.MaxNumberImages = maxImages;
this.MinimumValue = minValue;
this.MaximumValue = maxValue;
}
#region Configuration Properties
/// <summary>
/// The index of the image that should be drawn
/// </summary>
[Category("Behavior"),
Description("The index of the image that should be drawn"),
DefaultValue(-1)]
public int ImageIndex {
get {
if (imageSelector is Int32)
return (Int32)imageSelector;
else
return -1;
}
set { imageSelector = value; }
}
/// <summary>
/// The name of the image that should be drawn
/// </summary>
[Category("Behavior"),
Description("The index of the image that should be drawn"),
DefaultValue(null)]
public string ImageName {
get {
return imageSelector as String;
}
set { imageSelector = value; }
}
/// <summary>
/// The image selector that will give the image to be drawn
/// </summary>
/// <remarks>Like all image selectors, this can be an int, string or Image</remarks>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Object ImageSelector {
get { return imageSelector; }
set { imageSelector = value; }
}
private Object imageSelector;
/// <summary>
/// What is the maximum number of images that this renderer should draw?
/// </summary>
[Category("Behavior"),
Description("The maximum number of images that this renderer should draw"),
DefaultValue(10)]
public int MaxNumberImages {
get { return maxNumberImages; }
set { maxNumberImages = value; }
}
private int maxNumberImages = 10;
/// <summary>
/// Values less than or equal to this will have 0 images drawn
/// </summary>
[Category("Behavior"),
Description("Values less than or equal to this will have 0 images drawn"),
DefaultValue(0)]
public int MinimumValue {
get { return minimumValue; }
set { minimumValue = value; }
}
private int minimumValue = 0;
/// <summary>
/// Values greater than or equal to this will have MaxNumberImages images drawn
/// </summary>
[Category("Behavior"),
Description("Values greater than or equal to this will have MaxNumberImages images drawn"),
DefaultValue(100)]
public int MaximumValue {
get { return maximumValue; }
set { maximumValue = value; }
}
private int maximumValue = 100;
#endregion
/// <summary>
/// Draw our data value
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
public override void Render(Graphics g, Rectangle r) {
this.DrawBackground(g, r);
r = this.ApplyCellPadding(r);
Image image = this.GetImage(this.ImageSelector);
if (image == null)
return;
// Convert our aspect to a numeric value
IConvertible convertable = this.Aspect as IConvertible;
if (convertable == null)
return;
double aspectValue = convertable.ToDouble(NumberFormatInfo.InvariantInfo);
// Calculate how many images we need to draw to represent our aspect value
int numberOfImages;
if (aspectValue <= this.MinimumValue)
numberOfImages = 0;
else if (aspectValue < this.MaximumValue)
numberOfImages = 1 + (int)(this.MaxNumberImages * (aspectValue - this.MinimumValue) / this.MaximumValue);
else
numberOfImages = this.MaxNumberImages;
// If we need to shrink the image, what will its on-screen dimensions be?
int imageScaledWidth = image.Width;
int imageScaledHeight = image.Height;
if (r.Height < image.Height) {
imageScaledWidth = (int)((float)image.Width * (float)r.Height / (float)image.Height);
imageScaledHeight = r.Height;
}
// Calculate where the images should be drawn
Rectangle imageBounds = r;
imageBounds.Width = (this.MaxNumberImages * (imageScaledWidth + this.Spacing)) - this.Spacing;
imageBounds.Height = imageScaledHeight;
imageBounds = this.AlignRectangle(r, imageBounds);
// Finally, draw the images
Color backgroundColor = GetBackgroundColor();
for (int i = 0; i < numberOfImages; i++)
{
if (this.ListItem.Enabled)
g.DrawImage(image, imageBounds.X, imageBounds.Y, imageScaledWidth, imageScaledHeight);
else
ControlPaint.DrawImageDisabled(g, image, imageBounds.X, imageBounds.Y, backgroundColor);
imageBounds.X += (imageScaledWidth + this.Spacing);
}
}
/// <summary>
/// Handle the GetEditRectangle request
/// </summary>
/// <param name="g"></param>
/// <param name="cellBounds"></param>
/// <param name="item"></param>
/// <param name="subItemIndex"></param>
/// <param name="preferredSize"> </param>
/// <returns></returns>
protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) {
return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize);
}
}
/// <summary>
/// A class to render a value that contains a bitwise-OR'ed collection of values.
/// </summary>
public class FlagRenderer : BaseRenderer
{
/// <summary>
/// Register the given image to the given value
/// </summary>
/// <param name="key">When this flag is present...</param>
/// <param name="imageSelector">...draw this image</param>
public void Add(Object key, Object imageSelector) {
Int32 k2 = ((IConvertible)key).ToInt32(NumberFormatInfo.InvariantInfo);
this.imageMap[k2] = imageSelector;
this.keysInOrder.Remove(k2);
this.keysInOrder.Add(k2);
}
/// <summary>
/// Draw the flags
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
public override void Render(Graphics g, Rectangle r) {
this.DrawBackground(g, r);
IConvertible convertable = this.Aspect as IConvertible;
if (convertable == null)
return;
r = this.ApplyCellPadding(r);
Int32 v2 = convertable.ToInt32(NumberFormatInfo.InvariantInfo);
ArrayList images = new ArrayList();
foreach (Int32 key in this.keysInOrder) {
if ((v2 & key) == key) {
Image image = this.GetImage(this.imageMap[key]);
if (image != null)
images.Add(image);
}
}
if (images.Count > 0)
this.DrawImages(g, r, images);
}
/// <summary>
/// Do the actual work of hit testing. Subclasses should override this rather than HitTest()
/// </summary>
/// <param name="g"></param>
/// <param name="hti"></param>
/// <param name="x"></param>
/// <param name="y"></param>
protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) {
IConvertible convertable = this.Aspect as IConvertible;
if (convertable == null)
return;
Int32 v2 = convertable.ToInt32(NumberFormatInfo.InvariantInfo);
Point pt = this.Bounds.Location;
foreach (Int32 key in this.keysInOrder) {
if ((v2 & key) == key) {
Image image = this.GetImage(this.imageMap[key]);
if (image != null) {
Rectangle imageRect = new Rectangle(pt, image.Size);
if (imageRect.Contains(x, y)) {
hti.UserData = key;
return;
}
pt.X += (image.Width + this.Spacing);
}
}
}
}
private List<Int32> keysInOrder = new List<Int32>();
private Dictionary<Int32, Object> imageMap = new Dictionary<Int32, object>();
}
/// <summary>
/// This renderer draws an image, a single line title, and then multi-line descrition
/// under the title.
/// </summary>
/// <remarks>
/// <para>This class works best with FullRowSelect = true.</para>
/// <para>It's not designed to work with cell editing -- it will work but will look odd.</para>
/// <para>
/// This class is experimental. It may not work properly and may disappear from
/// future versions.
/// </para>
/// </remarks>
public class DescribedTaskRenderer : BaseRenderer
{
/// <summary>
/// Create a DescribedTaskRenderer
/// </summary>
public DescribedTaskRenderer() {
}
#region Configuration properties
/// <summary>
/// Gets or set the font that will be used to draw the title of the task
/// </summary>
/// <remarks>If this is null, the ListView's font will be used</remarks>
[Category("ObjectListView"),
Description("The font that will be used to draw the title of the task"),
DefaultValue(null)]
public Font TitleFont {
get { return titleFont; }
set { titleFont = value; }
}
private Font titleFont;
/// <summary>
/// Return a font that has been set for the title or a reasonable default
/// </summary>
[Browsable(false)]
public Font TitleFontOrDefault {
get {
return this.TitleFont ?? this.ListView.Font;
}
}
/// <summary>
/// Gets or set the color of the title of the task
/// </summary>
/// <remarks>This color is used when the task is not selected or when the listview
/// has a translucent selection mechanism.</remarks>
[Category("ObjectListView"),
Description("The color of the title"),
DefaultValue(typeof(Color), "")]
public Color TitleColor {
get { return titleColor; }
set { titleColor = value; }
}
private Color titleColor;
/// <summary>
/// Return the color of the title of the task or a reasonable default
/// </summary>
[Browsable(false)]
public Color TitleColorOrDefault {
get {
if (this.IsItemSelected || this.TitleColor.IsEmpty)
return this.GetForegroundColor();
else
return this.TitleColor;
}
}
/// <summary>
/// Gets or set the font that will be used to draw the description of the task
/// </summary>
/// <remarks>If this is null, the ListView's font will be used</remarks>
[Category("ObjectListView"),
Description("The font that will be used to draw the description of the task"),
DefaultValue(null)]
public Font DescriptionFont {
get { return descriptionFont; }
set { descriptionFont = value; }
}
private Font descriptionFont;
/// <summary>
/// Return a font that has been set for the title or a reasonable default
/// </summary>
[Browsable(false)]
public Font DescriptionFontOrDefault {
get {
return this.DescriptionFont ?? this.ListView.Font;
}
}
/// <summary>
/// Gets or set the color of the description of the task
/// </summary>
/// <remarks>This color is used when the task is not selected or when the listview
/// has a translucent selection mechanism.</remarks>
[Category("ObjectListView"),
Description("The color of the description"),
DefaultValue(typeof(Color), "DimGray")]
public Color DescriptionColor {
get { return descriptionColor; }
set { descriptionColor = value; }
}
private Color descriptionColor = Color.DimGray;
/// <summary>
/// Return the color of the description of the task or a reasonable default
/// </summary>
[Browsable(false)]
public Color DescriptionColorOrDefault {
get {
if (this.DescriptionColor.IsEmpty || (this.IsItemSelected && !this.ListView.UseTranslucentSelection))
return this.GetForegroundColor();
else
return this.DescriptionColor;
}
}
/// <summary>
/// Gets or sets the number of pixels that will be left between the image and the text
/// </summary>
[Category("ObjectListView"),
Description("The number of pixels that that will be left between the image and the text"),
DefaultValue(4)]
public int ImageTextSpace {
get { return imageTextSpace; }
set { imageTextSpace = value; }
}
private int imageTextSpace = 4;
/// <summary>
/// Gets or sets the name of the aspect of the model object that contains the task description
/// </summary>
[Category("ObjectListView"),
Description("The name of the aspect of the model object that contains the task description"),
DefaultValue(null)]
public string DescriptionAspectName {
get { return descriptionAspectName; }
set { descriptionAspectName = value; }
}
private string descriptionAspectName;
#endregion
#region Calculating
/// <summary>
/// Fetch the description from the model class
/// </summary>
/// <returns></returns>
protected virtual string GetDescription() {
if (String.IsNullOrEmpty(this.DescriptionAspectName))
return String.Empty;
if (this.descriptionGetter == null)
this.descriptionGetter = new Munger(this.DescriptionAspectName);
return this.descriptionGetter.GetValue(this.RowObject) as String;
}
Munger descriptionGetter;
#endregion
#region Rendering
/// <summary>
/// Draw our item
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
public override void Render(Graphics g, Rectangle r) {
this.DrawBackground(g, r);
r = this.ApplyCellPadding(r);
this.DrawDescribedTask(g, r, this.Aspect as String, this.GetDescription(), this.GetImage());
}
/// <summary>
/// Draw the task
/// </summary>
/// <param name="g"></param>
/// <param name="r"></param>
/// <param name="title"></param>
/// <param name="description"></param>
/// <param name="image"></param>
protected virtual void DrawDescribedTask(Graphics g, Rectangle r, string title, string description, Image image) {
Rectangle cellBounds = this.ApplyCellPadding(r);
Rectangle textBounds = cellBounds;
if (image != null) {
g.DrawImage(image, cellBounds.Location);
int gapToText = image.Width + this.ImageTextSpace;
textBounds.X += gapToText;
textBounds.Width -= gapToText;
}
// Color the background if the row is selected and we're not using a translucent selection
if (this.IsItemSelected && !this.ListView.UseTranslucentSelection) {
using (SolidBrush b = new SolidBrush(this.GetTextBackgroundColor())) {
g.FillRectangle(b, textBounds);
}
}
// Draw the title
if (!String.IsNullOrEmpty(title)) {
using (StringFormat fmt = new StringFormat(StringFormatFlags.NoWrap)) {
fmt.Trimming = StringTrimming.EllipsisCharacter;
fmt.Alignment = StringAlignment.Near;
fmt.LineAlignment = StringAlignment.Near;
Font f = this.TitleFontOrDefault;
using (SolidBrush b = new SolidBrush(this.TitleColorOrDefault)) {
g.DrawString(title, f, b, textBounds, fmt);
}
// How tall was the title?
SizeF size = g.MeasureString(title, f, (int)textBounds.Width, fmt);
textBounds.Y += (int)size.Height;
textBounds.Height -= (int)size.Height;
}
}
// Draw the description
if (!String.IsNullOrEmpty(description)) {
using (StringFormat fmt2 = new StringFormat()) {
fmt2.Trimming = StringTrimming.EllipsisCharacter;
using (SolidBrush b = new SolidBrush(this.DescriptionColorOrDefault)) {
g.DrawString(description, this.DescriptionFontOrDefault, b, textBounds, fmt2);
}
}
}
}
#endregion
#region Hit Testing
/// <summary>
/// Handle the HitTest request
/// </summary>
/// <param name="g"></param>
/// <param name="hti"></param>
/// <param name="x"></param>
/// <param name="y"></param>
protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) {
if (this.Bounds.Contains(x, y))
hti.HitTestLocation = HitTestLocation.Text;
}
#endregion
}
}