General purpose MultiSlider


(Visual Studio 2017, VS 2015 update 3) Note: only minor changes would be needed to convert for earlier versions.


As it’s name suggests, MultiSlider is a UserControl capable of displaying more than one arrow in a track bar. I have kept most of the behaviour and appearance of the stock Slider control and also added minor enhancements.

  • New arrows may be added (optional) by the user by double-clicking on the trackbar.
  • Arrows may be removed (optional) by the user by Alt+Right-clicking on them.
  • Arrows have an IsMoveable property; if it is false then they can neither be moved nor deleted (visually denoted by a colored adorner circle on the arrow).
  • Arrows have an IsUser property; if it is true then the brushes used for coloring the plain arrow will be different. The user arrow and the standard arrow are otherwise treated the same.
  • AutoToolTipPlacement is catered for.


When tabbing through the main window’s controls using the Tab key or Modifier+Cursor:

  1. If an arrow is selected, Shift+Cursor may be used to tab over any adjacent arrows.
  2. The Tab key will NOT tab over any adjacent arrows, but only over the Controls.
  3. When a MultiSlider control is tabbed to, depressing the Space key will activate the prior selected arrow.
  4. Control+Cursor acts as per normal when tabbing over controls.

There are a few multi-slider tools out there:


    This is dated 2013 and uses Windows 7 arrows, so is out of date for W10 style. Limited functionality although can add new arrows.


    This is even older (2007).


    This looks as if it is for W7. I haven’t investigated this one.


    This is fixed at just 2 arrows so is not as flexible as desired but looks quite good for what it is.

None of them are comprehensive enough for my use so I developed my own:

  • Event handling for the programmer is comprehensively catered for (see specs below).
  • I haven’t included ‘tick’ graphics and capabilities (may be for a future date) but there is AutoToolTip capability.


There are two projects in the solution that will demonstrate the MultiSlider in action. The first, TestSimple, merely displays all the arrow types and use of overlay coloring with basic functionality. The second, TestMultiSlider, is more thorough, with

  • Facilities for the user to manipulate arrows in all ways,
  • Switches to change the states of the multi-sliders,
  • A vertical and a horizontal oriented multi-slider,
  • Readout of user actions taken in a TextBlock.


  • AdornerColor – (get/set) – The color to use for the adorning circle of an arrow. If null is passed, the color is reset.
  • AdornerRatio – (get/set) – Set/get the ratio of the adorner circle’s diameter relative to the Up arrow width. If null is passed, the ratio is reset.
  • ArrowAtIndex(int index) – Returns the ArrowData at position ‘index’ (base=0 starting from the Minimum end).
  • ArrowCount – (get) – The number of arrows in the trackbar.
  • ArrowType – (get/set) – MultiSlider.ArrowTypes enum. Values: LeftRight, UpDown or Rect.
  • AutoToolTipPlacement – (get/set) – Where to place the auto tool tip relative to the arrow.
  • AutoToolTipPrecision – (get/set) – Prints the arrow’s Value to N decimal places.
  • AutoToolTipSigFigs – (get/set) – Prints the arrow’s Value in N significant figures if N > 0 and takes precedence over AutoToolTipPrecision.
  • CanDelAddArrows – (get/set) – Whether the user can Add/Delete arrows with the mouse.
  • CreateArrow(double value, bool isUser = false, bool isMoveable = true) – Create and show a new arrow. Returns an ArrowData.
    • value“>A value within Minimum and Maximum properties of the TrackBar.
    • isUser“>true: Use the arrow’s User state brushes.
    • isMoveable“>false: The arrow can neither be moved nor deleted.
  • DeleteArrow(int index, bool fireEvent) – Removes the arrow at ‘index’ (from the trackbar canvas also. base=0 starting from the Minimum end). If 'fireEvent' = true then the ArrowDeleted event is fired.
  • DeleteAllArrows(bool fireEvent) – Removes all arrows fromm the trackbar canvas. If 'fireEvent' = true then the ArrowDeleted event is fired.
  • HelpText – (get). – A string containing basic formatted text giving the user instructions. Suitable for adding to and displaying in a MessageBox.
  • IsValidValue(value) – Whether the arg falls within the multi slider’s Minimum/Maximum values.
  • Minimum – (get/set) – Minimum value of an arrow in the trackbar. The setter deletes all existing arrows and the ArrowDeleted event is not fired.
  • Maximum – (get/set) – Maximum value of an arrow in the trackbar. The setter deletes all existing arrows and the ArrowDeleted event is not fired.
  • Orientation – (get/set) – Specifies either a vertical or horizontal axis for the trackbar.
  • ReverseDirection – (get/set) – Whether Maximum is at the maximum or minimum end of the trackbar (vice versa for Minimum).
  • SchemeOverlayColor – (get,set) – Overlay color to use on top of the trackbar and plain arrows (suggested alpha=10%).
  • SmallChange – (get/set) – The small incremental Value to add/subtract to/from the current arrow’s value when using the keyboard cursor keys to move with.
  • LargeChange – (get/set) – The large incremental Value to add/subtract to/from the current arrow’s value when using the keyboard cursor keys (+Alt key) to move with.
  • ValueAtIndex(int index) – Return the Value at the valid arrow index (base 0 starting from the Minimum end) on the trackbar.
  • Value – (get/set) – The value for the first arrow on the trackbar. If the trackbar is empty DoubleNaN is got; or the trackbar is ignored for set. This is useful for a non-deletable single-arrow slider.

