/* * Adornments - Adornments are the basis for overlays and decorations -- things that can be rendered over the top of a ListView * * Author: Phillip Piper * Date: 16/08/2009 1:02 AM * * Change log: * v2.6 * 2012-08-18 JPP - Correctly dispose of brush and pen resources * v2.3 * 2009-09-22 JPP - Added Wrap property to TextAdornment, to allow text wrapping to be disabled * - Added ShrinkToWidth property to ImageAdornment * 2009-08-17 JPP - Initial version * * To do: * - Use IPointLocator rather than Corners * - Add RotationCenter property ratherr than always using middle center * * Copyright (C) 2009-2014 Phillip Piper * * 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.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace BrightIdeasSoftware { /// /// An adorment is the common base for overlays and decorations. /// public class GraphicAdornment { #region Public properties /// /// Gets or sets the corner of the adornment that will be positioned at the reference corner /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public System.Drawing.ContentAlignment AdornmentCorner { get { return this.adornmentCorner; } set { this.adornmentCorner = value; } } private System.Drawing.ContentAlignment adornmentCorner = System.Drawing.ContentAlignment.MiddleCenter; /// /// Gets or sets location within the reference rectange where the adornment will be drawn /// /// This is a simplied interface to ReferenceCorner and AdornmentCorner [Category("ObjectListView"), Description("How will the adornment be aligned"), DefaultValue(System.Drawing.ContentAlignment.BottomRight), NotifyParentProperty(true)] public System.Drawing.ContentAlignment Alignment { get { return this.alignment; } set { this.alignment = value; this.ReferenceCorner = value; this.AdornmentCorner = value; } } private System.Drawing.ContentAlignment alignment = System.Drawing.ContentAlignment.BottomRight; /// /// Gets or sets the offset by which the position of the adornment will be adjusted /// [Category("ObjectListView"), Description("The offset by which the position of the adornment will be adjusted"), DefaultValue(typeof(Size), "0,0")] public Size Offset { get { return this.offset; } set { this.offset = value; } } private Size offset = new Size(); /// /// Gets or sets the point of the reference rectangle to which the adornment will be aligned. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public System.Drawing.ContentAlignment ReferenceCorner { get { return this.referenceCorner; } set { this.referenceCorner = value; } } private System.Drawing.ContentAlignment referenceCorner = System.Drawing.ContentAlignment.MiddleCenter; /// /// Gets or sets the degree of rotation by which the adornment will be transformed. /// The centre of rotation will be the center point of the adornment. /// [Category("ObjectListView"), Description("The degree of rotation that will be applied to the adornment."), DefaultValue(0), NotifyParentProperty(true)] public int Rotation { get { return this.rotation; } set { this.rotation = value; } } private int rotation; /// /// Gets or sets the transparency of the overlay. /// 0 is completely transparent, 255 is completely opaque. /// [Category("ObjectListView"), Description("The transparency of this adornment. 0 is completely transparent, 255 is completely opaque."), DefaultValue(128)] public int Transparency { get { return this.transparency; } set { this.transparency = Math.Min(255, Math.Max(0, value)); } } private int transparency = 128; #endregion #region Calculations /// /// Calculate the location of rectangle of the given size, /// so that it's indicated corner would be at the given point. /// /// The point /// /// Which corner will be positioned at the reference point /// /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.TopLeft) -> Point(50, 100) /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.MiddleCenter) -> Point(45, 90) /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.BottomRight) -> Point(40, 80) public virtual Point CalculateAlignedPosition(Point pt, Size size, System.Drawing.ContentAlignment corner) { switch (corner) { case System.Drawing.ContentAlignment.TopLeft: return pt; case System.Drawing.ContentAlignment.TopCenter: return new Point(pt.X - (size.Width / 2), pt.Y); case System.Drawing.ContentAlignment.TopRight: return new Point(pt.X - size.Width, pt.Y); case System.Drawing.ContentAlignment.MiddleLeft: return new Point(pt.X, pt.Y - (size.Height / 2)); case System.Drawing.ContentAlignment.MiddleCenter: return new Point(pt.X - (size.Width / 2), pt.Y - (size.Height / 2)); case System.Drawing.ContentAlignment.MiddleRight: return new Point(pt.X - size.Width, pt.Y - (size.Height / 2)); case System.Drawing.ContentAlignment.BottomLeft: return new Point(pt.X, pt.Y - size.Height); case System.Drawing.ContentAlignment.BottomCenter: return new Point(pt.X - (size.Width / 2), pt.Y - size.Height); case System.Drawing.ContentAlignment.BottomRight: return new Point(pt.X - size.Width, pt.Y - size.Height); } // Should never reach here return pt; } /// /// Calculate a rectangle that has the given size which is positioned so that /// its alignment point is at the reference location of the given rect. /// /// /// /// public virtual Rectangle CreateAlignedRectangle(Rectangle r, Size sz) { return this.CreateAlignedRectangle(r, sz, this.ReferenceCorner, this.AdornmentCorner, this.Offset); } /// /// Create a rectangle of the given size which is positioned so that /// its indicated corner is at the indicated corner of the reference rect. /// /// /// /// /// /// /// /// /// Creates a rectangle so that its bottom left is at the centre of the reference: /// corner=BottomLeft, referenceCorner=MiddleCenter /// This is a powerful concept that takes some getting used to, but is /// very neat once you understand it. /// public virtual Rectangle CreateAlignedRectangle(Rectangle r, Size sz, System.Drawing.ContentAlignment corner, System.Drawing.ContentAlignment referenceCorner, Size offset) { Point referencePt = this.CalculateCorner(r, referenceCorner); Point topLeft = this.CalculateAlignedPosition(referencePt, sz, corner); return new Rectangle(topLeft + offset, sz); } /// /// Return the point at the indicated corner of the given rectangle (it doesn't /// have to be a corner, but a named location) /// /// The reference rectangle /// Which point of the rectangle should be returned? /// A point /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.TopLeft) -> Point(0, 0) /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.MiddleCenter) -> Point(25, 50) /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.BottomRight) -> Point(50, 100) public virtual Point CalculateCorner(Rectangle r, System.Drawing.ContentAlignment corner) { switch (corner) { case System.Drawing.ContentAlignment.TopLeft: return new Point(r.Left, r.Top); case System.Drawing.ContentAlignment.TopCenter: return new Point(r.X + (r.Width / 2), r.Top); case System.Drawing.ContentAlignment.TopRight: return new Point(r.Right, r.Top); case System.Drawing.ContentAlignment.MiddleLeft: return new Point(r.Left, r.Top + (r.Height / 2)); case System.Drawing.ContentAlignment.MiddleCenter: return new Point(r.X + (r.Width / 2), r.Top + (r.Height / 2)); case System.Drawing.ContentAlignment.MiddleRight: return new Point(r.Right, r.Top + (r.Height / 2)); case System.Drawing.ContentAlignment.BottomLeft: return new Point(r.Left, r.Bottom); case System.Drawing.ContentAlignment.BottomCenter: return new Point(r.X + (r.Width / 2), r.Bottom); case System.Drawing.ContentAlignment.BottomRight: return new Point(r.Right, r.Bottom); } // Should never reach here return r.Location; } /// /// Given the item and the subitem, calculate its bounds. /// /// /// /// public virtual Rectangle CalculateItemBounds(OLVListItem item, OLVListSubItem subItem) { if (item == null) return Rectangle.Empty; if (subItem == null) return item.Bounds; return item.GetSubItemBounds(item.SubItems.IndexOf(subItem)); } #endregion #region Commands /// /// Apply any specified rotation to the Graphic content. /// /// The Graphics to be transformed /// The rotation will be around the centre of this rect protected virtual void ApplyRotation(Graphics g, Rectangle r) { if (this.Rotation == 0) return; // THINK: Do we want to reset the transform? I think we want to push a new transform g.ResetTransform(); Matrix m = new Matrix(); m.RotateAt(this.Rotation, new Point(r.Left + r.Width / 2, r.Top + r.Height / 2)); g.Transform = m; } /// /// Reverse the rotation created by ApplyRotation() /// /// protected virtual void UnapplyRotation(Graphics g) { if (this.Rotation != 0) g.ResetTransform(); } #endregion } /// /// An overlay that will draw an image over the top of the ObjectListView /// public class ImageAdornment : GraphicAdornment { #region Public properties /// /// Gets or sets the image that will be drawn /// [Category("ObjectListView"), Description("The image that will be drawn"), DefaultValue(null), NotifyParentProperty(true)] public Image Image { get { return this.image; } set { this.image = value; } } private Image image; /// /// Gets or sets if the image will be shrunk to fit with its horizontal bounds /// [Category("ObjectListView"), Description("Will the image be shrunk to fit within its width?"), DefaultValue(false)] public bool ShrinkToWidth { get { return this.shrinkToWidth; } set { this.shrinkToWidth = value; } } private bool shrinkToWidth; #endregion #region Commands /// /// Draw the image in its specified location /// /// The Graphics used for drawing /// The bounds of the rendering public virtual void DrawImage(Graphics g, Rectangle r) { if (this.ShrinkToWidth) this.DrawScaledImage(g, r, this.Image, this.Transparency); else this.DrawImage(g, r, this.Image, this.Transparency); } /// /// Draw the image in its specified location /// /// The image to be drawn /// The Graphics used for drawing /// The bounds of the rendering /// How transparent should the image be (0 is completely transparent, 255 is opaque) public virtual void DrawImage(Graphics g, Rectangle r, Image image, int transparency) { if (image != null) this.DrawImage(g, r, image, image.Size, transparency); } /// /// Draw the image in its specified location /// /// The image to be drawn /// The Graphics used for drawing /// The bounds of the rendering /// How big should the image be? /// How transparent should the image be (0 is completely transparent, 255 is opaque) public virtual void DrawImage(Graphics g, Rectangle r, Image image, Size sz, int transparency) { if (image == null) return; Rectangle adornmentBounds = this.CreateAlignedRectangle(r, sz); try { this.ApplyRotation(g, adornmentBounds); this.DrawTransparentBitmap(g, adornmentBounds, image, transparency); } finally { this.UnapplyRotation(g); } } /// /// Draw the image in its specified location, scaled so that it is not wider /// than the given rectangle. Height is scaled proportional to the width. /// /// The image to be drawn /// The Graphics used for drawing /// The bounds of the rendering /// How transparent should the image be (0 is completely transparent, 255 is opaque) public virtual void DrawScaledImage(Graphics g, Rectangle r, Image image, int transparency) { if (image == null) return; // If the image is too wide to be drawn in the space provided, proportionally scale it down. // Too tall images are not scaled. Size size = image.Size; if (image.Width > r.Width) { float scaleRatio = (float)r.Width / (float)image.Width; size.Height = (int)((float)image.Height * scaleRatio); size.Width = r.Width - 1; } this.DrawImage(g, r, image, size, transparency); } /// /// Utility to draw a bitmap transparenly. /// /// /// /// /// protected virtual void DrawTransparentBitmap(Graphics g, Rectangle r, Image image, int transparency) { ImageAttributes imageAttributes = null; if (transparency != 255) { imageAttributes = new ImageAttributes(); float a = (float)transparency / 255.0f; float[][] colorMatrixElements = { new float[] {1, 0, 0, 0, 0}, new float[] {0, 1, 0, 0, 0}, new float[] {0, 0, 1, 0, 0}, new float[] {0, 0, 0, a, 0}, new float[] {0, 0, 0, 0, 1}}; imageAttributes.SetColorMatrix(new ColorMatrix(colorMatrixElements)); } g.DrawImage(image, r, // destination rectangle 0, 0, image.Size.Width, image.Size.Height, // source rectangle GraphicsUnit.Pixel, imageAttributes); } #endregion } /// /// An adornment that will draw text /// public class TextAdornment : GraphicAdornment { #region Public properties /// /// Gets or sets the background color of the text /// Set this to Color.Empty to not draw a background /// [Category("ObjectListView"), Description("The background color of the text"), DefaultValue(typeof(Color), "")] public Color BackColor { get { return this.backColor; } set { this.backColor = value; } } private Color backColor = Color.Empty; /// /// Gets the brush that will be used to paint the text /// [Browsable(false)] public Brush BackgroundBrush { get { return new SolidBrush(Color.FromArgb(this.workingTransparency, this.BackColor)); } } /// /// Gets or sets the color of the border around the billboard. /// Set this to Color.Empty to remove the border /// [Category("ObjectListView"), Description("The color of the border around the text"), DefaultValue(typeof(Color), "")] public Color BorderColor { get { return this.borderColor; } set { this.borderColor = value; } } private Color borderColor = Color.Empty; /// /// Gets the brush that will be used to paint the text /// [Browsable(false)] public Pen BorderPen { get { return new Pen(Color.FromArgb(this.workingTransparency, this.BorderColor), this.BorderWidth); } } /// /// Gets or sets the width of the border around the text /// [Category("ObjectListView"), Description("The width of the border around the text"), DefaultValue(0.0f)] public float BorderWidth { get { return this.borderWidth; } set { this.borderWidth = value; } } private float borderWidth; /// /// How rounded should the corners of the border be? 0 means no rounding. /// /// If this value is too large, the edges of the border will appear odd. [Category("ObjectListView"), Description("How rounded should the corners of the border be? 0 means no rounding."), DefaultValue(16.0f), NotifyParentProperty(true)] public float CornerRounding { get { return this.cornerRounding; } set { this.cornerRounding = value; } } private float cornerRounding = 16.0f; /// /// Gets or sets the font that will be used to draw the text /// [Category("ObjectListView"), Description("The font that will be used to draw the text"), DefaultValue(null), NotifyParentProperty(true)] public Font Font { get { return this.font; } set { this.font = value; } } private Font font; /// /// Gets the font that will be used to draw the text or a reasonable default /// [Browsable(false)] public Font FontOrDefault { get { return this.Font ?? new Font("Tahoma", 16); } } /// /// Does this text have a background? /// [Browsable(false)] public bool HasBackground { get { return this.BackColor != Color.Empty; } } /// /// Does this overlay have a border? /// [Browsable(false)] public bool HasBorder { get { return this.BorderColor != Color.Empty && this.BorderWidth > 0; } } /// /// Gets or sets the maximum width of the text. Text longer than this will wrap. /// 0 means no maximum. /// [Category("ObjectListView"), Description("The maximum width the text (0 means no maximum). Text longer than this will wrap"), DefaultValue(0)] public int MaximumTextWidth { get { return this.maximumTextWidth; } set { this.maximumTextWidth = value; } } private int maximumTextWidth; /// /// Gets or sets the formatting that should be used on the text /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual StringFormat StringFormat { get { if (this.stringFormat == null) { this.stringFormat = new StringFormat(); this.stringFormat.Alignment = StringAlignment.Center; this.stringFormat.LineAlignment = StringAlignment.Center; this.stringFormat.Trimming = StringTrimming.EllipsisCharacter; if (!this.Wrap) this.stringFormat.FormatFlags = StringFormatFlags.NoWrap; } return this.stringFormat; } set { this.stringFormat = value; } } private StringFormat stringFormat; /// /// Gets or sets the text that will be drawn /// [Category("ObjectListView"), Description("The text that will be drawn over the top of the ListView"), DefaultValue(null), NotifyParentProperty(true), Localizable(true)] public string Text { get { return this.text; } set { this.text = value; } } private string text; /// /// Gets the brush that will be used to paint the text /// [Browsable(false)] public Brush TextBrush { get { return new SolidBrush(Color.FromArgb(this.workingTransparency, this.TextColor)); } } /// /// Gets or sets the color of the text /// [Category("ObjectListView"), Description("The color of the text"), DefaultValue(typeof(Color), "DarkBlue"), NotifyParentProperty(true)] public Color TextColor { get { return this.textColor; } set { this.textColor = value; } } private Color textColor = Color.DarkBlue; /// /// Gets or sets whether the text will wrap when it exceeds its bounds /// [Category("ObjectListView"), Description("Will the text wrap?"), DefaultValue(true)] public bool Wrap { get { return this.wrap; } set { this.wrap = value; } } private bool wrap = true; #endregion #region Implementation /// /// Draw our text with our stored configuration in relation to the given /// reference rectangle /// /// The Graphics used for drawing /// The reference rectangle in relation to which the text will be drawn public virtual void DrawText(Graphics g, Rectangle r) { this.DrawText(g, r, this.Text, this.Transparency); } /// /// Draw the given text with our stored configuration /// /// The Graphics used for drawing /// The reference rectangle in relation to which the text will be drawn /// The text to draw /// How opaque should be text be public virtual void DrawText(Graphics g, Rectangle r, string s, int transparency) { if (String.IsNullOrEmpty(s)) return; Rectangle textRect = this.CalculateTextBounds(g, r, s); this.DrawBorderedText(g, textRect, s, transparency); } /// /// Draw the text with a border /// /// The Graphics used for drawing /// The bounds within which the text should be drawn /// The text to draw /// How opaque should be text be protected virtual void DrawBorderedText(Graphics g, Rectangle textRect, string text, int transparency) { Rectangle borderRect = textRect; borderRect.Inflate((int)this.BorderWidth / 2, (int)this.BorderWidth / 2); borderRect.Y -= 1; // Looker better a little higher try { this.ApplyRotation(g, textRect); using (GraphicsPath path = this.GetRoundedRect(borderRect, this.CornerRounding)) { this.workingTransparency = transparency; if (this.HasBackground) { using (Brush b = this.BackgroundBrush) g.FillPath(b, path); } using (Brush b = this.TextBrush) g.DrawString(text, this.FontOrDefault, b, textRect, this.StringFormat); if (this.HasBorder) { using (Pen p = this.BorderPen) g.DrawPath(p, path); } } } finally { this.UnapplyRotation(g); } } /// /// Return the rectangle that will be the precise bounds of the displayed text /// /// /// /// /// The bounds of the text protected virtual Rectangle CalculateTextBounds(Graphics g, Rectangle r, string s) { int maxWidth = this.MaximumTextWidth <= 0 ? r.Width : this.MaximumTextWidth; SizeF sizeF = g.MeasureString(s, this.FontOrDefault, maxWidth, this.StringFormat); Size size = new Size(1 + (int)sizeF.Width, 1 + (int)sizeF.Height); return this.CreateAlignedRectangle(r, size); } /// /// Return a GraphicPath that is a round cornered rectangle /// /// The rectangle /// The diameter of the corners /// A round cornered rectagle path /// If I could rely on people using C# 3.0+, this should be /// an extension method of GraphicsPath. protected virtual GraphicsPath GetRoundedRect(Rectangle 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 private int workingTransparency; } }