using System; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; 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_Gdi; namespace Vanara.Windows.Forms { /// Extends the class to provide full native-control functionality. /// 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. public event PaintEventHandler DrawChannel; /// Occurs when the thumb for a needs to be drawn and the property is set to true. public event PaintEventHandler DrawThumb; /// Occurs when the ticks for a need to be drawn and the property is set to true. 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. [DefaultValue(null), Category("Appearance"), Description("Indicates the logical values of the trackbar where ticks are drawn.")] public int[] TickPositions { get { if (!IsHandleCreated || TickStyle == TickStyle.None) return null; var ptr = SendMsg(TrackBarMessage.TBM_GETPTICS); return ptr.ToIEnum(SendMsg(TrackBarMessage.TBM_GETNUMTICS).ToInt32() - 2).OrderBy(i => i).Distinct().ToArray(); } set { if (value != null) { if (value.Min() < Minimum || value.Max() > Maximum) throw new ArgumentOutOfRangeException(nameof(TickPositions), "All values must be between Minimum and Maximum range values."); if (TickStyle == TickStyle.None) throw new ArgumentException("Tick positions cannot be set when TickStyle is None."); } if (ticks != null) SendMsg(TrackBarMessage.TBM_CLEARTICS); ticks = value; 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) SetSelectionRange(selMin, selMax); if (thumbLength >= 0) SendMsg(TrackBarMessage.TBM_SETTHUMBLENGTH, thumbLength); if (ticks != null && TickStyle != TickStyle.None) SetTicks(); AdjustSize(); } 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); } } 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 IntPtr SendMsg(TrackBarMessage msg, ref RECT rect) => SendMessage(Handle, (uint)msg, IntPtr.Zero, ref rect); private void SetTicks() { if (ticks == null) return; foreach (var t in ticks) SendMsg(TrackBarMessage.TBM_SETTIC, 0, t); } private bool ShouldSerializeThumbLength() => thumbLength >= 0; private bool ShouldSerializeTickPositions() => ticks != null; } }