All too often I see examples of custom controls which unnecessarily add child controls. For instance, a custom trackbar control may have a label or panel added to act as the scrolling thumb tab.
In this instance that's not so bad as it only creates one extra window handle, but where several such child controls are added the user will see what is percieved as flicker as each window is individually painted one after the other. No amount of doublebuffering will cure this.
Since we need to respond to mouse, keyboard or code change events and then calculate the exact bounds of the child control, it really makes no sense to add the extra control. We may as well apply those bounds to a rectangle instead, and then paint within this rectangle (don't have to paint a Rectangle just need to paint inside it).
The following example shows how to define and use a rectangle for this purpose. I have tried to make it behave just like a standard System.Windows.Forms.Trackbar control, but have made no offort to have it look like one. Hopefully, you'll be able to apply the same principles to other custom controls, perhaps using some of the many control renderers to simulate "lite" controls.
by: Mick Dohertys'
In this instance that's not so bad as it only creates one extra window handle, but where several such child controls are added the user will see what is percieved as flicker as each window is individually painted one after the other. No amount of doublebuffering will cure this.
Since we need to respond to mouse, keyboard or code change events and then calculate the exact bounds of the child control, it really makes no sense to add the extra control. We may as well apply those bounds to a rectangle instead, and then paint within this rectangle (don't have to paint a Rectangle just need to paint inside it).
The following example shows how to define and use a rectangle for this purpose. I have tried to make it behave just like a standard System.Windows.Forms.Trackbar control, but have made no offort to have it look like one. Hopefully, you'll be able to apply the same principles to other custom controls, perhaps using some of the many control renderers to simulate "lite" controls.
VB net Sample:
Imports System.ComponentModel Namespace Dotnetrix.Samples.VB <DefaultEvent("Scroll"), _ DefaultProperty("Value"), _ ToolboxBitmap(GetType(System.Windows.Forms.TrackBar))> _ Public Class TrackBar Inherits Control Public Sub New() MyBase.New() Me.SetStyle(ControlStyles.SupportsTransparentBackColor Or ControlStyles.OptimizedDoubleBuffer Or _ ControlStyles.AllPaintingInWmPaint Or ControlStyles.Selectable Or ControlStyles.UserMouse, True) Me.Thumb = New Rectangle() Me.LayoutTrackBarParts() End Sub Protected Overrides ReadOnly Property DefaultSize() As System.Drawing.Size Get Return New Size(100, 60) End Get End Property #Region " Event Delarations " Public Event Scroll As EventHandler Public Event ValueChanged As EventHandler #End Region #Region " Private Instance Variables " Private _orientation As Orientation = Orientation.Horizontal Private _minimum As Int32 Private _maximum As Int32 = 10 Private _smallChange As Int32 = 1 Private _largeChange As Int32 = 5 Private _value As Int32 Private _tickStyle As TickStyle = TickStyle.BottomRight Private _tickFrequency As Int32 = 1 Private _thumbDragging As Boolean Private _scrollUp As Boolean Private Thumb As Rectangle Private _thumbFocused As Boolean Private scrollTimer As Timer #End Region #Region " Public properties " <DefaultValue(GetType(Orientation), "Horizontal")> _ Public Property Orientation() As Orientation Get Return _orientation End Get Set(ByVal value As Orientation) If (_orientation <> value) Then _orientation = value Dim w, h As Int32 w = Me.Height h = Me.Width Me.Size = New Size(w, h) Me.LayoutTrackBarParts() Me.Invalidate() End If End Set End Property <DefaultValue(0), _ RefreshProperties(RefreshProperties.All)> _ Public Property Minimum() As Int32 Get Return _minimum End Get Set(ByVal value As Int32) If (_minimum <> value) Then _minimum = value If (_maximum <= value) Then _maximum = value End If Me.LayoutTrackBarParts() Me.Invalidate() End If End Set End Property <DefaultValue(10)> _ Public Property Maximum() As Int32 Get Return _maximum End Get Set(ByVal value As Int32) If (_maximum <> value) Then _maximum = value If (Minimum >= value) Then Minimum = value End If Me.LayoutTrackBarParts() Me.Invalidate() End If End Set End Property <DefaultValue(1)> _ Public Property SmallChange() As Int32 Get Return _smallChange End Get Set(ByVal value As Int32) _smallChange = value End Set End Property <DefaultValue(5)> _ Public Property LargeChange() As Int32 Get Return _largeChange End Get Set(ByVal value As Int32) _largeChange = value End Set End Property Public Property Value() As Int32 Get If (_value < Me.Minimum) Then Return Me.Minimum End If Return _value End Get Set(ByVal value As Int32) If (value < Me.Minimum) Then value = _minimum End If If (value > _maximum) Then value = _maximum End If If (value <> _value) Then _value = value Me.LayoutTrackBarParts() Me.OnValueChanged(EventArgs.Empty) End If End Set End Property Private Function ShouldSerializeValue() As Boolean Return Me._value <> Me.Minimum End Function Private Sub ResetValue() Me._value = Me.Minimum End Sub <DefaultValue(False)> _ Public Shadows Property TabStop() As Boolean Get Return MyBase.TabStop End Get Set(ByVal value As Boolean) MyBase.TabStop = value End Set End Property <DefaultValue(""), _ Browsable(False), _ EditorBrowsable(EditorBrowsableState.Never)> _ Public Overrides Property Text() As String Get Return String.Empty End Get Set(ByVal value As String) MyBase.Text = String.Empty End Set End Property <DefaultValue(GetType(TickStyle), "BottomRight")> _ Public Property TickStyle() As TickStyle Get Return _tickStyle End Get Set(ByVal value As TickStyle) If (_tickStyle <> value) Then _tickStyle = value Me.Invalidate() End If End Set End Property <DefaultValue(1)> _ Public Property TickFrequency() As Int32 Get Return _tickFrequency End Get Set(ByVal value As Int32) If (_tickFrequency <> value) Then _tickFrequency = value Me.Invalidate() End If End Set End Property #End Region #Region " private Properties " Private ReadOnly Property Horizontal() As Boolean Get Return _orientation = Orientation.Horizontal End Get End Property Private Property ThumbDragging() As Boolean Get Return _thumbDragging End Get Set(ByVal value As Boolean) If (_thumbDragging <> value) Then _thumbDragging = value Me.Invalidate() End If End Set End Property #End Region #Region " Overridden Methods " Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) 'Do default painting MyBase.OnPaint(e) 'Draw Tick Marks DrawTicks(e.Graphics) 'Draw Channel Dim channelBounds As Rectangle If Me.Horizontal Then channelBounds = New Rectangle(6, Me.Height / 2 - 2, Me.Width - 16, 4) Else channelBounds = New Rectangle(Me.Width / 2 - 2, 6, 4, Me.Height - 16) End If ControlPaint.DrawBorder3D(e.Graphics, channelBounds, Border3DStyle.Sunken) ' Draw the Thumb Object Using brush As SolidBrush = New SolidBrush(Color.Blue) If (_thumbFocused) Then brush.Color = Color.Green End If If (ThumbDragging) Then brush.Color = Color.Red End If e.Graphics.FillRectangle(brush, Me.Thumb) End Using 'Draw Focus If (Me.Focused AndAlso Me.ShowFocusCues) Then ControlPaint.DrawFocusRectangle(e.Graphics, Me.ClientRectangle) End If End Sub Protected Overrides Sub OnGotFocus(ByVal e As EventArgs) MyBase.OnGotFocus(e) _thumbFocused = (Me.Focused AndAlso Me.ShowFocusCues) Me.Invalidate() End Sub Protected Overrides Sub OnLostFocus(ByVal e As EventArgs) MyBase.OnLostFocus(e) _thumbFocused = (Me.Focused AndAlso Me.ShowFocusCues) Me.Invalidate() End Sub Protected Overrides Function IsInputKey(ByVal keyData As Keys) As Boolean Select Case (keyData) Case Keys.Up, Keys.Down, Keys.Left, Keys.Right Return True Case Else Return MyBase.IsInputKey(keyData) End Select End Function Protected Overrides Sub OnKeyDown(ByVal e As KeyEventArgs) MyBase.OnKeyDown(e) Select Case (e.KeyCode) Case Keys.Up, Keys.Left If (Me.Horizontal) Then Me.Value -= _smallChange Else Me.Value += _smallChange End If Case Keys.Down, Keys.Right If (Me.Horizontal) Then Me.Value += _smallChange Else Me.Value -= _smallChange End If Case Keys.PageDown If (Me.Horizontal) Then Me.Value += _largeChange Else Me.Value -= _largeChange End If Case Keys.PageUp If (Me.Horizontal) Then Me.Value -= _largeChange Else Me.Value += _largeChange End If Case Keys.Home If (Me.Horizontal) Then Me.Value = _minimum Else Me.Value = _maximum End If Case Keys.End If (Me.Horizontal) Then Me.Value = _maximum Else Me.Value = _minimum End If End Select End Sub Protected Overrides Sub OnMouseWheel(ByVal e As MouseEventArgs) MyBase.OnMouseWheel(e) Me.Value += (e.Delta / WHEEL_DELTA) * WHEEL_LINES End Sub Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs) MyBase.OnMouseDown(e) If (e.Button = MouseButtons.Left) Then ThumbDragging = Thumb.Contains(e.Location) If (Not ThumbDragging) Then If (Me.Horizontal) Then _scrollUp = e.X > Thumb.Right Else _scrollUp = e.Y < Thumb.Top End If If (scrollTimer Is Nothing) Then InitTimer() End If scrollTimer.Start() End If End If End Sub Protected Overrides Sub OnMouseUp(ByVal e As MouseEventArgs) MyBase.OnMouseUp(e) ThumbDragging = False If (Me.scrollTimer IsNot Nothing) Then Me.scrollTimer.Stop() End If End Sub Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs) MyBase.OnMouseMove(e) If (ThumbDragging) Then Me.Value = ValueFromPoint(e.Location) End If End Sub Protected Overrides Sub OnVisibleChanged(ByVal e As EventArgs) MyBase.OnVisibleChanged(e) If (Me.Visible) Then Me.LayoutTrackBarParts() End If End Sub Protected Overrides Sub OnSizeChanged(ByVal e As EventArgs) MyBase.OnSizeChanged(e) Me.LayoutTrackBarParts() End Sub #End Region #Region " Protected Methods " Protected Overridable Sub OnScroll(ByVal e As EventArgs) RaiseEvent Scroll(Me, e) End Sub Protected Overridable Sub OnValueChanged(ByVal eventArgs As EventArgs) RaiseEvent ValueChanged(Me, eventArgs) Me.LayoutTrackBarParts() Me.OnScroll(eventArgs) Me.Invalidate() End Sub #End Region #Region " Internal Methods " Private Sub LayoutTrackBarParts() If Me.Horizontal Then Thumb.Size = New Size(14, 28) Else Thumb.Size = New Size(28, 14) End If Dim channelLength As Single If Me.Horizontal Then channelLength = Me.Width - 26 ' Channel Left margin + Channel Right margin + Thumb.Width Else channelLength = Me.Height - 26 ' Channel Top margin + Channel Bottom margin + Thumb.Height End If Dim stepCount As Single = (Me.Maximum - Me.Minimum) Dim stepSize As Single If stepCount > 0 Then stepSize = channelLength / stepCount Else stepSize = 0 End If Dim thumbOffset As Single = (stepSize) * (Me.Value - Me.Minimum) If Me.Horizontal Then Thumb.Location = Point.Round(New PointF(6 + thumbOffset, Me.Height / 2 - 14)) Else Thumb.Location = Point.Round(New PointF(Me.Width / 2 - 14, channelLength - thumbOffset + 6)) End If End Sub Private Sub InitTimer() Me.scrollTimer = New Timer() Me.scrollTimer.Interval = 500 AddHandler scrollTimer.Tick, AddressOf scrollTimer_Tick End Sub Private Sub scrollTimer_Tick(ByVal sender As Object, ByVal e As EventArgs) If (_scrollUp) Then Me.Value += Me.LargeChange Else Me.Value -= Me.LargeChange End If If (Me._value = Me.Minimum OrElse Me._value = Me.Maximum) Then Me.scrollTimer.Stop() End If Dim val As Int32 = Me.ValueFromPoint(Me.PointToClient(Cursor.Position)) If (_scrollUp AndAlso Me._value > val) Then Me.scrollTimer.Stop() End If If (Not _scrollUp AndAlso Me._value < val) Then Me.scrollTimer.Stop() End If End Sub Private Function ValueFromPoint(ByVal point As Point) As Int32 Dim channelLength As Single If (Me.Horizontal) Then channelLength = Me.Width - 26 ' Channel Left margin + Channel Right margin + Thumb.Width Else channelLength = Me.Height - 26 ' Channel Top margin + Channel Bottom margin + Thumb.Height End If Dim stepCount As Single = (Me.Maximum - Me.Minimum) Dim stepSize As Single = 0 If (stepCount > 0) Then stepSize = channelLength / stepCount End If If (Me.Horizontal) Then point.Offset(-7, 0) Return (point.X / stepSize) + Me.Minimum End If point.Offset(0, -7) Return Me.Maximum - (point.Y / stepSize) + Me.Minimum End Function Private Sub DrawTicks(ByVal graphics As Graphics) If (TickStyle <> TickStyle.None) Then 'TODO: Implement Tick Drawing End If End Sub #End Region Private WHEEL_DELTA As Int32 = SystemInformation.MouseWheelScrollDelta Private WHEEL_LINES As Int32 = SystemInformation.MouseWheelScrollLines End Class End Namespace
CSharp Sample:
using System.Windows.Forms; using System.Drawing; using System.ComponentModel; using System; namespace Dotnetrix.Samples.CSharp { [DefaultEvent("Scroll")] [DefaultProperty("Value")] [ToolboxBitmap(typeof(System.Windows.Forms.TrackBar))] public class TrackBar : Control { public TrackBar() : base() { this.SetStyle(ControlStyles.SupportsTransparentBackColor | ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.Selectable | ControlStyles.UserMouse, true); this.Thumb = new Rectangle(); this.LayoutTrackBarParts(); } protected override Size DefaultSize { get { return new Size(100, 60); } } #region Event Delarations public event EventHandler Scroll; public event EventHandler ValueChanged; #endregion #region Private Instance Variables private Orientation orientation = Orientation.Horizontal; private int minimum = 0; private int maximum = 10; private int smallChange = 1; private int largeChange = 5; private int _value = 0; private TickStyle tickStyle = TickStyle.BottomRight; private int tickFrequency = 1; private bool thumbDragging = false; private bool scrollUp = false; private Rectangle Thumb; private bool ThumbFocused; private Timer scrollTimer; #endregion #region Public properties [DefaultValue(typeof(Orientation), "Horizontal")] public Orientation Orientation { get { return this.orientation; } set { if (this.orientation != value) { this.orientation = value; int w, h; w = this.Height; h = this.Width; this.Size = new Size(w, h); this.LayoutTrackBarParts(); this.Invalidate(); } } } [DefaultValue(0)] [RefreshProperties(RefreshProperties.All)] public int Minimum { get { return minimum; } set { if (minimum != value) { minimum = value; if (maximum <= value) Maximum = value; this.LayoutTrackBarParts(); this.Invalidate(); } } } [DefaultValue(10)] public int Maximum { get { return maximum; } set { if (maximum != value) { maximum = value; if (minimum >= value) Minimum = value; this.LayoutTrackBarParts(); this.Invalidate(); } } } [DefaultValue(1)] public int SmallChange { get { return smallChange; } set { if (smallChange != value) smallChange = value; } } [DefaultValue(5)] public int LargeChange { get { return largeChange; } set { if (largeChange != value) largeChange = value; } } public int Value { get { if (_value < this.minimum) return this.minimum; return _value; } set { if (value < this.minimum) value = this.minimum; if (value > this.maximum) value = this.maximum; if (value != _value) { _value = value; this.LayoutTrackBarParts(); this.OnValueChanged(EventArgs.Empty); } } } private bool ShouldSerializeValue() { return this._value != this.minimum; } private void ResetValue() { this._value = this.minimum; } [DefaultValue(false)] public new bool TabStop { get { return base.TabStop; } set { base.TabStop = value; } } [DefaultValue("")] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public override string Text { get { return ""; } set { base.Text = ""; } } [DefaultValue(typeof(TickStyle), "BottomRight")] public TickStyle TickStyle { get { return this.tickStyle; } set { if (this.tickStyle != value) { this.tickStyle = value; this.Invalidate(); } } } [DefaultValue(1)] public int TickFrequency { get { return this.tickFrequency; } set { if (this.tickFrequency != value) { this.tickFrequency = value; this.Invalidate(); } } } #endregion #region private Properties private bool Horizontal { get { return this.orientation == Orientation.Horizontal; } } private bool ThumbDragging { get { return thumbDragging; } set { if (thumbDragging != value) { thumbDragging = value; this.Invalidate(); } } } #endregion #region Overridden Methods protected override void OnPaint(PaintEventArgs e) { //Do default painting base.OnPaint(e); //Draw Tick Marks DrawTicks(e.Graphics); //Draw Channel Rectangle channelBounds = this.Horizontal ? new Rectangle(6, this.Height / 2 - 2, this.Width - 16, 4) : new Rectangle(this.Width / 2 - 2, 6, 4, this.Height - 16); ControlPaint.DrawBorder3D(e.Graphics, channelBounds, Border3DStyle.Sunken); // Draw the Thumb Object using (SolidBrush brush = new SolidBrush(Color.Blue)) { if (ThumbFocused) brush.Color = Color.Green; if (ThumbDragging) brush.Color = Color.Red; e.Graphics.FillRectangle(brush, this.Thumb); } //Draw Focus if (this.Focused && this.ShowFocusCues) ControlPaint.DrawFocusRectangle(e.Graphics, this.ClientRectangle); } protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); ThumbFocused = (this.Focused && this.ShowFocusCues); this.Invalidate(); } protected override void OnLostFocus(EventArgs e) { base.OnLostFocus(e); ThumbFocused = (this.Focused && this.ShowFocusCues); this.Invalidate(); } protected override bool IsInputKey(Keys keyData) { switch (keyData) { case Keys.Up: case Keys.Down: case Keys.Left: case Keys.Right: return true; default: return base.IsInputKey(keyData); } } protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); switch (e.KeyCode) { case Keys.Up: case Keys.Left: this.Value += this.Horizontal ? -this.smallChange : this.smallChange; break; case Keys.Down: case Keys.Right: this.Value += this.Horizontal ? this.smallChange : -this.smallChange; break; case Keys.PageDown: this.Value += this.Horizontal ? this.largeChange : -this.largeChange; break; case Keys.PageUp: this.Value += this.Horizontal ? -this.largeChange : this.largeChange; break; case Keys.Home: this.Value = this.Horizontal ? this.minimum : this.maximum; break; case Keys.End: this.Value = this.Horizontal ? this.maximum : this.minimum; break; } } protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); this.Value += (e.Delta / WHEEL_DELTA) * WHEEL_LINES; } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (e.Button == MouseButtons.Left) { ThumbDragging = Thumb.Contains(e.Location); if (!ThumbDragging) { scrollUp = this.Horizontal ? e.X > Thumb.Right : e.Y < Thumb.Top; if (scrollTimer == null) InitTimer(); scrollTimer.Start(); } } } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); ThumbDragging = false; if (this.scrollTimer != null) this.scrollTimer.Stop(); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (ThumbDragging) { this.Value = ValueFromPoint(e.Location); } } protected override void OnVisibleChanged(EventArgs e) { base.OnVisibleChanged(e); if (this.Visible) this.LayoutTrackBarParts(); } protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); this.LayoutTrackBarParts(); } #endregion #region Protected Methods protected virtual void OnScroll(EventArgs e) { if (this.Scroll != null) this.Scroll(this, e); } protected virtual void OnValueChanged(EventArgs eventArgs) { if (this.ValueChanged != null) this.ValueChanged(this, eventArgs); this.LayoutTrackBarParts(); this.OnScroll(eventArgs); this.Invalidate(); } #endregion #region Internal Methods private void LayoutTrackBarParts() { if (Thumb == null) return; Thumb.Size = this.Horizontal ? new Size(14, 28) : new Size(28, 14); float channelLength = this.Horizontal ? this.Width - 26 : // Channel Left margin + Channel Right margin + Thumb.Width this.Height - 26; // Channel Top margin + Channel Bottom margin + Thumb.Height float stepCount = (this.Maximum - this.Minimum); float stepSize = stepCount > 0 ? channelLength / stepCount : 0; float thumbOffset = (stepSize) * (this.Value - this.minimum); Thumb.Location = this.Horizontal ? Point.Round(new PointF(6 + thumbOffset, this.Height / 2 - 14)) : Point.Round(new PointF(this.Width / 2 - 14, channelLength - thumbOffset + 6)); } private void InitTimer() { this.scrollTimer = new Timer(); this.scrollTimer.Interval = 500; this.scrollTimer.Tick += new EventHandler(scrollTimer_Tick); } private void scrollTimer_Tick(object sender, EventArgs e) { this.Value += this.scrollUp ? this.largeChange : -this.largeChange; if (this._value == this.minimum || this._value == this.maximum) this.scrollTimer.Stop(); int val = this.ValueFromPoint(this.PointToClient(Cursor.Position)); if (this.scrollUp && this._value > val) this.scrollTimer.Stop(); if (!this.scrollUp && this._value < val) this.scrollTimer.Stop(); } private int ValueFromPoint(Point point) { float channelLength = this.Horizontal ? this.Width - 26 : // Channel Left margin + Channel Right margin + Thumb.Width this.Height - 26; // Channel Top margin + Channel Bottom margin + Thumb.Height float stepCount = (this.maximum - this.minimum); float stepSize = stepCount > 0 ? channelLength / stepCount : 0; if (this.Horizontal) { point.Offset(-7, 0); return (int)(point.X / stepSize) + this.minimum; } point.Offset(0, -7); return this.maximum - (int)(point.Y / stepSize) + this.minimum; } private void DrawTicks(Graphics graphics) { if (tickStyle == TickStyle.None) return; //TODO: Implement Tick Drawing } #endregion private int WHEEL_DELTA = SystemInformation.MouseWheelScrollDelta; private int WHEEL_LINES = SystemInformation.MouseWheelScrollLines; } }
by: Mick Dohertys'
Hi hope someone can help.
ReplyDeleteUsing VB.net in Visual Studio 2019
When I try to add this to a form I get an error saying Type 'TrackBar.TrackBar' is not defined.
When I click on the error it takes me to Me.TrackBar3 = New TrackBar.TrackBar().
Deleting TrackBar. gets it working again. Does anyone know what in the code is causing this?