using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Data; using System.Windows.Forms; namespace TradeIdeas.TIProGUI.Charting.Controls.ColorHexagonPicker { [DefaultProperty("Color")] [DefaultEvent("ColorChanged")] public partial class ColorWheel: Control { private const int PADDING = 10; private const int INNER_RADIUS = 200; private const int OUTER_RADIUS = INNER_RADIUS + 50; #region Fields private Brush _brush; private PointF _centerPoint; private Color _color; private int _colorStep; private bool _dragStartedWithinWheel; private HslColor _hslColor; private int _largeChange; private float _radius; private int _selectionSize; private int _smallChange; private int _updateCount; #endregion #region Constructors /// /// Initializes a new instance of the class. /// public ColorWheel() { this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.Selectable | ControlStyles.StandardClick | ControlStyles.StandardDoubleClick, true); this.Color = Color.Black; this.ColorStep = 4; this.SelectionSize = 10; this.SmallChange = 1; this.LargeChange = 5; this.SelectionGlyph = this.CreateSelectionGlyph(); } #endregion #region Events /// /// Occurs when the Color property value changes /// [Category("Property Changed")] public event EventHandler ColorChanged; /// /// Occurs when the ColorStep property value changes /// [Category("Property Changed")] public event EventHandler ColorStepChanged; /// /// Occurs when the HslColor property value changes /// [Category("Property Changed")] public event EventHandler HslColorChanged; /// /// Occurs when the LargeChange property value changes /// [Category("Property Changed")] public event EventHandler LargeChangeChanged; /// /// Occurs when the SelectionSize property value changes /// [Category("Property Changed")] public event EventHandler SelectionSizeChanged; /// /// Occurs when the SmallChange property value changes /// [Category("Property Changed")] public event EventHandler SmallChangeChanged; #endregion #region Overridden Methods /// /// Releases the unmanaged resources used by the and its child controls and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (disposing) { if (_brush != null) { _brush.Dispose(); } if (this.SelectionGlyph != null) { this.SelectionGlyph.Dispose(); } } base.Dispose(disposing); } /// /// Determines whether the specified key is a regular input key or a special key that requires preprocessing. /// /// One of the values. /// true if the specified key is a regular input key; otherwise, false. protected override bool IsInputKey(Keys keyData) { bool result; if ((keyData & Keys.Left) == Keys.Left || (keyData & Keys.Up) == Keys.Up || (keyData & Keys.Down) == Keys.Down || (keyData & Keys.Right) == Keys.Right || (keyData & Keys.PageUp) == Keys.PageUp || (keyData & Keys.PageDown) == Keys.PageDown) { result = true; } else { result = base.IsInputKey(keyData); } return result; } /// /// Raises the event. /// /// An that contains the event data. protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); this.Invalidate(); } /// /// Raises the event. /// /// A that contains the event data. protected override void OnKeyDown(KeyEventArgs e) { HslColor color; double hue; int step; color = this.HslColor; hue = color.H; step = e.Shift ? this.LargeChange : this.SmallChange; switch (e.KeyCode) { case Keys.Right: case Keys.Up: hue += step; break; case Keys.Left: case Keys.Down: hue -= step; break; case Keys.PageUp: hue += this.LargeChange; break; case Keys.PageDown: hue -= this.LargeChange; break; } if (hue >= 360) { hue = 0; } if (hue < 0) { hue = 359; } if (hue != color.H) { color.H = hue; // As the Color and HslColor properties update each other, need to temporarily disable this and manually set both // otherwise the wheel "sticks" due to imprecise conversion from RGB to HSL this.LockUpdates = true; this.Color = color.ToRgbColor(); this.HslColor = color; this.LockUpdates = false; e.Handled = true; } base.OnKeyDown(e); } /// /// Raises the event. /// /// An that contains the event data. protected override void OnLostFocus(EventArgs e) { base.OnLostFocus(e); this.Invalidate(); } /// /// Raises the event. /// /// A that contains the event data. protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (!this.Focused && this.TabStop) { this.Focus(); } if (e.Button == MouseButtons.Left && this.IsPointInWheel(e.Location)) { _dragStartedWithinWheel = true; this.SetColor(e.Location); } } /// /// Raises the event. /// /// A that contains the event data. protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (e.Button == MouseButtons.Left && _dragStartedWithinWheel) { this.SetColor(e.Location); } } /// /// Raises the event. /// /// A that contains the event data. protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _dragStartedWithinWheel = false; } /// /// Raises the event. /// /// A that contains the event data. protected override void OnPaddingChanged(EventArgs e) { base.OnPaddingChanged(e); this.RefreshWheel(); } /// /// Raises the event. /// /// A that contains the event data. protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (this.AllowPainting) { base.OnPaintBackground(e); if (this.BackgroundImage == null && this.Parent != null && (this.BackColor == this.Parent.BackColor || this.Parent.BackColor.A != 255)) { ButtonRenderer.DrawParentBackground(e.Graphics, this.DisplayRectangle, this); } if (_brush != null) { e.Graphics.FillPie(_brush, this.ClientRectangle, 0, 360); } // smooth out the edge of the wheel e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; using (Pen pen = new Pen(this.BackColor, 2)) { e.Graphics.DrawEllipse(pen, new RectangleF(_centerPoint.X - _radius, _centerPoint.Y - _radius, _radius * 2, _radius * 2)); } if (!this.Color.IsEmpty) { this.PaintCurrentColor(e); } } } /// /// Raises the event. /// /// An that contains the event data. protected override void OnResize(EventArgs e) { base.OnResize(e); this.RefreshWheel(); } #endregion #region Public Properties /// /// Gets or sets the component color. /// /// The component color. [Category("Appearance")] [DefaultValue(typeof(Color), "Black")] public virtual Color Color { get { return _color; } set { if (this.Color != value) { _color = value; this.OnColorChanged(EventArgs.Empty); } } } /// /// Gets or sets the increment for rendering the color wheel. /// /// The color step. /// Value must be between 1 and 359 [Category("Appearance")] [DefaultValue(4)] public virtual int ColorStep { get { return _colorStep; } set { if (value < 1 || value > 359) { throw new ArgumentOutOfRangeException("value", value, "Value must be between 1 and 359"); } if (this.ColorStep != value) { _colorStep = value; this.OnColorStepChanged(EventArgs.Empty); } } } /// /// Gets or sets the component color. /// /// The component color. [Category("Appearance")] [DefaultValue(typeof(HslColor), "0, 0, 0")] [Browsable(false) /* disable editing until I write a proper type convertor */] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual HslColor HslColor { get { return _hslColor; } set { if (this.HslColor != value) { _hslColor = value; this.OnHslColorChanged(EventArgs.Empty); } } } /// /// Gets or sets a value to be added to or subtracted from the property when the wheel selection is moved a large distance. /// /// A numeric value. The default value is 5. [Category("Behavior")] [DefaultValue(5)] public virtual int LargeChange { get { return _largeChange; } set { if (this.LargeChange != value) { _largeChange = value; this.OnLargeChangeChanged(EventArgs.Empty); } } } /// /// Gets or sets the size of the selection handle. /// /// The size of the selection handle. [Category("Appearance")] [DefaultValue(10)] public virtual int SelectionSize { get { return _selectionSize; } set { if (this.SelectionSize != value) { _selectionSize = value; this.OnSelectionSizeChanged(EventArgs.Empty); } } } /// /// Gets or sets a value to be added to or subtracted from the property when the wheel selection is moved a small distance. /// /// A numeric value. The default value is 1. [Category("Behavior")] [DefaultValue(1)] public virtual int SmallChange { get { return _smallChange; } set { if (this.SmallChange != value) { _smallChange = value; this.OnSmallChangeChanged(EventArgs.Empty); } } } #endregion #region Protected Properties /// /// Gets a value indicating whether painting of the control is allowed. /// /// /// true if painting of the control is allowed; otherwise, false. /// protected virtual bool AllowPainting { get { return _updateCount == 0; } } protected Color[] Colors { get; set; } protected bool LockUpdates { get; set; } protected PointF[] Points { get; set; } protected Image SelectionGlyph { get; set; } #endregion #region Public Members /// /// Disables any redrawing of the image box /// public virtual void BeginUpdate() { _updateCount++; } /// /// Enables the redrawing of the image box /// public virtual void EndUpdate() { if (_updateCount > 0) { _updateCount--; } if (this.AllowPainting) { this.Invalidate(); } } #endregion #region Protected Members /// /// Calculates wheel attributes. /// protected virtual void CalculateWheel() { List points; List colors; points = new List(); colors = new List(); // Only define the points if the control is above a minimum size, otherwise if it's too small, // you get an "out of memory" exceptions (of all things) when creating the brush if (this.ClientSize.Width > 16 && this.ClientSize.Height > 16) { int w; int h; w = this.ClientSize.Width; h = this.ClientSize.Height; _centerPoint = new PointF(w / 2.0F, h / 2.0F); _radius = this.GetRadius(_centerPoint); for (double angle = 0; angle < 360; angle += this.ColorStep) { double angleR; PointF location; angleR = angle * (Math.PI / 180); location = this.GetColorLocation(angleR, _radius); points.Add(location); colors.Add(new HslColor(angle, 1.0, 0.5).ToRgbColor()); } } this.Points = points.ToArray(); this.Colors = colors.ToArray(); } /// /// Creates the gradient brush used to paint the wheel. /// protected virtual Brush CreateGradientBrush() { Brush result; if (this.Points.Length != 0 && this.Points.Length == this.Colors.Length) { result = new PathGradientBrush(this.Points, WrapMode.Clamp) { CenterPoint = _centerPoint, CenterColor = Color.White, SurroundColors = this.Colors }; } else { result = null; } return result; } /// /// Creates the selection glyph. /// protected virtual Image CreateSelectionGlyph() { Image image; int halfSize; halfSize = this.SelectionSize / 2; image = new Bitmap(this.SelectionSize + 1, this.SelectionSize + 1); using (Graphics g = Graphics.FromImage(image)) { Point[] diamondOuter; diamondOuter = new[] { new Point(halfSize, 0), new Point(this.SelectionSize, halfSize), new Point(halfSize, this.SelectionSize), new Point(0, halfSize) }; g.FillPolygon(SystemBrushes.Control, diamondOuter); g.DrawPolygon(SystemPens.ControlDark, diamondOuter); using (Pen pen = new Pen(Color.FromArgb(128, SystemColors.ControlDark))) { g.DrawLine(pen, halfSize, 1, this.SelectionSize - 1, halfSize); g.DrawLine(pen, halfSize, 2, this.SelectionSize - 2, halfSize); g.DrawLine(pen, halfSize, this.SelectionSize - 1, this.SelectionSize - 2, halfSize + 1); g.DrawLine(pen, halfSize, this.SelectionSize - 2, this.SelectionSize - 3, halfSize + 1); } using (Pen pen = new Pen(Color.FromArgb(196, SystemColors.ControlLightLight))) { g.DrawLine(pen, halfSize, this.SelectionSize - 1, 1, halfSize); } g.DrawLine(SystemPens.ControlLightLight, 1, halfSize, halfSize, 1); } return image; } /// /// Gets the point within the wheel representing the source color. /// /// The color. protected PointF GetColorLocation(Color color) { return this.GetColorLocation(new HslColor(color)); } /// /// Gets the point within the wheel representing the source color. /// /// The color. protected virtual PointF GetColorLocation(HslColor color) { double angle; double radius; angle = color.H * Math.PI / 180; radius = _radius * color.S; return this.GetColorLocation(angle, radius); } protected PointF GetColorLocation(double angleR, double radius) { double x; double y; x = this.Padding.Left + _centerPoint.X + Math.Cos(angleR) * radius; y = this.Padding.Top + _centerPoint.Y - Math.Sin(angleR) * radius; return new PointF((float)x, (float)y); } protected float GetRadius(PointF centerPoint) { return Math.Min(centerPoint.X, centerPoint.Y) - (Math.Max(this.Padding.Horizontal, this.Padding.Vertical) + (this.SelectionSize / 2)); } /// /// Determines whether the specified point is within the bounds of the color wheel. /// /// The point. /// true if the specified point is within the bounds of the color wheel; otherwise, false. protected bool IsPointInWheel(Point point) { PointF normalized; // http://my.safaribooksonline.com/book/programming/csharp/9780672331985/graphics-with-windows-forms-and-gdiplus/ch17lev1sec21 normalized = new PointF(point.X - _centerPoint.X, point.Y - _centerPoint.Y); return (normalized.X * normalized.X + normalized.Y * normalized.Y) <= (_radius * _radius); } /// /// Raises the event. /// /// The instance containing the event data. protected virtual void OnColorChanged(EventArgs e) { EventHandler handler; if (!this.LockUpdates) { this.HslColor = new HslColor(this.Color); } this.Refresh(); handler = this.ColorChanged; if (handler != null) { handler(this, e); } } /// /// Raises the event. /// /// The instance containing the event data. protected virtual void OnColorStepChanged(EventArgs e) { EventHandler handler; this.RefreshWheel(); handler = this.ColorStepChanged; if (handler != null) { handler(this, e); } } /// /// Raises the event. /// /// The instance containing the event data. protected virtual void OnHslColorChanged(EventArgs e) { EventHandler handler; if (!this.LockUpdates) { this.Color = this.HslColor.ToRgbColor(); } this.Invalidate(); handler = this.HslColorChanged; if (handler != null) { handler(this, e); } } /// /// Raises the event. /// /// The instance containing the event data. protected virtual void OnLargeChangeChanged(EventArgs e) { EventHandler handler; handler = this.LargeChangeChanged; if (handler != null) { handler(this, e); } } /// /// Raises the event. /// /// The instance containing the event data. protected virtual void OnSelectionSizeChanged(EventArgs e) { EventHandler handler; if (this.SelectionGlyph != null) { this.SelectionGlyph.Dispose(); } this.SelectionGlyph = this.CreateSelectionGlyph(); this.RefreshWheel(); handler = this.SelectionSizeChanged; if (handler != null) { handler(this, e); } } /// /// Raises the event. /// /// The instance containing the event data. protected virtual void OnSmallChangeChanged(EventArgs e) { EventHandler handler; handler = this.SmallChangeChanged; if (handler != null) { handler(this, e); } } protected void PaintColor(PaintEventArgs e, HslColor color) { this.PaintColor(e, color, false); } protected virtual void PaintColor(PaintEventArgs e, HslColor color, bool includeFocus) { PointF location; location = this.GetColorLocation(color); if (!float.IsNaN(location.X) && !float.IsNaN(location.Y)) { int x; int y; x = (int)location.X - (this.SelectionSize / 2); y = (int)location.Y - (this.SelectionSize / 2); if (this.SelectionGlyph == null) { e.Graphics.DrawRectangle(Pens.Black, x, y, this.SelectionSize, this.SelectionSize); } else { e.Graphics.DrawImage(this.SelectionGlyph, x, y); } if (this.Focused && includeFocus) { ControlPaint.DrawFocusRectangle(e.Graphics, new Rectangle(x - 1, y - 1, this.SelectionSize + 2, this.SelectionSize + 2)); } } } protected virtual void PaintCurrentColor(PaintEventArgs e) { this.PaintColor(e, this.HslColor, true); } protected virtual void SetColor(Point point) { double dx; double dy; double angle; double distance; double saturation; dx = Math.Abs(point.X - _centerPoint.X - this.Padding.Left); dy = Math.Abs(point.Y - _centerPoint.Y - this.Padding.Top); angle = Math.Atan(dy / dx) / Math.PI * 180; distance = Math.Pow((Math.Pow(dx, 2) + (Math.Pow(dy, 2))), 0.5); saturation = distance / _radius; if (distance < 6) { saturation = 0; // snap to center } if (point.X < _centerPoint.X) { angle = 180 - angle; } if (point.Y > _centerPoint.Y) { angle = 360 - angle; } this.LockUpdates = true; this.HslColor = new HslColor(angle, saturation, 0.5); this.Color = this.HslColor.ToRgbColor(); this.LockUpdates = false; } #endregion #region Private Members /// /// Refreshes the wheel attributes and then repaints the control /// private void RefreshWheel() { if (_brush != null) { _brush.Dispose(); } this.CalculateWheel(); _brush = this.CreateGradientBrush(); this.Invalidate(); } #endregion } }