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 { /// /// Extends the class to provide full native-control functionality, including tick marks and value, and custom drawing. /// /// 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; /// Initializes a new instance of the class. public TrackBarEx() { SetStyle(ControlStyles.SupportsTransparentBackColor, true); BackColorChanged += (o, a) => { if (IsHandleCreated) RecreateHandle(); }; requestedDim = PreferredDimension; } /// /// Occurs when the channel for a needs to be drawn and the property is set to true. /// [Category("Drawing")] public event PaintEventHandler DrawChannel; /// /// Occurs when the thumb for a needs to be drawn and the property is set to true. /// [Category("Drawing")] public event PaintEventHandler DrawThumb; /// /// Occurs when the ticks for a need to be drawn and the property is set to true. /// [Category("Drawing")] public event PaintEventHandler DrawTics; /// Gets or sets a value indicating whether to draw ticks based on the interval. /// true if automatic ticks should be shown; otherwise, false. [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(); } } /// Gets the bounds of the channel (slider track). /// The channel bounds. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Rectangle ChannelBounds { get { var r = new RECT(); if (IsHandleCreated) SendMsg(TrackBarMessage.TBM_GETCHANNELRECT, ref r); return r; } } /// Gets or sets a value indicating whether to limit thumb movement to the selection. /// true if movement is limited to selection; otherwise, false. [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; } } /// Gets or sets a value indicating the horizontal or vertical orientation of the track bar. [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(); } } } /// Gets or sets a value indicating whether the control is drawn by the operating system or by code that you provide. /// /// true if the control is drawn by code that you provide; false if the control is drawn by the operating system. The /// default is false. /// [DefaultValue(false), Category("Behavior"), Description("Specifies whether to custom draw the trackbar.")] public bool OwnerDraw { get; set; } /// Gets or sets the upper limit of the selection range this TrackBar is working with. /// /// The logical position at which the selection ends. This value must be less than or equal to the value of the property. /// [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); } /// Gets or sets the lower limit of the selection range this TrackBar is working with. /// /// The logical position at which the selection starts. This value must be greater than or equal to the value of the property. /// [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); } /// /// Gets or sets a value indicating whether to show the selection area defined by and . /// /// true if showing selection area; otherwise, false. [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(); } } /// Gets the bounds of the thumb slider in its current position. /// The thumb bounds. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Rectangle ThumbBounds { get { var r = new RECT(); if (IsHandleCreated) SendMsg(TrackBarMessage.TBM_GETTHUMBRECT, ref r); return r; } } /// Gets or sets the length of the thumb, overriding the default. /// /// The length of the thumb. This is the vertical length if the orientation is horizontal and the horizontal length if the /// orientation is vertical. /// [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(); } } /// Gets or sets an array that contains the positions of the tick marks for a trackbar. /// /// 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. /// [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(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(); } } /// 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; } } /// 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; } } /// Sets the starting and ending positions for the available selection range in a trackbar. /// The starting logical position for the selection range. /// The ending logical position for the selection range. /// /// 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. /// 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); } /// Adjusts the size of the control. 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; } } } /// protected override void OnAutoSizeChanged(EventArgs e) { base.OnAutoSizeChanged(e); AdjustSize(); } /// Raises the event. /// The instance containing the event data. /// /// If overwritten, the method should return true to indicate that all drawing has been done by the method and the system /// should not draw this item. If this method returns false, the system will draw the item. /// protected virtual bool OnDrawChannel(PaintEventArgs pe) { DrawChannel?.Invoke(this, pe); return DrawChannel != null; } /// Raises the event. /// The instance containing the event data. /// /// If overwritten, the method should return true to indicate that all drawing has been done by the method and the system /// should not draw this item. If this method returns false, the system will draw the item. /// protected virtual bool OnDrawThumb(PaintEventArgs pe) { DrawThumb?.Invoke(this, pe); return DrawThumb != null; } /// Raises the event. /// The instance containing the event data. /// /// If overwritten, the method should return true to indicate that all drawing has been done by the method and the system /// should not draw this item. If this method returns false, the system will draw the item. /// protected virtual bool OnDrawTics(PaintEventArgs pe) { DrawTics?.Invoke(this, pe); return DrawTics != null; } /// 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(); } /// Raises the event. /// A that contains the event data. 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); } } /// Raises the event. /// The that contains the event data. protected override void OnValueChanged(EventArgs e) { base.OnValueChanged(e); Debug.WriteLine($">> TB ValueChg={Value} from {lastValue}"); lastValue = Value; } /// Sends the supplied message and parameters to the underlying TRACKBAR system control. /// The Windows message identifier. /// The wParam. /// The lParam. /// The result value defined for the message. protected IntPtr SendMsg(TrackBarMessage msg, int wParam = 0, int lParam = 0) => IsHandleCreated ? SendMessage(Handle, (uint)msg, (IntPtr)wParam, (IntPtr)lParam) : IntPtr.Zero; /// 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); } /// 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; } }