For the arrows (type ArrowData sent by the event handlers):

  • Index – (get) – Base 0. The n’th arrow on the trackbar starting from the Minimum end.
  • IsMoveable – (get/set) – Whether an arrow can be moved/deleted. May be done on the fly, eg in the event handlers below.
  • IsUser – (get/set) – Only the brushes to use when setting the ‘plain’ arrow brushes are different. May be done on the fly, eg in the event handlers below.
  • Value – (get/set) – Value between {Minimum to Maximum}.


  • ArrowCreated
  • ArrowRightClicked – If the Alt key is used for deleting an arrow, this will not be fired.
  • ArrowScrolled
  • ArrowDeletedArrowSelected will fire also after this if the focus changes to an adjacent arrow.
  • ArrowLeftClicked
  • ArrowSelected – Fired when an arrow receives keyboard focus (eg is clicked on or tabbed to). All pass their sender arg as an (object)ArrowData.

Implementation Notes

If Minimum==Maximum => Behaviour is a little odd but acceptable.

There is a Conditional Compilation Symbol, MULTISLIDER, in the MultiSlider properties window; change this to prevent Console output when running the Debug version outside of the Debugger.

Construction of the MultiSlider control

The control firstly has a Canvas representing the trackbar (as a Border) and it’s size is set to that of the user control. To this is first added a trackbar Border control. This Border’s size is set relative to the

width or height property of the MultiSlider. Then composite arrows, having been created and initialized, are added to the main trackbar Canvas.

An arrow consists of a Canvas which is added to the main trackbar; to this Canvas are added firstly an initialized Polygon describing the arrow, then an Ellipse (which acts as an adorner) and lastly a Label (which displays the AutoToolTip text). Whenever the arrow’s visual position needs to be changed, it is the Canvas that is placed relative to the main trackbar Canvas; thus the Polygon’s points are never accessed or changed.

Arrow keyboard focus using Shift+Cursor_key

Using Shift+Cursor key for tabbing over the arrows inside the control is achieved in method MultiSlider.Arrow_KeyDown().

Arrow moving by holding mouse down on the trackbar

This is all done in the trackbar MouseLeftButtonDown/Up handlers. Inside TrackBar_MouseLeftButtonDown(). The predicate if(Mouse.LeftButton == MouseButtonState.Released) can’t be used inside a loop because the reported Mouse button state remains unaltered when the mouse-button is released. The only technique that works is the following:

  • Deploy an iVar flag _mouseUp.
  • In TrackBar_MouseLeftButtonDown() set _mouseUp to false.
  • Write event handler, TrackBar_MouseLeftButtonUp() – sets _mouseUp to true.

Processing continuous holding mouse button down:

Write a BackgroundWorker.DoWork event handler, ProcessHoldingMouseDown(), with args set to the desired types to be passed by TrackBar_MouseLeftButtonDown(); this will do the mouse-button state detection and perform the arrow-moving operations.

In TrackBar_MouseLeftButtonDown() write the following code where the arrow moving is to be done:

BackgroundWorker threadBW = new BackgroundWorker(); threadBW.DoWork += (obj, e2) => ProcessHoldingMouseDown(mouseCoords, arrow); threadBW.RunWorkerAsync();

And in ProcessHoldingMouseDown() the GUI main thread invoker is deployed several times, eg Dispatcher.Invoke(() => TrackBar.InitBounds(arrow));

Schematic Wiring

The behaviour of the MultiSlider requires quite intricate ‘tuning’ of the internal event handlers. Modify them at your own peril!

A Shape.Polygon

RenderBounds() doesn’t take into account any Transforms (eg. Scaling, Rotating) on the shape. Just includes the StrokeThickness, etc; so when the arrow’s Polygon is rendered, it exactly fits it’s containing Canvas.