Vanara/Windows.Forms/Controls/TrackBarEx.cs

566 lines
20 KiB
C#

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.ComponentModel.Design.Serialization;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Design;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using Vanara.Extensions;
using Vanara.PInvoke;
using static Vanara.PInvoke.ComCtl32;
using static Vanara.PInvoke.Macros;
using static Vanara.PInvoke.User32;
namespace Vanara.Windows.Forms
{
/// <summary>
/// Extends the <see cref="TrackBar"/> class to provide full native-control functionality, including tick marks and value, and custom drawing.
/// </summary>
/// <seealso cref="System.Windows.Forms.TrackBar"/>
public class TrackBarEx : TrackBar
{
private bool autoTicks = true;
private int cumulativeWheelData;
private int lastValue;
private bool limitThumbToSel, showSel;
private int requestedDim;
private int selMin, selMax, thumbLength = -1;
private int[] ticks;
/// <summary>Initializes a new instance of the <see cref="TrackBarEx"/> class.</summary>
public TrackBarEx()
{
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
BackColorChanged += (o, a) => { if (IsHandleCreated) RecreateHandle(); };
requestedDim = PreferredDimension;
}
/// <summary>
/// Occurs when the channel for a <see cref="TrackBarEx"/> needs to be drawn and the <see cref="OwnerDraw"/> property is set to <c>true</c>.
/// </summary>
[Category("Drawing")]
public event PaintEventHandler DrawChannel;
/// <summary>
/// Occurs when the thumb for a <see cref="TrackBarEx"/> needs to be drawn and the <see cref="OwnerDraw"/> property is set to <c>true</c>.
/// </summary>
[Category("Drawing")]
public event PaintEventHandler DrawThumb;
/// <summary>
/// Occurs when the ticks for a <see cref="TrackBarEx"/> need to be drawn and the <see cref="OwnerDraw"/> property is set to <c>true</c>.
/// </summary>
[Category("Drawing")]
public event PaintEventHandler DrawTics;
/// <summary>Gets or sets a value indicating whether to draw ticks based on the <see cref="TrackBar.TickFrequency"/> interval.</summary>
/// <value><c>true</c> if automatic ticks should be shown; otherwise, <c>false</c>.</value>
[DefaultValue(true), Category("Appearance"), Description("Specifies whether to automatically draw tick marks at the frequency defined by TickFreqency.")]
public bool AutoTicks
{
get => autoTicks;
set
{
if (autoTicks == value) return;
autoTicks = value;
if (IsHandleCreated) RecreateHandle();
}
}
/// <summary>Gets the bounds of the channel (slider track).</summary>
/// <value>The channel bounds.</value>
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Rectangle ChannelBounds
{
get
{
var r = new RECT();
if (IsHandleCreated)
SendMsg(TrackBarMessage.TBM_GETCHANNELRECT, ref r);
return r;
}
}
/// <summary>Gets or sets a value indicating whether to limit thumb movement to the selection.</summary>
/// <value><c>true</c> if movement is limited to selection; otherwise, <c>false</c>.</value>
[DefaultValue(false), Category("Appearance"), Description("Indicates if thumb movement is limited to the selection range.")]
public bool LimitThumbToSelection
{
get => limitThumbToSel;
set
{
if (limitThumbToSel == value) return;
limitThumbToSel = value;
if (!showSel) return;
if (Value < SelectionStart)
Value = SelectionStart;
else if (Value > SelectionEnd)
Value = SelectionEnd;
}
}
/// <summary>Gets or sets a value indicating the horizontal or vertical orientation of the track bar.</summary>
[DefaultValue(Orientation.Horizontal), Category("Appearance"), Description("Indicates orientation of the trackbar.")]
public new Orientation Orientation
{
get => base.Orientation;
set
{
//valid values are 0x0 to 0x1
if (value != Orientation.Horizontal && value != Orientation.Vertical)
throw new InvalidEnumArgumentException(nameof(Orientation), (int)value, typeof(Orientation));
if (base.Orientation != value)
{
if (value == Orientation.Horizontal)
Width = requestedDim;
else
Height = requestedDim;
base.Orientation = value;
if (IsHandleCreated)
AdjustSize();
}
}
}
/// <summary>Gets or sets a value indicating whether the control is drawn by the operating system or by code that you provide.</summary>
/// <value>
/// <c>true</c> if the control is drawn by code that you provide; <c>false</c> if the control is drawn by the operating system. The
/// default is <c>false</c>.
/// </value>
[DefaultValue(false), Category("Behavior"), Description("Specifies whether to custom draw the trackbar.")]
public bool OwnerDraw { get; set; }
/// <summary>Gets or sets the upper limit of the selection range this TrackBar is working with.</summary>
/// <value>
/// The logical position at which the selection ends. This value must be less than or equal to the value of the <see
/// cref="TrackBar.Maximum"/> property.
/// </value>
[DefaultValue(0), Category("Behavior"), Description("The ending logical position of the current selection range in a trackbar.")]
public int SelectionEnd
{
get => selMax;
set => SetSelectionRange(selMin, value);
}
/// <summary>Gets or sets the lower limit of the selection range this TrackBar is working with.</summary>
/// <value>
/// The logical position at which the selection starts. This value must be greater than or equal to the value of the <see
/// cref="TrackBar.Minimum"/> property.
/// </value>
[DefaultValue(0), Category("Behavior"), Description("The starting logical position of the current selection range in a trackbar.")]
public int SelectionStart
{
get => selMin;
set => SetSelectionRange(value, selMax);
}
/// <summary>
/// Gets or sets a value indicating whether to show the selection area defined by <see cref="SelectionStart"/> and <see cref="SelectionEnd"/>.
/// </summary>
/// <value><c>true</c> if showing selection area; otherwise, <c>false</c>.</value>
[DefaultValue(false), Category("Appearance"), Description("Indicates if the TaskBar shows a selection range.")]
public bool ShowSelection
{
get => showSel;
set
{
if (showSel == value) return;
showSel = value;
if (IsHandleCreated) RecreateHandle();
}
}
/// <summary>Gets the bounds of the thumb slider in its current position.</summary>
/// <value>The thumb bounds.</value>
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Rectangle ThumbBounds
{
get
{
var r = new RECT();
if (IsHandleCreated)
SendMsg(TrackBarMessage.TBM_GETTHUMBRECT, ref r);
return r;
}
}
/// <summary>Gets or sets the length of the thumb, overriding the default.</summary>
/// <value>
/// The length of the thumb. This is the vertical length if the orientation is horizontal and the horizontal length if the
/// orientation is vertical.
/// </value>
[Category("Appearance"), Description("The length of the slider in a trackbar.")]
public int ThumbLength
{
get => IsHandleCreated ? SendMsg(TrackBarMessage.TBM_GETTHUMBLENGTH).ToInt32() : (thumbLength == -1 ? (TickStyle == TickStyle.Both ? 21 : 19) : thumbLength);
set
{
if (thumbLength == value) return;
thumbLength = value;
if (IsHandleCreated) RecreateHandle();
}
}
/// <summary>Gets or sets an array that contains the positions of the tick marks for a trackbar.</summary>
/// <value>
/// The elements of the array specify the logical positions of the trackbar's tick marks, not including the first and last tick
/// marks created by the trackbar. The logical positions can be any of the integer values in the trackbar's range of minimum to
/// maximum slider positions.
/// </value>
[Category("Appearance"), Description("Indicates the logical values of the trackbar where ticks are drawn.")]
[Browsable(false)]
//[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
//[Editor(typeof(ArrayEditor), typeof(UITypeEditor))]
//[TypeConverter(typeof(ArrayConverter))]
public int[] TickPositions
{
get
{
var tickCnt = 0;
if (!IsHandleCreated || TickStyle == TickStyle.None || 0 == (tickCnt = SendMsg(TrackBarMessage.TBM_GETNUMTICS).ToInt32()))
return new int[0];
var ptr = SendMsg(TrackBarMessage.TBM_GETPTICS);
return ptr.ToIEnum<int>(tickCnt - 2).OrderBy(i => i).Distinct().ToArray();
}
set
{
if (value != null && value.Length > 0)
{
if (TickStyle == TickStyle.None) throw new ArgumentException("Tick positions cannot be set when TickStyle is None.");
if (value.Min() < Minimum || value.Max() > Maximum) throw new ArgumentOutOfRangeException(nameof(TickPositions), "All values must be between Minimum and Maximum range values.");
}
ticks = value ?? new int[0];
if (IsHandleCreated)
RecreateHandle();
}
}
/// <inheritdoc/>
protected override CreateParams CreateParams
{
get
{
var cp = base.CreateParams;
cp.Style |= (int)TrackBarStyle.TBS_NOTIFYBEFOREMOVE;
cp.Style = showSel ? cp.Style | (int)TrackBarStyle.TBS_ENABLESELRANGE : cp.Style & ~(int)TrackBarStyle.TBS_ENABLESELRANGE;
cp.Style = BackColor == Color.Transparent ? cp.Style | (int)TrackBarStyle.TBS_TRANSPARENTBKGND : cp.Style & ~(int)TrackBarStyle.TBS_TRANSPARENTBKGND;
cp.Style = thumbLength >= 0 ? cp.Style | (int)TrackBarStyle.TBS_FIXEDLENGTH : cp.Style & ~(int)TrackBarStyle.TBS_FIXEDLENGTH;
cp.Style = autoTicks && TickStyle != TickStyle.None ? cp.Style | (int)TrackBarStyle.TBS_AUTOTICKS : cp.Style & ~(int)TrackBarStyle.TBS_AUTOTICKS;
return cp;
}
}
/// <inheritdoc/>
protected override Size DefaultSize => new Size(104, PreferredDimension);
private int PreferredDimension
{
get
{
var track = TickStyle == TickStyle.None ? 6 : (TickStyle == TickStyle.Both ? 22 : 14);
return ThumbLength + track;
}
}
/// <summary>Sets the starting and ending positions for the available selection range in a trackbar.</summary>
/// <param name="rangeMin">The starting logical position for the selection range.</param>
/// <param name="rangeMax">The ending logical position for the selection range.</param>
/// <param name="redrawAll">
/// If this parameter is TRUE, the trackbar is redrawn after the selection range is set. If this parameter is FALSE, the message
/// sets the selection range but does not redraw the trackbar.
/// </param>
public void SetSelectionRange(int rangeMin, int rangeMax, bool redrawAll = true)
{
if (rangeMin == selMin && rangeMax == selMax) return;
if (rangeMin < 0) throw new ArgumentOutOfRangeException(nameof(rangeMin));
if (rangeMax < 0 || rangeMax < rangeMin) throw new ArgumentOutOfRangeException(nameof(rangeMax));
if (rangeMin == rangeMax)
{
ShowSelection = false;
}
else
{
ShowSelection = true;
if (rangeMin < Minimum) rangeMin = Minimum;
if (rangeMin > Maximum) rangeMax = Maximum;
}
selMin = rangeMin;
selMax = rangeMax;
SendMsg(TrackBarMessage.TBM_SETSELEND, 0, rangeMax);
SendMsg(TrackBarMessage.TBM_SETSELSTART, redrawAll ? 1 : 0, rangeMin);
}
/// <summary>Adjusts the size of the control.</summary>
protected virtual void AdjustSize()
{
if (IsHandleCreated)
{
var saveDim = requestedDim;
try
{
if (Orientation == Orientation.Horizontal)
Height = AutoSize ? PreferredDimension : saveDim;
else
Width = AutoSize ? PreferredDimension : saveDim;
}
finally
{
requestedDim = saveDim;
}
}
}
/// <inheritdoc/>
protected override void OnAutoSizeChanged(EventArgs e)
{
base.OnAutoSizeChanged(e);
AdjustSize();
}
/// <summary>Raises the <see cref="E:DrawChannel"/> event.</summary>
/// <param name="pe">The <see cref="PaintEventArgs"/> instance containing the event data.</param>
/// <returns>
/// If overwritten, the method should return <c>true</c> to indicate that all drawing has been done by the method and the system
/// should not draw this item. If this method returns <c>false</c>, the system will draw the item.
/// </returns>
protected virtual bool OnDrawChannel(PaintEventArgs pe)
{
DrawChannel?.Invoke(this, pe);
return DrawChannel != null;
}
/// <summary>Raises the <see cref="E:DrawThumb"/> event.</summary>
/// <param name="pe">The <see cref="PaintEventArgs"/> instance containing the event data.</param>
/// <returns>
/// If overwritten, the method should return <c>true</c> to indicate that all drawing has been done by the method and the system
/// should not draw this item. If this method returns <c>false</c>, the system will draw the item.
/// </returns>
protected virtual bool OnDrawThumb(PaintEventArgs pe)
{
DrawThumb?.Invoke(this, pe);
return DrawThumb != null;
}
/// <summary>Raises the <see cref="E:DrawTics"/> event.</summary>
/// <param name="pe">The <see cref="PaintEventArgs"/> instance containing the event data.</param>
/// <returns>
/// If overwritten, the method should return <c>true</c> to indicate that all drawing has been done by the method and the system
/// should not draw this item. If this method returns <c>false</c>, the system will draw the item.
/// </returns>
protected virtual bool OnDrawTics(PaintEventArgs pe)
{
DrawTics?.Invoke(this, pe);
return DrawTics != null;
}
/// <inheritdoc/>
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (showSel)
{
SendMsg(TrackBarMessage.TBM_SETSELEND, 0, selMax);
SendMsg(TrackBarMessage.TBM_SETSELSTART, 1, selMin);
}
if (thumbLength >= 0)
SendMsg(TrackBarMessage.TBM_SETTHUMBLENGTH, thumbLength);
SendMsg(TrackBarMessage.TBM_CLEARTICS);
if (ticks != null && TickStyle != TickStyle.None)
foreach (var t in ticks)
SendMsg(TrackBarMessage.TBM_SETTIC, 0, t);
AdjustSize();
}
/// <summary>Raises the <see cref="Control.MouseWheel"/> event.</summary>
/// <param name="e">A <see cref="MouseEventArgs"/> that contains the event data.</param>
protected override void OnMouseWheel(MouseEventArgs e)
{
const int WHEEL_DELTA = 120;
var hme = e as HandledMouseEventArgs;
if (hme != null)
{
if (hme.Handled) return;
hme.Handled = true;
}
if ((ModifierKeys & (Keys.Shift | Keys.Alt)) != 0 || MouseButtons != MouseButtons.None)
return; // Do not scroll when Shift or Alt key is down, or when a mouse button is down.
var wheelScrollLines = SystemInformation.MouseWheelScrollLines;
if (wheelScrollLines == 0) return; // Do not scroll when the user system setting is 0 lines per notch
Debug.Assert(cumulativeWheelData > -WHEEL_DELTA, "cumulativeWheelData is too small");
Debug.Assert(cumulativeWheelData < WHEEL_DELTA, "cumulativeWheelData is too big");
cumulativeWheelData += e.Delta;
var partialNotches = cumulativeWheelData / (float)WHEEL_DELTA;
if (wheelScrollLines == -1)
wheelScrollLines = TickFrequency;
// Evaluate number of bands to scroll
var scrollBands = (int)(wheelScrollLines * partialNotches);
if (scrollBands != 0)
{
int absScrollBands;
if (scrollBands > 0)
{
absScrollBands = scrollBands;
Value = Math.Min(absScrollBands + Value, showSel && limitThumbToSel ? selMax : Maximum);
cumulativeWheelData -= (int)(scrollBands * (WHEEL_DELTA / (float)wheelScrollLines));
}
else
{
absScrollBands = -scrollBands;
Value = Math.Max(Value - absScrollBands, showSel && limitThumbToSel ? selMin : Minimum);
cumulativeWheelData -= (int)(scrollBands * (WHEEL_DELTA / (float)wheelScrollLines));
}
}
if (e.Delta != Value)
{
OnScroll(EventArgs.Empty);
OnValueChanged(EventArgs.Empty);
}
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.TrackBar.ValueChanged"/> event.</summary>
/// <param name="e">The <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnValueChanged(EventArgs e)
{
base.OnValueChanged(e);
Debug.WriteLine($">> TB ValueChg={Value} from {lastValue}");
lastValue = Value;
}
/// <summary>Sends the supplied message and parameters to the underlying TRACKBAR system control.</summary>
/// <param name="msg">The Windows message identifier.</param>
/// <param name="wParam">The wParam.</param>
/// <param name="lParam">The lParam.</param>
/// <returns>The result value defined for the message.</returns>
protected IntPtr SendMsg(TrackBarMessage msg, int wParam = 0, int lParam = 0) => IsHandleCreated ? SendMessage(Handle, (uint)msg, (IntPtr)wParam, (IntPtr)lParam) : IntPtr.Zero;
/// <inheritdoc/>
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
requestedDim = Orientation == Orientation.Horizontal ? height : width;
if (AutoSize)
{
if (Orientation == Orientation.Horizontal)
{
if ((specified & BoundsSpecified.Height) != BoundsSpecified.None)
height = PreferredDimension;
}
else
{
if ((specified & BoundsSpecified.Width) != BoundsSpecified.None)
width = PreferredDimension;
}
}
// Call base method on Control and not TrackBar
typeof(Control).GetMethod("SetBoundsCore", BindingFlags.Instance | BindingFlags.NonPublic).InvokeNotOverride(this, x, y, width, height, specified);
}
/// <inheritdoc/>
protected override void WndProc(ref Message m)
{
var msg = (WindowMessage)m.Msg;
//Debug.WriteLine($"TB WndProc: Msg={msg}");
if (msg == (WindowMessage.WM_NOTIFY | WindowMessage.WM_REFLECT))
{
if (OwnerDraw)
{
var hdr = (NMHDR)m.GetLParam(typeof(NMHDR));
if (hdr.code == (int)CommonControlNotification.NM_CUSTOMDRAW)
{
var cd = (NMCUSTOMDRAW)m.GetLParam(typeof(NMCUSTOMDRAW));
Debug.WriteLine($"{new TimeSpan(Environment.TickCount)} TBCustDraw: {cd.dwDrawStage}, {cd.dwItemSpec.ToInt32()}, {cd.uItemState}, {(Rectangle)cd.rc}");
m.Result = new IntPtr((int)CustomDraw(ref cd));
return;
}
}
}
else if (msg == (WindowMessage.WM_HSCROLL | WindowMessage.WM_REFLECT) || msg == (WindowMessage.WM_VSCROLL | WindowMessage.WM_REFLECT))
{
var pos = Value;
var code = (TrackBarScrollNotification)LOWORD(m.WParam);
Debug.WriteLine($"TB_SCROLL: pos={pos}, lastPos={lastValue}, code={code}, sel={showSel}, limit={limitThumbToSel}, selMin={selMin}, selMax={selMax}");
if (showSel && limitThumbToSel)
{
if (pos > selMax) SendMsg(TrackBarMessage.TBM_SETPOS, 1, selMax);
else if (pos < selMin) SendMsg(TrackBarMessage.TBM_SETPOS, 1, selMin);
}
if (code != TrackBarScrollNotification.TB_THUMBPOSITION && code != TrackBarScrollNotification.TB_ENDTRACK && lastValue != Value)
{
var e = new ScrollEventArgs((ScrollEventType)code, lastValue, pos, m.Msg == (int)WindowMessage.WM_HSCROLL ? ScrollOrientation.HorizontalScroll : ScrollOrientation.VerticalScroll);
OnScroll(e);
OnValueChanged(EventArgs.Empty);
}
return;
}
base.WndProc(ref m);
}
private CustomDrawResponse CustomDraw(ref NMCUSTOMDRAW cd)
{
switch (cd.dwDrawStage)
{
case CustomDrawStage.CDDS_PREPAINT:
return CustomDrawResponse.CDRF_NOTIFYITEMDRAW; // | CustomDrawResponse.CDRF_NOTIFYPOSTPAINT;
case CustomDrawStage.CDDS_ITEMPREPAINT:
switch ((TrackBarCustomDraw)cd.dwItemSpec.ToInt32())
{
case TrackBarCustomDraw.TBCD_CHANNEL:
using (var g = Graphics.FromHdc((IntPtr)cd.hdc))
{
if (OnDrawChannel(new PaintEventArgs(g, cd.rc)))
return CustomDrawResponse.CDRF_SKIPDEFAULT;
}
break;
case TrackBarCustomDraw.TBCD_THUMB:
using (var g = Graphics.FromHdc((IntPtr)cd.hdc))
{
if (OnDrawThumb(new PaintEventArgs(g, cd.rc)))
return CustomDrawResponse.CDRF_SKIPDEFAULT;
}
break;
case TrackBarCustomDraw.TBCD_TICS:
using (var g = Graphics.FromHdc((IntPtr)cd.hdc))
{
if (OnDrawTics(new PaintEventArgs(g, cd.rc)))
return CustomDrawResponse.CDRF_SKIPDEFAULT;
}
break;
}
break;
}
return CustomDrawResponse.CDRF_DODEFAULT;
}
private void ResetThumbLength()
{
thumbLength = -1;
if (IsHandleCreated) RecreateHandle();
}
private void ResetTickPositions() => TickPositions = new int[0];
private IntPtr SendMsg(TrackBarMessage msg, ref RECT rect) => SendMessage(Handle, (uint)msg, IntPtr.Zero, ref rect);
private bool ShouldSerializeThumbLength() => thumbLength >= 0;
private bool ShouldSerializeTickPositions() => ticks != null && ticks.Length > 0;
}
}