/* * 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.9 * 2015-08-22 JPP - Allow selected row back/fore colours to be specified for each row * 2015-06-23 JPP - Added ColumnButtonRenderer plus general support for Buttons * 2015-06-22 JPP - Added BaseRenderer.ConfigureItem() and ConfigureSubItem() to easily allow * other renderers to be chained for use within a primary renderer. * - Lots of tightening of hit tests and edit rectangles * 2015-05-15 JPP - Handle renderering an Image when that Image is returned as an aspect. * 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 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 . * * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.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 { /// /// Renderers are the mechanism used for owner drawing cells. As such, they can also handle /// hit detection and positioning of cell editing rectangles. /// public interface IRenderer { /// /// Render the whole item within an ObjectListView. This is only used in non-Details views. /// /// The event /// A Graphics for rendering /// The bounds of the item /// The model object to be drawn /// Return true to indicate that the event was handled and no further processing is needed. bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, Object rowObject); /// /// Render one cell within an ObjectListView when it is in Details mode. /// /// The event /// A Graphics for rendering /// The bounds of the cell /// The model object to be drawn /// Return true to indicate that the event was handled and no further processing is needed. bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, Object rowObject); /// /// What is under the given point? /// /// /// x co-ordinate /// y co-ordinate /// This method should only alter HitTestLocation and/or UserData. void HitTest(OlvListViewHitTestInfo hti, int x, int y); /// /// When the value in the given cell is to be edited, where should the edit rectangle be placed? /// /// /// /// /// /// /// Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize); } /// /// Renderers that implement this interface will have the filter property updated, /// each time the filter on the ObjectListView is updated. /// public interface IFilterAwareRenderer { /// /// /// IModelFilter Filter { get; set; } } /// /// An AbstractRenderer is a do-nothing implementation of the IRenderer interface. /// [Browsable(true), ToolboxItem(false)] public class AbstractRenderer : Component, IRenderer { #region IRenderer Members /// /// Render the whole item within an ObjectListView. This is only used in non-Details views. /// /// The event /// A Graphics for rendering /// The bounds of the item /// The model object to be drawn /// Return true to indicate that the event was handled and no further processing is needed. public virtual bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, object rowObject) { return true; } /// /// Render one cell within an ObjectListView when it is in Details mode. /// /// The event /// A Graphics for rendering /// The bounds of the cell /// The model object to be drawn /// Return true to indicate that the event was handled and no further processing is needed. public virtual bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) { return false; } /// /// What is under the given point? /// /// /// x co-ordinate /// y co-ordinate /// This method should only alter HitTestLocation and/or UserData. public virtual void HitTest(OlvListViewHitTestInfo hti, int x, int y) {} /// /// When the value in the given cell is to be edited, where should the edit rectangle be placed? /// /// /// /// /// /// /// public virtual Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { return cellBounds; } #endregion } /// /// This class provides compatibility for v1 RendererDelegates /// [ToolboxItem(false)] internal class Version1Renderer : AbstractRenderer { public Version1Renderer(RenderDelegate renderDelegate) { this.RenderDelegate = renderDelegate; } /// /// The renderer delegate that this renderer wraps /// 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 } /// /// A BaseRenderer provides useful base level functionality for any custom renderer. /// /// /// Subclasses will normally override the Render or OptionalRender method, and use the other /// methods as helper functions. /// [Browsable(true), ToolboxItem(true)] public class BaseRenderer : AbstractRenderer { internal const TextFormatFlags NormalTextFormatFlags = TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis | TextFormatFlags.PreserveGraphicsTranslateTransform; #region Configuration Properties /// /// Can the renderer wrap lines that do not fit completely within the cell? /// /// Wrapping text doesn't work with the GDI renderer. [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; /// /// Gets or sets how many pixels will be left blank around this cell /// /// /// /// This setting only takes effect when the control is owner drawn. /// /// for more details. /// [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; /// /// Gets the horiztonal alignment of the column /// [Browsable(false)] public HorizontalAlignment CellHorizontalAlignment { get { return this.Column == null ? HorizontalAlignment.Left : this.Column.TextAlign; } } /// /// Gets or sets how cells drawn by this renderer will be vertically aligned. /// /// /// /// If this is not set, the value from the column or control itself will be used. /// /// [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; /// /// Gets the optional padding that this renderer should apply before drawing. /// This property considers all possible sources of padding /// [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; } } /// /// Gets the vertical cell alignment that should govern the rendering. /// This property considers all possible sources. /// [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; } } /// /// Gets or sets the image list from which keyed images will be fetched /// [Category("Appearance"), Description("The image list from which keyed images will be fetched for drawing. If this is not given, the small ImageList from the ObjectListView will be used"), DefaultValue(null)] public ImageList ImageList { get { return imageList; } set { imageList = value; } } private ImageList imageList; /// /// When rendering multiple images, how many pixels should be between each image? /// [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; /// /// Should text be rendered using GDI routines? This makes the text look more /// like a native List view control. /// [Category("Appearance"), Description("Should text be rendered using GDI routines?"), DefaultValue(true)] public virtual 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 /// /// Get or set the aspect of the model object that this renderer should draw /// [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; /// /// What are the bounds of the cell that is being drawn? /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Rectangle Bounds { get { return bounds; } set { bounds = value; } } private Rectangle bounds; /// /// Get or set the OLVColumn that this renderer will draw /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public OLVColumn Column { get { return column; } set { column = value; } } private OLVColumn column; /// /// Get/set the event that caused this renderer to be called /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public DrawListViewItemEventArgs DrawItemEvent { get { return drawItemEventArgs; } set { drawItemEventArgs = value; } } private DrawListViewItemEventArgs drawItemEventArgs; /// /// Get/set the event that caused this renderer to be called /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public DrawListViewSubItemEventArgs Event { get { return eventArgs; } set { eventArgs = value; } } private DrawListViewSubItemEventArgs eventArgs; /// /// Gets or sets the font to be used for text in this cell /// [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; /// /// Gets the image list from which keyed images will be fetched /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ImageList ImageListOrDefault { get { return this.ImageList ?? this.ListView.SmallImageList; } } /// /// Should this renderer fill in the background before drawing? /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool IsDrawBackground { get { return !this.IsPrinting; } } /// /// Cache whether or not our item is selected /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool IsItemSelected { get { return isItemSelected; } set { isItemSelected = value; } } private bool isItemSelected; /// /// Is this renderer being used on a printer context? /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool IsPrinting { get { return isPrinting; } set { isPrinting = value; } } private bool isPrinting; /// /// Get or set the listitem that this renderer will be drawing /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public OLVListItem ListItem { get { return listItem; } set { listItem = value; } } private OLVListItem listItem; /// /// Get/set the listview for which the drawing is to be done /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ObjectListView ListView { get { return objectListView; } set { objectListView = value; } } private ObjectListView objectListView; /// /// Get the specialized OLVSubItem that this renderer is drawing /// /// This returns null for column 0. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public OLVListSubItem OLVSubItem { get { return listSubItem as OLVListSubItem; } } /// /// Get or set the model object that this renderer should draw /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Object RowObject { get { return rowObject; } set { rowObject = value; } } private Object rowObject; /// /// Get or set the list subitem that this renderer will be drawing /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public OLVListSubItem SubItem { get { return listSubItem; } set { listSubItem = value; } } private OLVListSubItem listSubItem; /// /// The brush that will be used to paint the text /// [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; /// /// Will this renderer use the custom images from the parent ObjectListView /// to draw the checkbox images. /// /// /// /// If this is true, the renderer will use the images from the /// StateImageList to represent checkboxes. 0 - unchecked, 1 - checked, 2 - indeterminate. /// /// If this is false (the default), then the renderer will use .NET's standard /// CheckBoxRenderer. /// [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 /// /// Align the second rectangle with the first rectangle, /// according to the alignment of the column /// /// The cell's bounds /// The rectangle to be aligned within the bounds /// An aligned rectangle 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; } /// /// Calculate the left edge of the rectangle that aligns the outer rectangle with the inner one /// according to this renderer's horizontal alignment /// /// /// /// protected int AlignHorizontally(Rectangle outer, Rectangle inner) { HorizontalAlignment alignment = this.CellHorizontalAlignment; 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(); } } /// /// Calculate the top of the rectangle that aligns the outer rectangle with the inner rectangle /// according to this renders vertical alignment /// /// /// /// protected int AlignVertically(Rectangle outer, Rectangle inner) { return AlignVertically(outer, inner.Height); } /// /// 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 /// /// /// /// 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(); } } /// /// Calculate the space that our rendering will occupy and then align that space /// with the given rectangle, according to the Column alignment /// /// /// Pre-padded bounds of the cell /// protected virtual Rectangle CalculateAlignedRectangle(Graphics g, Rectangle r) { if (this.Column == null) return r; Rectangle contentRectangle = new Rectangle(Point.Empty, this.CalculateContentSize(g, r)); return this.AlignRectangle(r, contentRectangle); } /// /// Calculate the size of the content of this cell. /// /// /// Pre-padded bounds of the cell /// The width and height of the content protected virtual Size CalculateContentSize(Graphics g, Rectangle r) { Size checkBoxSize = this.CalculatePrimaryCheckBoxSize(g); Size imageSize = this.CalculateImageSize(g, this.GetImageSelector()); Size textSize = this.CalculateTextSize(g, this.GetText(), r.Width - (checkBoxSize.Width + imageSize.Width)); // If the combined width is greater than the whole cell, we just use the cell itself int width = Math.Min(r.Width, checkBoxSize.Width + imageSize.Width + textSize.Width); int componentMaxHeight = Math.Max(checkBoxSize.Height, Math.Max(imageSize.Height, textSize.Height)); int height = Math.Min(r.Height, componentMaxHeight); return new Size(width, height); } /// /// Calculate the bounds of a checkbox given the (pre-padded) cell bounds /// /// /// Pre-padded cell bounds /// protected Rectangle CalculateCheckBoxBounds(Graphics g, Rectangle cellBounds) { Size checkBoxSize = this.CalculateCheckBoxSize(g); return this.AlignRectangle(cellBounds, new Rectangle(0, 0, checkBoxSize.Width, checkBoxSize.Height)); } /// /// How much space will the check box for this cell occupy? /// /// Only column 0 can have check boxes. Sub item checkboxes are /// treated as images /// /// protected virtual Size CalculateCheckBoxSize(Graphics g) { if (UseCustomCheckboxImages && this.ListView.StateImageList != null) return this.ListView.StateImageList.ImageSize; return CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.UncheckedNormal); } /// /// How much space will the check box for this row occupy? /// If the list doesn't have checkboxes, or this isn't the primary column, /// this returns an empty size. /// /// /// protected virtual Size CalculatePrimaryCheckBoxSize(Graphics g) { if (!this.ListView.CheckBoxes || !this.ColumnIsPrimary) return Size.Empty; Size size = this.CalculateCheckBoxSize(g); size.Width += 6; return size; } /// /// How much horizontal space will the image of this cell occupy? /// /// /// /// protected virtual int CalculateImageWidth(Graphics g, object imageSelector) { return this.CalculateImageSize(g, imageSelector).Width + 2; } /// /// How much vertical space will the image of this cell occupy? /// /// /// /// protected virtual int CalculateImageHeight(Graphics g, object imageSelector) { return this.CalculateImageSize(g, imageSelector).Height; } /// /// How much space will the image of this cell occupy? /// /// /// /// protected virtual Size CalculateImageSize(Graphics g, object imageSelector) { if (imageSelector == null || imageSelector == DBNull.Value) return Size.Empty; // Check for the image in 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; } // Is the selector actually an image? Image image = imageSelector as Image; if (image != null) return image.Size; return Size.Empty; } /// /// How much horizontal space will the text of this cell occupy? /// /// /// /// /// protected virtual int CalculateTextWidth(Graphics g, string txt, int width) { if (String.IsNullOrEmpty(txt)) return 0; return CalculateTextSize(g, txt, width).Width; } /// /// How much space will the text of this cell occupy? /// /// /// /// /// protected virtual Size CalculateTextSize(Graphics g, string txt, int width) { if (String.IsNullOrEmpty(txt)) return Size.Empty; if (this.UseGdiTextRendering) { Size proposedSize = new Size(width, Int32.MaxValue); return TextRenderer.MeasureText(g, txt, this.Font, proposedSize, NormalTextFormatFlags); } // Using GDI+ renderering using (StringFormat fmt = new StringFormat()) { fmt.Trimming = StringTrimming.EllipsisCharacter; SizeF sizeF = g.MeasureString(txt, this.Font, width, fmt); return new Size(1 + (int)sizeF.Width, 1 + (int)sizeF.Height); } } /// /// Return the Color that is the background color for this item's cell /// /// The background color of the subitem public virtual Color GetBackgroundColor() { if (!this.ListView.Enabled) return SystemColors.Control; if (this.IsItemSelected && !this.ListView.UseTranslucentSelection && this.ListView.FullRowSelect) return this.GetSelectedBackgroundColor(); if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems) return this.ListItem.BackColor; return this.SubItem.BackColor; } /// /// Return the color of the background color when the item is selected /// /// The background color of the subitem public virtual Color GetSelectedBackgroundColor() { if (this.ListView.Focused) return this.ListItem.SelectedBackColor ?? this.ListView.SelectedBackColorOrDefault; if (!this.ListView.HideSelection) return this.ListView.UnfocusedSelectedBackColorOrDefault; return this.ListItem.BackColor; } /// /// Return the color to be used for text in this cell /// /// The text color of the subitem public virtual Color GetForegroundColor() { if (this.IsItemSelected && !this.ListView.UseTranslucentSelection && (this.ColumnIsPrimary || this.ListView.FullRowSelect)) return this.GetSelectedForegroundColor(); return this.SubItem == null || this.ListItem.UseItemStyleForSubItems ? this.ListItem.ForeColor : this.SubItem.ForeColor; } /// /// Return the color of the foreground color when the item is selected /// /// The foreground color of the subitem public virtual Color GetSelectedForegroundColor() { if (this.ListView.Focused) return this.ListItem.SelectedForeColor ?? this.ListView.SelectedForeColorOrDefault; if (!this.ListView.HideSelection) return this.ListView.UnfocusedSelectedForeColorOrDefault; return this.SubItem == null || this.ListItem.UseItemStyleForSubItems ? this.ListItem.ForeColor : this.SubItem.ForeColor; } /// /// Return the image that should be drawn against this subitem /// /// An Image or null if no image should be drawn. protected virtual Image GetImage() { return this.GetImage(this.GetImageSelector()); } /// /// Return the actual image that should be drawn when keyed by the given image selector. /// An image selector can be: /// an int, giving the index into the image list /// a string, giving the image key into the image list /// an Image, being the image itself /// /// /// The value that indicates the image to be used /// An Image or null protected virtual Image GetImage(Object imageSelector) { if (imageSelector == null || imageSelector == 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; } /// /// protected virtual Object GetImageSelector() { return this.ColumnIsPrimary ? this.ListItem.ImageSelector : this.OLVSubItem.ImageSelector; } /// /// Return the string that should be drawn within this /// /// protected virtual string GetText() { return this.SubItem == null ? this.ListItem.Text : this.SubItem.Text; } /// /// Return the Color that is the background color for this item's text /// /// The background color of the subitem's text [Obsolete("Use GetBackgroundColor() instead")] protected virtual Color GetTextBackgroundColor() { return Color.Red; // just so it shows up if it is used } #endregion #region IRenderer members /// /// Render the whole item in a non-details view. /// /// /// /// /// /// public override bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, object model) { this.ConfigureItem(e, itemBounds, model); return this.OptionalRender(g, itemBounds); } /// /// Prepare this renderer to draw in response to the given event /// /// /// /// /// Use this if you want to chain a second renderer within a primary renderer. public virtual void ConfigureItem(DrawListViewItemEventArgs e, Rectangle itemBounds, object model) { 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 = model; this.Bounds = itemBounds; this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; } /// /// Render one cell /// /// /// /// /// /// public override bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object model) { this.ConfigureSubItem(e, cellBounds, model); return this.OptionalRender(g, cellBounds); } /// /// Prepare this renderer to draw in response to the given event /// /// /// /// /// Use this if you want to chain a second renderer within a primary renderer. public virtual void ConfigureSubItem(DrawListViewSubItemEventArgs e, Rectangle cellBounds, object model) { 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 = model; this.Bounds = cellBounds; this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; } /// /// Calculate which part of this cell was hit /// /// /// /// 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); } } /// /// Calculate the edit rectangle /// /// /// /// /// /// /// 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. /// /// Draw our data into the given rectangle using the given graphics context. /// /// /// Subclasses should override this method. /// The graphics context that should be used for drawing /// The bounds of the subitem cell /// Returns whether the rendering has already taken place. /// If this returns false, the default processing will take over. /// public virtual bool OptionalRender(Graphics g, Rectangle r) { if (this.ListView.View != View.Details) return false; this.Render(g, r); return true; } /// /// Draw our data into the given rectangle using the given graphics context. /// /// /// Subclasses should override this method if they never want /// to fall back on the default processing /// The graphics context that should be used for drawing /// The bounds of the subitem cell public virtual void Render(Graphics g, Rectangle r) { this.StandardRender(g, r); } /// /// Do the actual work of hit testing. Subclasses should override this rather than HitTest() /// /// /// /// /// protected virtual void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { Rectangle r = this.CalculateAlignedRectangle(g, ApplyCellPadding(this.Bounds)); this.StandardHitTest(g, hti, r, x, y); } /// /// Handle a HitTest request after all state information has been initialized /// /// /// /// /// /// /// 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); // Center the editor vertically if (cellBounds.Height != preferredSize.Height) cellBounds.Y += (cellBounds.Height - preferredSize.Height) / 2; return cellBounds; } #endregion #region Standard IRenderer implementations /// /// Draw the standard "[checkbox] [image] [text]" cell after the state properties have been initialized. /// /// /// 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 && this.CellHorizontalAlignment == HorizontalAlignment.Left ) { 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); } /// /// Change the bounds of the given rectangle to take any cell padding into account /// /// /// 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; } /// /// Perform normal hit testing relative to the given aligned content bounds /// /// /// /// /// /// protected virtual void StandardHitTest(Graphics g, OlvListViewHitTestInfo hti, Rectangle alignedContentRectangle, int x, int y) { Rectangle r = alignedContentRectangle; // Match tweaking from renderer if (this.ColumnIsPrimary && this.CellHorizontalAlignment == HorizontalAlignment.Left && !(this is TreeListView.TreeRenderer)) { r.X += 3; r.Width -= 1; } int width = 0; // Did they hit a check box on the primary column? if (this.ColumnIsPrimary && this.ListView.CheckBoxes) { Size checkBoxSize = this.CalculateCheckBoxSize(g); int checkBoxTop = this.AlignVertically(r, checkBoxSize.Height); Rectangle r3 = new Rectangle(r.X, checkBoxTop, checkBoxSize.Width, checkBoxSize.Height); width = r3.Width + 6; // g.DrawRectangle(Pens.DarkGreen, r3); if (r3.Contains(x, y)) { hti.HitTestLocation = HitTestLocation.CheckBox; return; } } // 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(), r.Width); rTwo = r; rTwo.Width = width; // g.DrawRectangle(Pens.Blue, rTwo); if (rTwo.Contains(x, y)) { hti.HitTestLocation = HitTestLocation.Text; return; } hti.HitTestLocation = HitTestLocation.InCell; } /// /// This method calculates the bounds of the text within a standard layout /// (i.e. optional checkbox, optional image, text) /// /// This method only works correctly if the state of the renderer /// has been fully initialized (see BaseRenderer.GetEditRectangle) /// /// /// /// protected virtual Rectangle StandardGetEditRectangle(Graphics g, Rectangle cellBounds, Size preferredSize) { Size contentSize = this.CalculateContentSize(g, cellBounds); int contentWidth = this.Column.CellEditUseWholeCellEffective ? cellBounds.Width : contentSize.Width; Rectangle editControlBounds = this.CalculatePaddedAlignedBounds(g, cellBounds, new Size(contentWidth, preferredSize.Height)); Size checkBoxSize = this.CalculatePrimaryCheckBoxSize(g); int imageWidth = this.CalculateImageWidth(g, this.GetImageSelector()); int width = checkBoxSize.Width + imageWidth; // Indent the primary column by the required amount if (this.ListItem.IndentCount > 0) { int indentWidth = this.ListView.SmallImageSize.Width * this.ListItem.IndentCount; width += indentWidth; } editControlBounds.X += width; editControlBounds.Width -= width; if (editControlBounds.Width < 50) editControlBounds.Width = 50; if (editControlBounds.Right > cellBounds.Right) editControlBounds.Width = cellBounds.Right - editControlBounds.Left; return editControlBounds; } /// /// Apply any padding to the given bounds, and then align a rectangle of the given /// size within that padded area. /// /// /// /// /// protected Rectangle CalculatePaddedAlignedBounds(Graphics g, Rectangle cellBounds, Size preferredSize) { Rectangle r = ApplyCellPadding(cellBounds); r = this.AlignRectangle(r, new Rectangle(Point.Empty, preferredSize)); return r; } #endregion #region Drawing routines /// /// Draw the given image aligned horizontally within the column. /// /// /// Over tall images are scaled to fit. Over-wide images are /// truncated. This is by design! /// /// Graphics context to use for drawing /// Bounds of the cell /// The image to be drawn 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()); } /// /// Draw our subitems image and text /// /// Graphics context to use for drawing /// Pre-padded bounds of the cell protected virtual void DrawAlignedImageAndText(Graphics g, Rectangle r) { this.DrawImageAndText(g, this.CalculateAlignedRectangle(g, r)); } /// /// Fill in the background of this cell /// /// Graphics context to use for drawing /// Bounds of the cell 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); } } /// /// Draw the primary check box of this row (checkboxes in other sub items use a different method) /// /// Graphics context to use for drawing /// The pre-aligned and padded target rectangle protected virtual int DrawCheckBox(Graphics g, Rectangle r) { // The odd constants are to match checkbox placement in native mode (on XP at least) // TODO: Unify this with CheckStateRenderer // The rectangle r is already horizontally aligned. We still need to align it vertically. Size checkBoxSize = this.CalculateCheckBoxSize(g); Point checkBoxLocation = new Point(r.X, this.AlignVertically(r, checkBoxSize.Height)); 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, new Rectangle(checkBoxLocation, checkBoxSize), this.ListView.StateImageList.Images[imageIndex]) + 4; } CheckBoxState boxState = this.GetCheckBoxState(this.ListItem.CheckState); CheckBoxRenderer.DrawCheckBox(g, checkBoxLocation, boxState); return checkBoxSize.Width; } /// /// Calculate the CheckBoxState we need to correctly draw the given state /// /// /// 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.IsCheckboxHot) { 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; } } /// /// Should this checkbox be drawn as disabled? /// 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)); } } /// /// Is the current item hot (i.e. under the mouse)? /// protected bool IsCellHot { get { return this.ListView != null && this.ListView.HotRowIndex == this.ListItem.Index && this.ListView.HotColumnIndex == (this.Column == null ? 0 : this.Column.Index); } } /// /// Is the mouse over a checkbox in this cell? /// protected bool IsCheckboxHot { get { return this.IsCellHot && this.ListView.HotCellHitLocation == HitTestLocation.CheckBox; } } /// /// Draw the given text and optional image in the "normal" fashion /// /// Graphics context to use for drawing /// Bounds of the cell /// The optional image to be drawn protected virtual int DrawImage(Graphics g, Rectangle r, Object imageSelector) { if (imageSelector == null || imageSelector == DBNull.Value) return 0; // Draw from the image list (most common case) ImageList il = this.ImageListOrDefault; if (il != null) { // Try to translate our imageSelector into a valid ImageList index 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 we found a valid index into the ImageList, draw it. // We want to draw using the native DrawImageList calls, since that let's us do some nice effects // But the native call does not work on PrinterDCs, so if we're printing we have to skip this bit. if (selectorAsInt >= 0) { if (!this.IsPrinting) { 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); NativeMethods.DrawImageList(g, il, selectorAsInt, r2.X, r2.Y, this.IsItemSelected, !this.ListItem.Enabled); return il.ImageSize.Width; } // 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]; } } // Is the selector actually an image? Image image = imageSelector as Image; if (image == null) return 0; // no, give up 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; } /// /// Draw our subitems image and text /// /// Graphics context to use for drawing /// Bounds of the cell protected virtual void DrawImageAndText(Graphics g, Rectangle r) { int offset = 0; if (this.ListView.CheckBoxes && this.ColumnIsPrimary) { offset = this.DrawCheckBox(g, r) + 6; 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()); } /// /// Draw the given collection of image selectors /// /// /// /// protected virtual int DrawImages(Graphics g, Rectangle r, ICollection imageSelectors) { // Collect the non-null images List images = new List(); 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; } /// /// Draw the given text and optional image in the "normal" fashion /// /// Graphics context to use for drawing /// Bounds of the cell /// The string to be drawn 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); } /// /// Print the given text in the given rectangle using only GDI routines /// /// /// /// /// /// The native list control uses GDI routines to do its drawing, so using them /// here makes the owner drawn mode looks more natural. /// This method doesn't honour the CanWrap setting on the renderer. All /// text is single line /// 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.GetSelectedBackgroundColor(); TextFormatFlags flags = NormalTextFormatFlags | 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; } } /// /// Gets the cell's vertical alignment as a TextFormatFlag /// /// 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(); } } } /// /// Gets the StringFormat needed when drawing text using GDI+ /// 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; } } /// /// Print the given text in the given rectangle using normal GDI+ .NET methods /// /// Printing to a printer dc has to be done using this method. 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.GetSelectedBackgroundColor())) { g.FillRectangle(brush, r2); } } RectangleF rf = r; g.DrawString(txt, f, this.TextBrush, rf, fmt); } // We should put a focus rectangle 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 } /// /// This renderer highlights substrings that match a given text filter. /// public class HighlightTextRenderer : BaseRenderer, IFilterAwareRenderer { #region Life and death /// /// Create a HighlightTextRenderer /// public HighlightTextRenderer() { this.FramePen = Pens.DarkGreen; this.FillBrush = Brushes.Yellow; } /// /// Create a HighlightTextRenderer /// /// public HighlightTextRenderer(TextMatchFilter filter) : this() { this.Filter = filter; } /// /// Create a HighlightTextRenderer /// /// [Obsolete("Use HighlightTextRenderer(TextMatchFilter) instead", true)] public HighlightTextRenderer(string text) {} #endregion #region Configuration properties /// /// Gets or set how rounded will be the corners of the text match frame /// [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; /// /// Gets or set the brush will be used to paint behind the matched substrings. /// Set this to null to not fill the frame. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Brush FillBrush { get { return fillBrush; } set { fillBrush = value; } } private Brush fillBrush; /// /// Gets or sets the filter that is filtering the ObjectListView and for /// which this renderer should highlight text /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public TextMatchFilter Filter { get { return filter; } set { filter = value; } } private TextMatchFilter filter; /// /// When a filter changes, keep track of the text matching filters /// IModelFilter IFilterAwareRenderer.Filter { get { return filter; } set { RegisterNewFilter(value); } } internal void RegisterNewFilter(IModelFilter newFilter) { TextMatchFilter textFilter = newFilter as TextMatchFilter; if (textFilter != null) { Filter = textFilter; return; } CompositeFilter composite = newFilter as CompositeFilter; if (composite != null) { foreach (TextMatchFilter textSubFilter in composite.TextFilters) { Filter = textSubFilter; return; } } Filter = null; } /// /// Gets or set the pen will be used to frame the matched substrings. /// Set this to null to not draw a frame. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Pen FramePen { get { return framePen; } set { framePen = value; } } private Pen framePen; /// /// Gets or sets whether the frame around a text match will have rounded corners /// [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 /// /// Gets or set the text that will be highlighted /// [Obsolete("Set the Filter directly rather than just the text", true)] [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public string TextToHighlight { get { return String.Empty; } set { } } /// /// Gets or sets the manner in which substring will be compared. /// /// /// Use this to control if substring matches are case sensitive or insensitive. [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 /// /// Handle a HitTest request after all state information has been initialized /// /// /// /// /// /// /// 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. /// /// Draw text using GDI /// /// /// /// protected override void DrawTextGdi(Graphics g, Rectangle r, string txt) { if (this.ShouldDrawHighlighting) this.DrawGdiTextHighlighting(g, r, txt); base.DrawTextGdi(g, r, txt); } /// /// Draw the highlighted text using GDI /// /// /// /// protected virtual void DrawGdiTextHighlighting(Graphics g, Rectangle r, string txt) { // TextRenderer puts horizontal padding around the strings, so we need to take // that into account when measuring strings const 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, NormalTextFormatFlags); 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, NormalTextFormatFlags); 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); } } /// /// Draw an indication around the given frame that shows a text match /// /// /// /// /// /// 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) { if (this.IsItemSelected) { g.FillPath(Brushes.Red, path); } else { g.FillPath(this.FillBrush, path); } } if (this.FramePen != null) g.DrawPath(this.FramePen, path); } } else { if (this.FillBrush != null) if (this.IsItemSelected) { g.FillRectangle(Brushes.Red, x, y, width, height); } else { g.FillRectangle(this.FillBrush, x, y, width, height); } if (this.FramePen != null) g.DrawRectangle(this.FramePen, x, y, width, height); } } /// /// Draw the text using GDI+ /// /// /// /// protected override void DrawTextGdiPlus(Graphics g, Rectangle r, string txt) { if (this.ShouldDrawHighlighting) this.DrawGdiPlusTextHighlighting(g, r, txt); base.DrawTextGdiPlus(g, r, txt); } /// /// Draw the highlighted text using GDI+ /// /// /// /// protected virtual void DrawGdiPlusTextHighlighting(Graphics g, Rectangle r, string txt) { // Find the substrings we want to highlight List ranges = new List(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 /// /// Gets whether the renderer should actually draw highlighting /// protected bool ShouldDrawHighlighting { get { return this.Column == null || (this.Column.Searchable && this.Filter != null && this.Filter.HasComponents); } } /// /// Return a GraphicPath that is a round cornered rectangle /// /// A round cornered rectangle path /// If I could rely on people using C# 3.0+, this should be /// an extension method of GraphicsPath. /// /// /// /// /// protected GraphicsPath GetRoundedRect(float x, float y, float width, float height, float diameter) { return GetRoundedRect(new RectangleF(x, y, width, height), diameter); } /// /// Return a GraphicPath that is a round cornered rectangle /// /// The rectangle /// The diameter of the corners /// A round cornered rectangle path /// If I could rely on people using C# 3.0+, this should be /// an extension method of GraphicsPath. 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 } /// /// This class maps a data value to an image that should be drawn for that value. /// /// It is useful for drawing data that is represented as an enum or boolean. public class MappedImageRenderer : BaseRenderer { /// /// Return a renderer that draw boolean values using the given images /// /// Draw this when our data value is true /// Draw this when our data value is false /// A Renderer public static MappedImageRenderer Boolean(Object trueImage, Object falseImage) { return new MappedImageRenderer(true, trueImage, false, falseImage); } /// /// Return a renderer that draw tristate boolean values using the given images /// /// Draw this when our data value is true /// Draw this when our data value is false /// Draw this when our data value is null /// A Renderer public static MappedImageRenderer TriState(Object trueImage, Object falseImage, Object nullImage) { return new MappedImageRenderer(new Object[] {true, trueImage, false, falseImage, null, nullImage}); } /// /// Make a new empty renderer /// public MappedImageRenderer() { map = new System.Collections.Hashtable(); } /// /// Make a new renderer that will show the given image when the given key is the aspect value /// /// The data value to be matched /// The image to be shown when the key is matched public MappedImageRenderer(Object key, Object image) : this() { this.Add(key, image); } /// /// Make a new renderer that will show the given images when it receives the given keys /// /// /// /// /// public MappedImageRenderer(Object key1, Object image1, Object key2, Object image2) : this() { this.Add(key1, image1); this.Add(key2, image2); } /// /// Build a renderer from the given array of keys and their matching images /// /// An array of key/image pairs 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]); } /// /// Register the image that should be drawn when our Aspect has the data value. /// /// Value that the Aspect must match /// An ImageSelector -- an int, string or image public void Add(Object value, Object image) { if (value == null) this.nullImage = image; else map[value] = image; } /// /// Render our value /// /// /// 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); } /// /// Draw a collection of images /// /// /// /// 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); } /// /// Draw one image /// /// /// /// 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 } /// /// This renderer draws just a checkbox to match the check state of our model object. /// public class CheckStateRenderer : BaseRenderer { /// /// Draw our cell /// /// /// 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.ImageListOrDefault.Images[key]); } else { r = this.CalculateCheckBoxBounds(g, r); CheckBoxRenderer.DrawCheckBox(g, r.Location, this.GetCheckBoxState(state)); } } /// /// Handle the GetEditRectangle request /// /// /// /// /// /// /// protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize); } /// /// Handle the HitTest request /// /// /// /// /// 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; } } /// /// Render an image that comes from our data source. /// /// The image can be sourced from: /// /// a byte-array (normally when the image to be shown is /// stored as a value in a database) /// an int, which is treated as an index into the image list /// a string, which is treated first as a file name, and failing that as an index into the image list /// an ICollection of ints or strings, which will be drawn as consecutive images /// /// If an image is an animated GIF, it's state is stored in the SubItem object. /// By default, the image renderer does not render animations (it begins life with animations paused). /// To enable animations, you must call Unpause(). /// 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. /// public class ImageRenderer : BaseRenderer { /// /// Make an empty image renderer /// public ImageRenderer() { this.stopwatch = new Stopwatch(); } /// /// Make an empty image renderer that begins life ready for animations /// public ImageRenderer(bool startAnimations) : this() { this.Paused = !startAnimations; } /// /// Finalizer /// protected override void Dispose(bool disposing) { Paused = true; base.Dispose(disposing); } #region Properties /// /// Should the animations in this renderer be paused? /// [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; } /// /// Gets a timer that can be used to trigger redraws on animations /// 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 /// /// Pause any animations /// public void Pause() { this.Paused = true; } /// /// Unpause any animations /// public void Unpause() { this.Paused = false; } #endregion #region Drawing /// /// Draw our image /// /// /// 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); } } /// /// Translate our Aspect into an image. /// /// The strategy is: /// 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 load a file by that name. If we can't, /// we use the string as an index into our image list. /// /// An image 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 = this.Aspect as Image; if (image != null) { // Don't do anything else } else 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 /// /// This is the method that is invoked by the timer. It basically switches control to the listview thread. /// /// not used public void OnTimer(Object state) { if (this.IsListViewDead) return; if (this.Paused) return; if (this.ListView.InvokeRequired) this.ListView.Invoke((MethodInvoker) delegate { this.OnTimer(state); }); else this.OnTimerInThread(); } private bool IsListViewDead { get { // Apply a whole heap of sanity checks, which basically ensure that the ListView is still alive return this.ListView == null || this.ListView.Disposing || this.ListView.IsDisposed || !this.ListView.IsHandleCreated; } } /// /// 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. /// 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.IsListViewDead) return; if (this.Paused) 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 /// /// Instances of this class kept track of the animation state of a single image. /// internal class AnimationState { private const int PropertyTagTypeShort = 3; private const int PropertyTagTypeLong = 4; private const int PropertyTagFrameDelay = 0x5100; private const int PropertyTagLoopCount = 0x5101; /// /// Is the given image an animation /// /// The image to be tested /// Is the image an animation? public static bool IsAnimation(Image image) { if (image == null) return false; else return (new List(image.FrameDimensionsList)).Contains(FrameDimension.Time.Guid); } /// /// Create an AnimationState in a quiet state /// public AnimationState() { this.imageDuration = new List(); } /// /// Create an animation state for the given image, which may or may not /// be an animation /// /// The image to be rendered 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."); } /// /// Does this state represent a valid animation /// public bool IsValid { get { return (this.image != null && this.frameCount > 0); } } /// /// Advance our images current frame and calculate when it will expire /// 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 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 } /// /// Render our Aspect as a progress bar /// public class BarRenderer : BaseRenderer { #region Constructors /// /// Make a BarRenderer /// public BarRenderer() : base() {} /// /// Make a BarRenderer for the given range of data values /// public BarRenderer(int minimum, int maximum) : this() { this.MinimumValue = minimum; this.MaximumValue = maximum; } /// /// Make a BarRenderer using a custom bar scheme /// public BarRenderer(Pen pen, Brush brush) : this() { this.Pen = pen; this.Brush = brush; this.UseStandardBar = false; } /// /// Make a BarRenderer using a custom bar scheme /// public BarRenderer(int minimum, int maximum, Pen pen, Brush brush) : this(minimum, maximum) { this.Pen = pen; this.Brush = brush; this.UseStandardBar = false; } /// /// Make a BarRenderer that uses a horizontal gradient /// public BarRenderer(Pen pen, Color start, Color end) : this() { this.Pen = pen; this.SetGradient(start, end); } /// /// Make a BarRenderer that uses a horizontal gradient /// 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 /// /// 此栏是否应以系统样式绘制? /// [Category("ObjectListView"), Description("此栏是否应以系统样式绘制"), DefaultValue(true)] public bool UseStandardBar { get { return useStandardBar; } set { useStandardBar = value; } } private bool useStandardBar = true; /// /// 将从我们的单元格边框开始绘制多少像素的条形图 /// [Category("ObjectListView"), Description("将从我们的单元格边框开始绘制多少像素的条形图"), DefaultValue(2)] public int Padding { get { return padding; } set { padding = value; } } private int padding = 2; /// ///在绘制进度条之前,将使用什么颜色填充控件内部? /// [Category("ObjectListView"), Description("条形图内部的颜色"), DefaultValue(typeof (Color), "AliceBlue")] public Color BackgroundColor { get { return backgroundColor; } set { backgroundColor = value; } } private Color backgroundColor = Color.AliceBlue; /// /// 进度条边框颜色 /// [Category("ObjectListView"), Description("进度条边框颜色"), DefaultValue(typeof (Color), "Black")] public Color FrameColor { get { return frameColor; } set { frameColor = value; } } private Color frameColor = Color.Black; /// /// 进度条的边框像素宽 /// [Category("ObjectListView"), Description("进度条的边框像素宽"), DefaultValue(1.0f)] public float FrameWidth { get { return frameWidth; } set { frameWidth = value; } } private float frameWidth = 1.0f; /// /// 进度条的“填充”部分应该是什么颜色? /// /// 仅当GradientStartColor为Color.Empty时才使用此选项 [Category("ObjectListView"), Description("进度条的“填充”部分应该是什么颜色?"), DefaultValue(typeof (Color), "BlueViolet")] public Color FillColor { get { return fillColor; } set { fillColor = value; } } private Color fillColor = Color.BlueViolet; /// /// 使用渐变以此颜色开始填充进度条 /// [Category("ObjectListView"), Description("使用渐变以此颜色开始填充进度条"), DefaultValue(typeof (Color), "CornflowerBlue")] public Color GradientStartColor { get { return startColor; } set { startColor = value; } } private Color startColor = Color.CornflowerBlue; /// /// 使用渐变填充以此颜色结尾的进度条 /// [Category("ObjectListView"), Description("使用渐变填充以此颜色结尾的进度条"), DefaultValue(typeof (Color), "DarkBlue")] public Color GradientEndColor { get { return endColor; } set { endColor = value; } } private Color endColor = Color.DarkBlue; /// /// 使用渐变以此颜色开始填充渐满进度条 /// [Category("ObjectListView"), Description("使用渐变以此颜色开始填充渐满进度条"), DefaultValue(typeof(Color), "CornflowerBlue")] public Color NearGradientStartColor { get { return near_startColor; } set { near_startColor = value; } } private Color near_startColor = Color.CornflowerBlue; /// /// 使用渐变填充以此颜色结尾的渐满进度条 /// [Category("ObjectListView"), Description("使用渐变填充以此颜色结尾的渐满进度条"), DefaultValue(typeof(Color), "DarkBlue")] public Color NearGradientEndColor { get { return near_endColor; } set { near_endColor = value; } } private Color near_endColor = Color.DarkBlue; /// /// 无论列变得多宽,进度条都不会比这个宽。 /// [Category("Behavior"), Description("进度条永远不会比这个更宽"), DefaultValue(100)] public int MaximumWidth { get { return maximumWidth; } set { maximumWidth = value; } } private int maximumWidth = 100; /// /// 无论单元格有多高,进度条都不会比这个高 /// [Category("Behavior"), Description("进度条永远不会比这个高"), DefaultValue(16)] public int MaximumHeight { get { return maximumHeight; } set { maximumHeight = value; } } private int maximumHeight = 16; /// /// 预期的最小数据值。小于此值的值将显示一个空条。 /// [Category("Behavior"), Description("预期的最小数据值。小于此值的值将显示一个空条。"), DefaultValue(0.0)] public double MinimumValue { get { return minimumValue; } set { minimumValue = value; } } private double minimumValue = 0.0; /// /// 接近满时的值,高于该值时,显示不同颜色,小于等于MinimumValue或大于MaximumValue值,则表示未设置该值 /// [Category("Behavior"), Description("接近满时的值,高于该值时,显示不同颜色,小于等于MinimumValue或大于MaximumValue值,则表示未设置该值"), DefaultValue(0.0)] public double NearFullValue { get { return nearfullValue; } set { nearfullValue = value; } } private double nearfullValue = 0.0; /// ///范围的最大值。大于此值的值将给出一个完整的条形。 /// [Category("Behavior"), Description("范围的最大值。大于此值的值将给出一个完整的条形。"), DefaultValue(100.0)] public double MaximumValue { get { return maximumValue; } set { maximumValue = value; } } private double maximumValue = 100.0; #endregion #region Public Properties (non-IDE) /// /// The Pen that will draw the frame surrounding this bar /// [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; /// /// The brush that will be used to fill the bar /// [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; /// /// The brush that will be used to fill the bar /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Brush NearBrush { get { if (this.near_brush == null && !this.FillColor.IsEmpty) return new SolidBrush(this.FillColor); else return this.near_brush; } set { this.near_brush = value; } } private Brush near_brush; /// /// The brush that will be used to fill the background of the bar /// [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 /// /// Draw this progress bar using a gradient /// /// /// public void SetGradient(Color start, Color end) { this.GradientStartColor = start; this.GradientEndColor = end; } /// /// Draw our aspect /// /// /// 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 (aspectValue >= nearfullValue && nearfullValue >= this.MinimumValue && nearfullValue<= this.MaximumValue) { if (this.NearGradientStartColor == Color.Empty) g.FillRectangle(this.NearBrush, fillRect); else { using (LinearGradientBrush gradient = new LinearGradientBrush(frameRect, this.NearGradientStartColor, this.NearGradientEndColor, LinearGradientMode.Horizontal)) { g.FillRectangle(gradient, fillRect); } } } else { 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); } } /// /// Handle the GetEditRectangle request /// /// /// /// /// /// /// protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize); } } /// /// An ImagesRenderer draws zero or more images depending on the data returned by its Aspect. /// /// This renderer's Aspect must return a ICollection of ints, strings or Images, /// each of which will be drawn horizontally one after the other. /// As of v2.1, this functionality has been absorbed into ImageRenderer and this is now an /// empty shell, solely for backwards compatibility. /// [ToolboxItem(false)] public class ImagesRenderer : ImageRenderer {} /// /// A MultiImageRenderer draws the same image a number of times based on our data value /// /// The stars in the Rating column of iTunes is a good example of this type of renderer. public class MultiImageRenderer : BaseRenderer { /// /// Make a quiet renderer /// public MultiImageRenderer() : base() {} /// /// Make an image renderer that will draw the indicated image, at most maxImages times. /// /// /// /// /// 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 /// /// The index of the image that should be drawn /// [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; } } /// /// The name of the image that should be drawn /// [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; } } /// /// The image selector that will give the image to be drawn /// /// Like all image selectors, this can be an int, string or Image [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Object ImageSelector { get { return imageSelector; } set { imageSelector = value; } } private Object imageSelector; /// /// What is the maximum number of images that this renderer should draw? /// [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; /// /// Values less than or equal to this will have 0 images drawn /// [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; /// /// Values greater than or equal to this will have MaxNumberImages images drawn /// [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 /// /// Draw our data value /// /// /// 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 Rectangle singleImageRect = new Rectangle(imageBounds.X, imageBounds.Y, imageScaledWidth, imageScaledHeight); Color backgroundColor = GetBackgroundColor(); for (int i = 0; i < numberOfImages; i++) { if (this.ListItem.Enabled) { this.DrawImage(g, singleImageRect, this.ImageSelector); } else ControlPaint.DrawImageDisabled(g, image, singleImageRect.X, singleImageRect.Y, backgroundColor); singleImageRect.X += (imageScaledWidth + this.Spacing); } } } /// /// A class to render a value that contains a bitwise-OR'ed collection of values. /// public class FlagRenderer : BaseRenderer { /// /// Register the given image to the given value /// /// When this flag is present... /// ...draw this image 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); } /// /// Draw the flags /// /// /// 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); } /// /// Do the actual work of hit testing. Subclasses should override this rather than HitTest() /// /// /// /// /// 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 keysInOrder = new List(); private Dictionary imageMap = new Dictionary(); } /// /// This renderer draws an image, a single line title, and then multi-line description /// under the title. /// /// /// This class works best with FullRowSelect = true. /// It's not designed to work with cell editing -- it will work but will look odd. /// /// It's not RightToLeft friendly. /// /// public class DescribedTaskRenderer : BaseRenderer, IFilterAwareRenderer { private readonly StringFormat noWrapStringFormat; private readonly HighlightTextRenderer highlightTextRenderer = new HighlightTextRenderer(); /// /// Create a DescribedTaskRenderer /// public DescribedTaskRenderer() { this.noWrapStringFormat = new StringFormat(StringFormatFlags.NoWrap); this.noWrapStringFormat.Trimming = StringTrimming.EllipsisCharacter; this.noWrapStringFormat.Alignment = StringAlignment.Near; this.noWrapStringFormat.LineAlignment = StringAlignment.Near; this.highlightTextRenderer.CellVerticalAlignment = StringAlignment.Near; } #region Configuration properties /// /// Should text be rendered using GDI routines? This makes the text look more /// like a native List view control. /// public override bool UseGdiTextRendering { get { return base.UseGdiTextRendering; } set { base.UseGdiTextRendering = value; this.highlightTextRenderer.UseGdiTextRendering = value; } } /// /// Gets or set the font that will be used to draw the title of the task /// /// If this is null, the ListView's font will be used [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; /// /// Return a font that has been set for the title or a reasonable default /// [Browsable(false)] public Font TitleFontOrDefault { get { return this.TitleFont ?? this.ListView.Font; } } /// /// Gets or set the color of the title of the task /// /// This color is used when the task is not selected or when the listview /// has a translucent selection mechanism. [Category("ObjectListView"), Description("The color of the title"), DefaultValue(typeof (Color), "")] public Color TitleColor { get { return titleColor; } set { titleColor = value; } } private Color titleColor; /// /// Return the color of the title of the task or a reasonable default /// [Browsable(false)] public Color TitleColorOrDefault { get { if (!this.ListItem.Enabled) return this.SubItem.ForeColor; if (this.IsItemSelected || this.TitleColor.IsEmpty) return this.GetForegroundColor(); return this.TitleColor; } } /// /// Gets or set the font that will be used to draw the description of the task /// /// If this is null, the ListView's font will be used [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; /// /// Return a font that has been set for the title or a reasonable default /// [Browsable(false)] public Font DescriptionFontOrDefault { get { return this.DescriptionFont ?? this.ListView.Font; } } /// /// Gets or set the color of the description of the task /// /// This color is used when the task is not selected or when the listview /// has a translucent selection mechanism. [Category("ObjectListView"), Description("The color of the description"), DefaultValue(typeof (Color), "")] public Color DescriptionColor { get { return descriptionColor; } set { descriptionColor = value; } } private Color descriptionColor = Color.Empty; /// /// Return the color of the description of the task or a reasonable default /// [Browsable(false)] public Color DescriptionColorOrDefault { get { if (!this.ListItem.Enabled) return this.SubItem.ForeColor; if (this.IsItemSelected && !this.ListView.UseTranslucentSelection) return this.GetForegroundColor(); return this.DescriptionColor.IsEmpty ? defaultDescriptionColor : this.DescriptionColor; } } private static Color defaultDescriptionColor = Color.FromArgb(45, 46, 49); /// /// Gets or sets the number of pixels that will be left between the image and the text /// [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; /// /// 顶部留白像素 /// [Category("ObjectListView"), Description("顶部留白像素"), DefaultValue(4)] public int TopSpace { get { return topSpace; } set { topSpace = value; } } private int topSpace = 4; /// /// Gets or sets the number of pixels that will be left between the title and the description /// [Category("ObjectListView"), Description("The number of pixels that that will be left between the title and the description"), DefaultValue(2)] public int TitleDescriptionSpace { get { return titleDescriptionSpace; } set { titleDescriptionSpace = value; } } private int titleDescriptionSpace = 2; /// /// Gets or sets the name of the aspect of the model object that contains the task description /// [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 Text highlighting /// /// Gets or sets the filter that is filtering the ObjectListView and for /// which this renderer should highlight text /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public TextMatchFilter Filter { get { return this.highlightTextRenderer.Filter; } set { this.highlightTextRenderer.Filter = value; } } /// /// When a filter changes, keep track of the text matching filters /// IModelFilter IFilterAwareRenderer.Filter { get { return this.Filter; } set { this.highlightTextRenderer.RegisterNewFilter(value); } } #endregion #region Calculating /// /// Fetch the description from the model class /// /// /// public virtual string GetDescription(object model) { if (String.IsNullOrEmpty(this.DescriptionAspectName)) return String.Empty; if (this.descriptionGetter == null) this.descriptionGetter = new Munger(this.DescriptionAspectName); return this.descriptionGetter.GetValue(model) as string; } private Munger descriptionGetter; #endregion #region Rendering /// /// /// /// /// /// public override void ConfigureSubItem(DrawListViewSubItemEventArgs e, Rectangle cellBounds, object model) { base.ConfigureSubItem(e, cellBounds, model); this.highlightTextRenderer.ConfigureSubItem(e, cellBounds, model); } /// /// Draw our item /// /// /// public override void Render(Graphics g, Rectangle r) { this.DrawBackground(g, r); r = this.ApplyCellPadding(r); this.DrawDescribedTask(g, r, this.GetText(), this.GetDescription(this.RowObject), this.GetImageSelector()); } /// /// Draw the task /// /// /// /// /// /// protected virtual void DrawDescribedTask(Graphics g, Rectangle r, string title, string description, object imageSelector) { //Debug.WriteLine(String.Format("DrawDescribedTask({0}, {1}, {2}, {3})", r, title, description, imageSelector)); // Draw the image if one's been given Rectangle textBounds = r; if (imageSelector != null) { int imageWidth = this.DrawImage(g, r, imageSelector); int gapToText = imageWidth + this.ImageTextSpace; textBounds.Y += TopSpace; textBounds.X += gapToText; textBounds.Width -= gapToText; } // Draw the title if (!String.IsNullOrEmpty(title)) { using (SolidBrush b = new SolidBrush(this.TitleColorOrDefault)) { this.highlightTextRenderer.CanWrap = false; this.highlightTextRenderer.Font = this.TitleFontOrDefault; this.highlightTextRenderer.TextBrush = b; this.highlightTextRenderer.DrawText(g, textBounds, title); } // How tall was the title? SizeF size = g.MeasureString(title, this.TitleFontOrDefault, textBounds.Width, this.noWrapStringFormat); int pixelsToDescription = this.TitleDescriptionSpace + (int)size.Height; textBounds.Y += pixelsToDescription; textBounds.Height -= pixelsToDescription; } // Draw the description if (!String.IsNullOrEmpty(description)) { using (SolidBrush b = new SolidBrush(this.DescriptionColorOrDefault)) { this.highlightTextRenderer.CanWrap = true; this.highlightTextRenderer.Font = this.DescriptionFontOrDefault; this.highlightTextRenderer.TextBrush = b; this.highlightTextRenderer.DrawText(g, textBounds, description); } } //g.DrawRectangle(Pens.OrangeRed, r); } #endregion #region Hit Testing /// /// Handle the HitTest request /// /// /// /// /// protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { if (this.Bounds.Contains(x, y)) hti.HitTestLocation = HitTestLocation.Text; } #endregion } /// /// This renderer draws a functioning button in its cell /// public class ColumnButtonRenderer : BaseRenderer { #region Properties /// /// Gets or sets how each button will be sized /// [Category("ObjectListView"), Description("How each button will be sized"), DefaultValue(OLVColumn.ButtonSizingMode.TextBounds)] public OLVColumn.ButtonSizingMode SizingMode { get { return this.sizingMode; } set { this.sizingMode = value; } } private OLVColumn.ButtonSizingMode sizingMode = OLVColumn.ButtonSizingMode.TextBounds; /// /// Gets or sets the size of the button when the SizingMode is FixedBounds /// /// If this is not set, the bounds of the cell will be used [Category("ObjectListView"), Description("The size of the button when the SizingMode is FixedBounds"), DefaultValue(null)] public Size? ButtonSize { get { return this.buttonSize; } set { this.buttonSize = value; } } private Size? buttonSize; /// /// Gets or sets the extra space that surrounds the cell when the SizingMode is TextBounds /// [Category("ObjectListView"), Description("The extra space that surrounds the cell when the SizingMode is TextBounds")] public Size? ButtonPadding { get { return this.buttonPadding; } set { this.buttonPadding = value; } } private Size? buttonPadding = new Size(10, 10); private Size ButtonPaddingOrDefault { get { return this.ButtonPadding ?? new Size(10, 10); } } /// /// Gets or sets the maximum width that a button can occupy. /// -1 means there is no maximum width. /// /// This is only considered when the SizingMode is TextBounds [Category("ObjectListView"), Description("The maximum width that a button can occupy when the SizingMode is TextBounds"), DefaultValue(-1)] public int MaxButtonWidth { get { return this.maxButtonWidth; } set { this.maxButtonWidth = value; } } private int maxButtonWidth = -1; /// /// Gets or sets the minimum width that a button can occupy. /// -1 means there is no minimum width. /// /// This is only considered when the SizingMode is TextBounds [Category("ObjectListView"), Description("The minimum width that a button can be when the SizingMode is TextBounds"), DefaultValue(-1)] public int MinButtonWidth { get { return this.minButtonWidth; } set { this.minButtonWidth = value; } } private int minButtonWidth = -1; /// ///获取或设置此列按钮的字体颜色(如果有按钮的话) /// [Category("ObjectListView"), Description("获取或设置此列按钮的字体颜色(如果有按钮的话)")] public Color ButtonForeColor { get; set; } = Color.White; /// ///获取或设置此列按钮的背景颜色(如果有按钮的话) /// [Category("ObjectListView"), Description("获取或设置此列按钮的背景颜色(如果有按钮的话)")] public Color ButtonBaseColor { get; set; } = Color.Green; /// ///获取或设置此列按钮的边框颜色(如果有按钮的话) /// [Category("ObjectListView"), Description("获取或设置此列按钮的边框颜色(如果有按钮的话)")] public Color ButtonBorderColor { get; set; } = Color.Gray; /// ///获取或设置此列按钮的内边框颜色(如果有按钮的话) /// [Category("ObjectListView"), Description("获取或设置此列按钮的内边框颜色(如果有按钮的话)")] public Color ButtonInnerBorderColor { get; set; } = Color.Gray; /// ///获取或设置此列按钮是否画边框(如果有按钮的话) /// [Category("ObjectListView"), Description("获取或设置此列按钮是否画边框(如果有按钮的话)")] public bool ButtonDrawBorder { get; set; } =true; #endregion #region Rendering /// /// Calculate the size of the contents /// /// /// /// protected override Size CalculateContentSize(Graphics g, Rectangle r) { if (this.SizingMode == OLVColumn.ButtonSizingMode.CellBounds) return r.Size; if (this.SizingMode == OLVColumn.ButtonSizingMode.FixedBounds) return this.ButtonSize ?? r.Size; // Ok, SizingMode must be TextBounds. So figure out the size of the text Size textSize = this.CalculateTextSize(g, this.GetText(), r.Width); // Allow for padding and max width textSize.Height += this.ButtonPaddingOrDefault.Height * 2; textSize.Width += this.ButtonPaddingOrDefault.Width * 2; if (this.MaxButtonWidth != -1 && textSize.Width > this.MaxButtonWidth) textSize.Width = this.MaxButtonWidth; if (textSize.Width < this.MinButtonWidth) textSize.Width = this.MinButtonWidth; return textSize; } /// /// Draw the button /// /// /// protected override void DrawImageAndText(Graphics g, Rectangle r) { TextFormatFlags textFormatFlags = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding | TextFormatFlags.SingleLine | TextFormatFlags.PreserveGraphicsTranslateTransform; if (this.ListView.RightToLeftLayout) textFormatFlags |= TextFormatFlags.RightToLeft; string buttonText = GetText(); //g.DrawString(buttonText, this.Font,Brushes.Black,new PointF(r.X,r.Y)); if (!String.IsNullOrEmpty(buttonText)) { var button_state = CalculatePushButtonState(); var baseColor = ButtonBaseColor; if (button_state == PushButtonState.Normal) { baseColor = ButtonBaseColor; } else if (button_state == PushButtonState.Hot) { baseColor = Rendering.Util_GDI.GetColor(ButtonBaseColor, 0, -35, -24, -30); } else if (button_state == PushButtonState.Pressed) { baseColor = Rendering.Util_GDI.GetColor(ButtonBaseColor, 0, -35, -24, -9); } else if (button_state == PushButtonState.Disabled) { baseColor = SystemColors.ControlDark; } Rendering.Util_GDI.DrawButtonX(buttonText, g, r, this.ButtonForeColor, baseColor, ButtonBorderColor, ButtonInnerBorderColor, ryControls.RoundStyle.All, 5, ButtonDrawBorder); //Rendering.Util_GDI.DrawRoundButton(buttonText, g, r, Rendering.buttonStyle.ButtonNormal); //ButtonRenderer.DrawButton(g, r, buttonText, this.Font, textFormatFlags, false, CalculatePushButtonState()); } } /// /// What part of the control is under the given point? /// /// /// /// /// /// protected override void StandardHitTest(Graphics g, OlvListViewHitTestInfo hti, Rectangle bounds, int x, int y) { Rectangle r = ApplyCellPadding(bounds); if (r.Contains(x, y)) hti.HitTestLocation = HitTestLocation.Button; } /// /// What is the state of the button? /// /// protected PushButtonState CalculatePushButtonState() { if (!this.ListItem.Enabled && !this.Column.EnableButtonWhenItemIsDisabled) return PushButtonState.Disabled; if (this.IsButtonHot) return ObjectListView.IsLeftMouseDown ? PushButtonState.Pressed : PushButtonState.Hot; return PushButtonState.Normal; } /// /// Is the mouse over the button? /// protected bool IsButtonHot { get { return this.IsCellHot && this.ListView.HotCellHitLocation == HitTestLocation.Button; } } #endregion } }