Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

SliderGdiCtrl: Yet Another Slider Control - Accepts (Almost) All POD Types and Uses GDI+

0.00/5 (No votes)
14 Apr 2011 5  
An MFC slider control that accepts and works with most POD types and has enhanced appearance.

Introduction

If you want to give the user of your app a means to select some integer values from a selection range, you will most probably use the MFC Slider control. You specify the selection range and increment, and the slider will notify your app about changes in the position of the slider's thumb. 

The MFC Slider control works with integer numbers only. What do we do if we need to work with floating point values, like doubles?  I needed a control to select a double number from the range 0...1.0, with a 0.001 increment.

The standard way to cope with it is to scale up your range numbers, convert them to integers, and feed them to the MFC Slider control. Upon receiving the user selection, you will convert it to double and scale it down. I did not want to do this. Besides, the appearance of the Slider control would be rather dull.

So I decided to write my own Slider control that accepts double numbers and has a better (in my opinion) look. I named it SliderGdiCtrl.

I have been using my SliderGdiCtrl for the last two years. When I decided to submit it to CodeProject and began to write this article, I found that with relatively minor changes, my Slider could accept and work with almost all POD numeric types. Actually, the changes were not so minor, but anyway, here it is, the SliderGdiCtrl control. This control accepts all POD types that might be converted to double without loss of precision. This excludes the long double and long long types. 

Acknowledgements

I borrowed the definitions of the templates TypeList and IndexOf from Ch. 3 of "Modern C++ Design" by Andrei Alexandrescu. I also used the compile time assertion STATIC_CHECK from static_check.h of Andrei's Loki library.

I had a very helpful discussion on Kill Focus in MFC with Code-o-mat here on the CodeProject forum.

I saw on CP a Slider control with very similar looks, but it is not the same as my control. Also, as I remember, it was written in C#. Sorry, but I am not able to locate it on CP now.

General Description

There are three C++ classes available in two source files: SliderGdiCtrl.h and SliderGdiCtrl.cpp:

  • class CSliderGdiCtrl : public CWnd;
  • template <typename T> class CSliderGdiCtrlT : public CSliderGdiCtrl;
  • class CTipWnd : public CWnd

These classes are in the namespace SliderGdi.

The class CSliderGdiCtrlT is a template wrapper around the SliderGdiCtrlT interface functions; all functionality is in CSliderGdiCtrl. CTipWnd is a helper class to display a control name and the current position value in a separate (tip) window upon user request.

All internal calculations are performed and results stored in the CSliderGdiCtrl data members as double numbers. All data members and output values of the user interface functions are rounded to the slider's increment.

The slider sends to its parent two notification messages: TB_THUMBTRACK when a thumb position is changing, and TB_THUMBPOSITION when a thumb position has changed.

The slider graphic interface uses GDI+. It uses linear and gradient brushes and an alpha channel (to get semi-transparent colors.)

The MFC CSliderCtrl uses two additional controls: a static control to display the slider name, and an edit control, a.k.a. buddy, to display the value corresponding to the current thumb position.

The SliderGdiCtrl control displays the slider name and the current value in the same window. 

The typical layout of SliderGdiCtrl in default colors is shown below. You can see the rounded rectangle of the bar, the thumb, the control name, the data label area with the current value, and the tip window. The tip window appears in response to a mouse right click, and hides on a second right click or when the mouse quits the bar rectangle. This window is similar, but not equal, to a tooltip window: there is no time limit on visibility.

The background color of the slider can be programmatically changed.

If the current value is too big to fit in the data area, the software will try to display the name and the value as one combined string. The thumb color is semi-transparent, so we will able to read the name and value no matter where the thumb position is.

If there is not enough space for the combined string, we will try to display the name only. The slider value will be shown in the tip window upon user request:

Finally, if there is not enough space even for a name, both the name and value will be shown in a tip window.

Only horizontal orientation has been implemented.

How to Use It

Preconditions

This project's classes are MFC-derived classes, using GDI+ to render graphics. All code was compiled and linked under MS VC++ 2008 (VC9). Be sure you have the appropriate MFC and GDI+ libraries included in your project.

You have to add some code for GDI+ to initialize on start and free on exit in your application. To do this, you must:

  • Include a private data member ULONG_PTR m_nGdiplusToken in your application:
  • class CMyApp : public CWinAppEx
    {
        .........
    
    private:
        ULONG_PTR m_nGdiplusToken;
        .........
    }
  • In the function CMyApp::InitInstance, add these two lines:
  • BOOL CMyApp::InitInstance ()
    { 
        ......
    
        Gdiplus::GdiplusStartupInput gdiplusStartupInput;
        Gdiplus::GdiplusStartup(&m_nGdiplusToken, &gdiplusStartupInput, NULL);
        ......
    }
  • To exit your application gracefully, add the function ExitInstance() to CMyApp if it is not there already, and insert in its body, the line:
  • int  CMyAppApp::ExitInstance()
    {
         ...........
         Gdiplus::GdiplusShutdown(m_nGdiplusToken);
         ...........
    }

You also have to include the header file SliderGdiCtrl.h in your project

You can use the SliderGdiCtrl control as a library or just include the source files in your project.

If you are going to use as a library, add the SliderGdiCtrlLib.lib file to a directory your project has access to. Include this library name in Linker\Input\Additional Dependency in the project properties dialog box.

If you are going to use the source files only, additionally include in your project, the source file SliderGdiCtrl.cpp.

If in your project you are using a precompiled header, include the following headers in your stdafx.h file: <afxext.h>, <sstream>, <iomanip>, <cmath>, <limits>, and <gdiplus.h>. If not, include them in SliderGdiCtrl.cpp and comment out the line '#include "stdafx.h".

Now you are ready to start using the CSliderGdiCtrl control in your project.

To Include the SliderGdiCtrl Control in Your Application

  1. If your application is dialog-based, in the resource editor, select the slider control in the toolbox and drag it to its place in the dialog box. Adjust the control's size and position. In the control properties window, enter a control ID (something like IDC_SLLONG or IDC_SLFL).
  2. Add the data member to your CDialog class definition, like:
  3. CSliderGdiCtrl<long> m_slGdiLong;

    or

    CSliderGdiCtrl<float>.m_slGdiFl;
  4. Add the function DDX_Control to the dialog function DoDataExchange(CDataExchange* pDX) to subclass the control, like:
  5. DDX_Control(pDX, IDC_SLLONG, m_slGdiLong);
  6. Or, if your application is a document-view application, declare the CSliderGdiCtrl<T> data member in a view class, like:
  7. CSliderGdiCtrl<long> m_slGdiLong;

    and call:

    m_slGdiLong.CreateSliderGdiCtrl(DWORD dwStyle, const CRect slRect, 
                                    CWnd* pParent, UINT slID)

    to create a slider window.

  8. Use the CSliderGdiCtrlT and CSliderGdiCtrl interface members to initialize and manipulate your slider control. If the application is dialog-based, initialize sliders in InitDialog; if not, do it in OnInitialUpdate of the slider's parent CView.

The SliderGdiCtrl Control Interface

To initialize and manipulate the SliderGdiCtrl, the programmer should use the interface member functions of the classes CSliderGdiCtrlT and CSliderGdiCtrl.

To get the current value (the thumb position):

template <typename T>
T CSliderGdiCtrlT<T>::GetCurrValue(void) const; 

To get the min and max values of the selection range:

template <typename T>
T CSliderGdiCtrlT<T>::GetMinVal(void) const;

template <typename T>
T CSliderGdiCtrlT<T>::GetMaxVal(void) const; 

In the CSliderGdiCtrlT's template member functions for setting the CSliderGdiCtrl data members, all functions arguments must be of the same type as the type T the slider was declared and instantiated with. This constraint is for type safety. Violation of this rule will throw a compile time assertion. If you really need to pass some other data types to these functions, explicitly cast them to T. If you want to pass literals as parameters to these functions, use literal suffixes to correctly define the literal type, e.g., 23i16 for short, 6.98f for float, etc. More about it in the Points of Interest section.

The function SetCurrValue sets the current position of the slider thumb. This function always clips values out of the slider range to the range's max or min, and returns false if the value was clipped. The flag bRedraw defines whether the slider should be redrawn immediately.

template <typename T>
template <typename T1>
bool CSliderGdiCtrlT<T>::SetCurrValue(T1 value, bool bRedraw = false);

The next function is to set the min and max values of the selection range. The function changes nothing and returns false if constraints on the min and max are violated, or the parameter bAdjustCurrVal == false and the current value is out of the new selection range. If bAdjustCurrVal == true, the current value is clipped to minVal or maxVal when it is out of the selection range.

template <typename T>
template <typename T1, typename T2>
bool CSliderGdiCtrlT<T>::SetMinMaxVal(T1 minVal, T2 maxVal, 
     bool bAdjustCurrVal = false, bool bRedraw = false);

The next function sets the boundaries of the selection range, the precision (an increment), and the current value (the thumb position). If the value of the precision is positive, it defines the number of decimal digits after the decimal point. The negative precision defines the number of non-significant zeros before the decimal point. If  precision = P, increment is 10-P. All values the slider exchanges with an application and displays to the user are rounded to 10-P. Precision = 3 means three digits to the right of the decimal point; precision = -2 means the last two digits before the decimal point are not significant and always equal to zero.

This function is preferable for initialization because it checks all relations and limits on range, precision, and current value.

The function changes nothing and returns false if any of the constraints on its parameters are violated.

template <typename T>
template <typename T1, typename T2, typename T3>
bool CSliderGdiCtrlT<T>::SetInitVals(T1 minVal, T2 maxVal, 
     int precision, T3 currVal, bool bRedraw = false);

To set the control's background color or restore the default colors, you will need these interface functions:

void BarColor(Gdiplus::Color barFirstCol, bool bRedraw =  false);
void SetBarColorDefault(bool bRedraw = false);

To get the current bar color, use:

Gdiplus::Color BarColor(void) const

Two overloaded functions will set the SliderGdiCtrl name:

void SetCaption(std::wstring& caption, bool bRedraw = false);
void SetCaption(WCHAR* caption, bool bRedraw = false);

To get the name, use:

std::wstring GetCaption();

To set a precision, use:

bool SetPrecision(int precision, bool bRedraw = false);

If the precision value is out of limits, the function returns false.

To get the precision and the increment values, call:

int Precision(void) const;

and:

double GetKeyStep (void) const;

You can programmatically move the slider control and/or change its size by sending or posting the Windows messages WM_SIZE and WM_MOVE to it.

Below is an example of the declaration and initialization of the slider control for the document/view architecture:

void CSliderGdiCtrlWView::OnInitialUpdate()
{
    CView::OnInitialUpdate();
    m_slCtrlPtr = new CSliderGdiCtrlT<long>;
    CRect cRect;
    GetClientRect(&cRect);
    cRect.DeflateRect(250, 250);
    m_slCtrlPtr->CreateSliderGdiCtrl(WS_CHILD|WS_VISIBLE, &cRect, this, 32789);
    // Min range -100, max range 1000 no digits after 
    // decimal point, current value 50
    m_slTest.SetInitVals(-100L, 100L, 0, 50L);
    // Set control name "LONG VALUES"
    m_slTest.SetCaption(L"LONG VALUES");
    // Set background color red
    m_slTest.BarColor(Color(255, 255, 0, 0));
}

and how it looks:

Notifications

The slider sends two notification messages to its parent: TB_THUMBTRACK on left mouse button down, on mouse moving, and on key down, and TB_THUMBPOSITION on left mouse button up or on key up.

If the SliderGdiCtrl's parent is to handle these notifications, include in the parent's message map, entries like:

ON_NOTIFY(TB_THUMBTRACK, IDC_SLID, OnSlidePosChanging)
ON_NOTIFY(TB_THUMBPOSITION, IDC_SLID, OnSlidePosChanged)

where the handler functions are declared as:

afx_msg void  OnSlidePosChanging(NMHDR *pNMHDR, LRESULT *pResult);
afx_msg void  OnSlidePosChanged(NMHDR *pNMHDR, LRESULT *pResult);

and NMHDR is a standard structure from the Windows API.

User Manual

To move the slider's thumb, the user should left click on the thumb and drag the mouse, keeping the left button down. Left click inside the thumb movement's area but outside of the thumb will move the thumb to the mouse X-coordinate. Left click anywhere in the bar will set the focus to the slider.

To move the thumb by increments, the user should use the arrow keys when the slider has focus:

  • Left arrow key will decrease the thumb position by one increment;
  • Right arrow key will increase the thumb position by one increment;
  • Page Up key decreases the thumb position by ten increments;
  • Page Down key increases the thumb position by ten increments;
  • HOME key sets the thumb at minimum (left) boundary of the range;
  • END key sets the thumb at maximum (right) boundary of the range.

To make the tip window visible, the user should right click anywhere inside the slider. The tip window will stay visible while the mouse cursor is inside the slider area. It closes on the second right click, or hides automatically when the cursor goes out of the bar area.

Points of Interest

Classes

As I mentioned before, there are three C++ classes in the namespace SliderGdi:

  • class CSliderGdiCtrl : public CWnd;
  • template <typename T> class CSliderGdiCtrlT : public CSliderGdiCtrl;
  • class CTipWnd : public CWnd

All the slider control's data members and member functions are in the class CSliderGdiCtrl. The data members of this class store the maximum and minimum boundaries of the slider's range, an increment value, and a current value corresponding to the current thumb position. These data members are of the double type. You may say that the internal representation of these values in CSliderGdiCtrl is double. In my opinion, it is the best choice. All POD types except long long and long double could be cast to the type double without loss of precision. Casts from double to other POD types also could be done precisely if some constraints on the double's values are honored. 

Of course, if you choose the floating-point type, you invite on yourself all traps of the floating-point arithmetic, but you gain much more universality, as I am going to show in a moment.

To work with POD types, there is the template class:

template <typename T> class CSliderGdiCtrlT: public CSliderGdiCtrl

Essentially, it is a template wrapper around the interface functions of CSliderGdiCtrl.

The CSliderGdiCtrlT class is responsible for safe casts from T to double and back. Inheritance from CSliderGdiCtrl decreases code bloating, because all T-independent functions are members of CSliderGdiCtrl that is not a template.

The CSliderGdiCtrl uses the third class, CTipWnd, to invoke a tip window.

Limits and Constraints

Let us discuss the limits and constraints on the SliderGdiCtrl range boundaries, the current value, and precision. These values are stored in the data members of CSliderGdiCtrl: m_fMinVal, m_fMaxVal, m_fCurrValue, and m_precision.

Some limits are trivial: the minimum value of the range cannot be greater than or equal to the maximum value of the range; an increment cannot be greater than the range or be equal to zero. An increment cannot be less than the minimal value of the type, DBL_MIN for double, FLT_MIN for float, and 1 for integer types.

Additionally, the maximum and minimum (or right and left) boundaries of the slider's range are indirectly limited by the value of the range. Obviously, the range cannot be larger than the maximum value of the type. For the type double:

Range = RightBoundary - LeftBoundary  DBL_MAX = 1.7976931348623158e+e308

(Of course, one cannot really expect that huge a range in an application. Still, this value might surface because of a wrong computation.)

Fortunately, the type double is not a modulo type. If a double number goes out of the type range, the result is positive or negative INFINITY, not some number nicely wrapped around one of the limits of the type. So the range must be tested against INFINITY (or for absence of INFINITY):

if (!_finite(dbRange))
    return false;

But the test against INFINITY is not enough.

Floating-point numbers have finite precision because they occupy a limited number of bits in memory. It means that if we would increase too big a number by too small a value, the big number is not changing. So there are constraints on the relationships between the minimum and maximum boundaries of the range, and a value of the increment.

Precision and Increment

To get a value for the slider's new position, we should add or subtract an increment(s) to/from the value. We know that increasing or decreasing a floating-point number by a too small increment does not change the number. Therefore, there should be some lower limit on the increment.

The constant that characterizes accuracy and precision of the double type is:

std::numeric_limits<double>:: digits10 = DBL_DIG = 15

The C++ Reference defines std::numeric_limits<T>::digits10 as the "number of digits (in decimal base) that can be represented without change". It is confusing. What change? Increasing a too big floating-point number by a too small increment does not change the big number.

As explained in [1], std::numeric_limits<T>::digits10 "is the number of decimal digits guaranteed to be correct". It defines the number of significant digits in a number.

Applied to the slider declared with type double, it means that to make the current value accurate, the difference between the greater absolute value of the range boundaries and an increment must be no greater than fifteen orders of magnitude. This is a lower limit on the increment.

The fifteen orders of magnitude is one quadrillionth of the number value, so it is a rather theoretical limit, but again, bad calculations might happen...

The actual number of orders of magnitude of this difference defines precision, or the decimal exponent of the increment. What about the mantissa? Let us see what problem we have there.

Assume you have the range boundaries -1.0, +1.0, and the increment 0.005. You want to move the slider's thumb to the position that has the value 0.345. You click on the slider's bar and get the value 0.344. The probability that the next mouse move or click would deliver exactly 0.345 is very low. The arrow key will increase the value by one increment, to 0.349. So for easy navigation, you should set the mantissa of the increment to one and round the range boundaries and the current value by the increment.

In our example, the increment should be 0.001. If the current value is 0.345, one click of the right arrow key will increase the current value to 0.345.

It is more convenient to measure the precision in number of digits related to the position of the decimal point. In this slider control, a positive precision equals the number of decimal digits to the right of the decimal point; a negative precision denotes the number of non-significant zeros to the left of the decimal point. The value of the increment is:

D = 10-P where P is the precision

All that is written above is about the limits and constraints on the slider's class CSliderGdiCtrlT<double>.

The slider's selection range of other POD types is not limited by the type's maximum value because all calculations are performed in double values, and related data members of the class CSliderGdiCtrl have type double. Of course, the boundaries of the range and the current slider value can not be out of the range of the type.

The minimal increment for the type float is limited by:

std::numeric_limits<float>::digits10 = FLT_DIG = 6

The minimal increment for integer types is one.

Therefore, the class CSliderGdiCtrl must know the actual types of its double data members.

Information of the actual type of the data members of CSliderGdiCtrl is stored in the data member:

int CSliderGdiCtrl::m_typeID; // Index of the slider data  
          //type: -1 if not supported, 
          // 0 -6 for int types, 7 for  
          // float, 8 for double

Nowhere in code can you see these numbers. Then how does the class initialize m_typeID and use it?

The answer is: at the instantiation of CSliderGdiCtrlT<T> via the initialization of the base class CSliderGdiCtrl:

template <typename T>
     CSliderGdiCtrlT<T>::CSliderGdiCtrlT(void):
                CSliderGdiCtrl(IndexOf<SL_TYPELIST, T>::value);

The constructor of the base class is:

CSliderGdiCtrl::CSliderGdiCtrl (int typeID = IndexOf<SL_TYPELIST, double>::value)
{
    .......................
    m_typeID = typeID;
    .......................
}

At instantiation of CSliderGdiCtrlT<T>, the compiler calculates the value of typeID and initializes m_typeID accordingly.

Every time we want to do something depending on the actual data type, we write a piece of meta-code:

 if (m_typeID == IndexOf<SL_TYPELIST, my_type>::value) do_something();

Again, the condition is resolved at compile time.

This mechanism uses meta-programming with TYPELISTS. I will return to TYPELISTS in a moment, but before that, I want to show the calculation of the maximal precision in the function GetMaxPrecision:

int CSliderGdiCtrl::GetMaxPrecision(double value) const
{
    // For integer types (located before float in TYPELIST)
    if (m_typeID < IndexOf<SL_TYPELIST, float>::value)
        return 0;
    // For floating-point types float and double
    double absVal = fabs(value);
    double lgAbsVal = log10(absVal);
    double maxDig; 
    modf(lgAbsVal, &maxDig); // Can't use ceil(lgAbsVal) 
               //because ceil(log10(1.0)) =   
               // 0 (we need 1) 
    int nMaxDig = static_cast<int>(maxDig); 
    if (lgAbsVal >= 0)
    //Greatest significant digit
        nMaxDig += 1;      //has a position log+1 
    int maxPrec = m_digitNmb - nMaxDig;
    // Check for minimum positive value of the type (has 
    // exponent < 0)
    int minExp = (m_typeID == IndexOf<SL_TYPELIST, double>::value) ?
                  -numeric_limits<double>::min_exponent10 : 
                  -numeric_limits<float>::min_exponent10;
    return min(maxPrec, minExp);
}

This member function of CSliderGdiCtrl sets the precision and an increment:

bool CSliderGdiCtrl::SetPrecision(int precision,bool bRedraw /*=false*/)
{
   // m_fMaxVal and m_fMinVal are the range bounddaries    
   int maxPrecision = GetMaxPrecision(max(abs(m_fMinVal), abs(m_fMaxVal)));
   if (precision > maxPrecision return false;
   double step = pow(10.0, -precision); // Increment
          
   // Calculate range
   double fRange = m_fMaxVal - m_fMinVal;
   if (step > fRange)
       return false;
   m_precision = precision;  // Set data member
   // Update the string to display current value    
   m_strValue = SetValStr(m_fCurrValue);
   // Set flag to indicate layout must be recalculated
   m_slStat = UNINIT;
   if (bRedraw) // Redraw immediately
       RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW | RDW_NOERASE); 
   return true;
}

TYPELISTs and Other Bits of Meta-programming

As I mentioned before, the class CSliderGdiCtrl must know the actual type of its double data members. At instantiation of CSliderGdiCtrlT<T>, the compiler calculates the value of typeID and initializes CSliderGdiCtrl:: m_typeID.

To do so, the compiler needs to have a collection of allowed types at hand and know how to get an index of the type from the collection.

I used TypeList as a type collection and meta.function IndexOf to search for the type's index in the collection. The code below is borrowed from [2], ch. 3:

class NullType;
template <class T, class U>
struct TypeList
{
    typedef T Head;
    typedef U Tail;
};
#define TYPELIST_1(T1) TypeList<T1, NullType>
#define TYPELIST_2(T1, T2) TypeList<T1, TYPELIST_1(T2)>
#define TYPELIST_3(T1, T2, T3) TypeList<T1, \
                                    TYPELIST_2(T2, T3)>
..........................................................

#define TYPELIST_9(T1, T2, T3, T4, T5, T6, T7, T8, T9) \
       TypeList<T1, TYPELIST_8(T2, T3, T4, T5, T6, T7,\ 
       T8, T9)>
template <class TList, typename T> struct IndexOf;
template <typename T> 
struct IndexOf<NullType, T>  { enum { value = -1};};
template <class Tail, typename T>
struct IndexOf<TypeList<T, Tail>, T>
{
    enum {value = 0};
};
template <class Head, class Tail, typename T>
struct IndexOf<TypeList<Head, Tail>, T>
{
private:
    enum {temp = IndexOf<Tail, T>::value};
public:
     enum {value = temp == -1 ? -1 : 1 +temp};
};

Now, let us define the types the slider can work with:

typedef TYPELIST_9(byte, short, unsigned short, int, 
        unsigned int, long, unsigned long, float,
        double) SL_TYPELIST;

and at compile time, the compiler will calculate an index of the type:

index<T> = IndexOf<SL_TYPELIST, T>::value;

to take care of type safety in the interface functions of CSliderGdiCtrlT<T>.

Now let us talk about type safety.

Take, for example, the function CSliderGdiCtrlT<T>::SetInitVals. On first glance, it seems enough to define this function as:

bool CSliderGdiCtrlT<T>::SetInitVals(T minVal, T maxVal, 
         int precision, T currVal, bool bRedraw = false)
{ 
    return CSliderGdiCtrl::SetInitVals(minVal, maxVal, 
                           precision, currVal, bRedraw);
}

This is not working.

Suppose, we have instantiated the slider as CSliderGdiCtrlT<short> slShort, and are try to initialize it with:

slShort.SetInitVals(250000, 260000, 0, 255000);

The compiler knows that slShort has the type CSliderGdiCtrlT<short>, so the parameters T minVal, T minVal, T currVal are of type short. Still, it will perceive the literals in the argument list as int and apply implicit conversions to short to these parameters. As a result, we will end with minVal = -12,144, minVal = -7144, and currVal = -2144.

If we are lucky, CSliderGdiCtrl::SetInitVals will catch errors and return false, but it is fully possible that the numbers are plausible, and we will have an absolutely wrong range, current value, and precision with no indication of the error.

Solution: Because all types T are known at compile time, I declared the related interface functions as template member functions only to throw a compile time assertion if the argument types are different from T. For example:

template <typename T>
template <typename T1, typename T2, typename T3>
bool CSliderGdiCtrlT<T>::SetInitVals(T1 minVal, T2 maxVal, 
     int precision, T3 currVal, bool bRedraw = false)
{ 
    STATIC_CHECK(((IndexOf<SL_TYPELIST, T>::value == IndexOf<SL_TYPELIST, T1>::value)&&
                   (IndexOf<SL_TYPELIST, T>::value == IndexOf<SL_TYPELIST, T2>::value) &&
                   (IndexOf<SL_TYPELIST, T>::value == IndexOf<SL_TYPELIST, T3>::value)), 
                    Wrong_Types_In_SetInitVals);
    return CSliderGdiCtrl::SetInitVals(minVal, maxVal, precision, currVal, bRedraw);
}

The compile time assert submits an undefined construction (e.g., structure or function) to the compiler if the condition is evaluated to false. There are many implementations of it, including BOOST_STATIC_ASSERT and C++ static_assert from C++0x.

I chose the code for STATIC_CHECK from Andrei Alexandrescu's Loki library file static_check.h (I changed the template parameter from int to bool):

 template<bool> struct CompileTimeError;
template<> struct CompileTimeError<true> {};
#define STATIC_CHECK(expr, msg) \
{ CompileTimeError<((expr) != 0)> ERROR_##msg; \ 
   (void)ERROR_##msg; }

Here, expr is the expression to evaluate, and msg is the name of the function in the compiler error message. For example, if msg = SetInitVals, the error message is:

error C2079: 'ERROR_ Wrong_Types_In_SetInitVals' uses 
  undefined struct 'SliderGdi::CompileTimeError<__formal>' 

  ...(place for the source file name and line number): see reference 
      to function template instantiation 
      'bool SliderGdi::CSliderGdiCtrlT<T>::SetInitVals<short,int,short> 
      T1,T2,int,T3,bool)' being compiled
          with
          [
            T=short,
            T1=short,
            T2=int, 
            T3=short
      ]
Must be T2=short.

Run-time Exceptions

Besides supplying wrong types to member functions of CSliderGdiCtrl<T>, there are others ways to mess with the parameters. For example, you might pass minVal > maxVal, currVal out of range (minVal, maxVal), or too big a value for precision to the function CSliderGdiCtrlT<T>::SetInitVals(T1 minVal, T2 maxVal, int precision, T3 currVal, bool bRedraw = false). On such occasions, the functions will return false.

I decided to throw no exceptions on this kind of errors. Nevertheless, you can catch the return value and throw an exception (invalid_argument, or out_of_range).

Graphic Interface and Appearance

I am using GDI+ for the graphic interface because I need linear and gradient brushes and semi-transparent colors. In addition, GDI+ allows floating-point coordinates that might increase the accuracy of   conversion of current values (double) into screen coordinates of the slider's thumb and back. Layout and colors of the SliderGdiCtrl are tuned to an LCD wide display with a resolution of 1680x1050 pixels.

As I noted before, a slider name and a slider current value are integrated in the slider window. 

A typical layout of the slider control is shown below in default colors:

The function CSliderGdiCtrl::InitSliderCtl(Gdiplus::Graphics& gr) calculates the slider layout and stores it in the CSliderGdiCtrl data members (rectangles, fonts, etc.).

This function is called from CSliderGdiCtrl::OnPaint() if the slider's range, precision, current value, size, or position were changed. It is called also if the user or application changed the thumb position. Technically, the layout is recalculated if the data member CSliderGdiCtrl::m_slStat = NOINIT.

To eliminate flicker, all drawing is performed onto a memory bitmap. This is how it looks in GDI+:

void CSliderGdiCtrl::OnPaint()
{
   CPaintDC dc(this);      // Device context for painting
   Graphics gr(dc.m_hDC);  // GDI+ graphics class to paint
   Rect rGdi;
   gr.GetVisibleClipBounds(&rGdi); // The same as the client rect
 
   if (m_slStat == UNINIT)
   {
        InitSliderCtl(gr); // Calculate layout, set rectangles
   // Prepare to highlite the thumb and to catch WM_MOUSELEAVE
   // if mouse is hovering over moving area 
   }
   Bitmap clBmp(rGdi.Width, rGdi.Height);  // Memory bitmap
   Graphics* grPtr = Graphics::FromImage(&clBmp);  // As memDC
   grPtr->SetSmoothingMode(SmoothingModeAntiAlias);
   GraphicsPath grPath; // To draw rounded rectangles

   DrawBar(grPtr, grPath);
   if (!m_rValLabelF.IsEmptyArea())// If data label visible
        DrawValLabel(grPtr, grPath);
   DrawMoveArea(grPtr, grPath);
   DrawThumb(grPtr, grPath);
   gr.DrawImage(&clBmp, rGdi);     // Transfer onto screen
   delete grPtr;
}

The tip window is created in response to mouse right click on the slider. It is a layered window created with styles:

wS_EX_LAYERED|WS_EX_TRANSPARENT|WS_EX_TOPMOST|WS_EX_NOACTIVATE

A bogus transparent color is set by a call to the function SetLayeredWindowAttributes(...).

There is this function to create a tipWnd:

BOOL CTipWnd::CreateTipWnd(const CRect parentRect, const std::wstring& maxStr, int strNmb)
 {
       BOOL bRes = FALSE;
       bRes =  CreateEx(
            WS_EX_LAYERED|WS_EX_TRANSPARENT|WS_EX_TOPMOST|
            WS_EX_NOACTIVATE,
            AfxRegisterWndClass(
                 CS_HREDRAW|CS_VREDRAW|CS_SAVEBITS),
             NULL,       // Wnd Title (Caption string ?)
             WS_POPUP,
             0, 0, 0, 0, // Zero Rectangle
             NULL,
             NULL,
             0);
       if (bRes) // Calculate the tipWnd layout and set a
       {
          // transparent color
          Graphics* grPtr = Graphics::FromHWND(m_hWnd);
          CRect tipWndRect = SetTipWndRects(grPtr, parentRect, maxStr, strNmb);
          Color bkgndCol(Color::Azure);
          bRes =  SetLayeredWindowAttributes(
                     bkgndCol.ToCOLORREF(), 0, LWA_COLORKEY);
          // Important: use only SetWindowPos 
          SetWindowPos(&wndBottom, tipWndRect.left, 
                       tipWndRect.top, tipWndRect.Width(), 
                       tipWndRect.Height(), SWP_NOACTIVATE);
          delete grPtr;
       }
       return bRes;
}

A topmost window steals focus from the slider that initiated its creation. It is up to the slider to reset focus.

Colors

To draw the slider, we need quite a few colors and linear and gradient brushes. It takes a long time to pickup and tune up all the colors and brushes. Therefore, I decided the users will not be allowed to pickup all colors, even though I recognize a need for it. It is just not done here.

However, there is a need to mark different sliders by color. Think about red, green, and blue sliders in color pickers. Therefore, the user might change the first color of the slider bar's linear brush.

Processing User Input

This enum describes the slider states:

enum CSliderGdiCtrl::SLIDE_STATUS
{ 
   UNINIT,  // Not initialized yet    
   IDLE,     // Out of the slider's bar    
   LBTNDOWN,// Set after left mouse's click on the 
           // slider's until LBUTTONUP received
   HOVERING,// If left button is up    
};

The response to mouse events depends on the slider's state.

When the mouse is over the slider's bar, the slider's state changes to HOWERING, and the thumb color changes to green; additionally, the slider prepares to catch WM_MOUSELEAVE. Code to prepare to track is shown below:

void CSliderGdiCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
     ...................
     // Prepare to catch WM_MOUSELEAVE; if the mouse is 
     // too quick, without this hovering colors will persist
     if (!m_bMouseTracking)
     {
        TRACKMOUSEEVENT trMouseEvent;
        trMouseEvent.cbSize = sizeof(TRACKMOUSEEVENT);
        trMouseEvent.dwFlags = TME_LEAVE;
        trMouseEvent.hwndTrack = this->m_hWnd;
        m_bMouseTracking = TrackMouseEvent(&trMouseEvent);
     }
     .......................
}

In HOWERING state, a left click on the bar sets the focus to the slider. If the cursor is in the thumb movement area, the mouse is captured and the cursor is limited to this area. If the cursor is in the thumb rectangle, it is moved to the thumb's center; the other way, the thumb moves to the cursor X coordinate and a new slider current value is calculated and displayed. The state changes to LBTNDOWN. Note that, in this state, left mouse click always positions the cursor at the center of the thumb.

Therefore, in LBTNDOWN WM_MOUSEMOVE always drag the thumb changing the slider's current value. The mouse can't leave the thumb movement area: it is captured.

WM_LBUTTONUP releases the mouse.

As mentioned before, right mouse click shows/destroys the tip window. Also, the tip window is destroyed in response to WM_MOUSELEAVE.

In mouse event handlers, the slider's current value is calculated from the mouse coordinates (the thumb position). Contrariwise, in response to arrow keys (WM_KEYDOWN), we calculate the current value first, and the thumb and cursor position second. Why? Because the cursor movement in response to the key event might be very small. For example, for the range 10000, increment 1, and the thumb movement area 100, one increment equals to a thumb movement of 0.01 pixel.

The slider does not react to mouse messages if one of the arrow keys is down.

Consult the source code and play with the demo application to get more ideas about processing the user input.

The Source Code and Demo Projects

SliderGdiCtrlSource.zip includes three files: stdafx.h, SliderGDICtrl.h, and SliderGDICtrl.cpp.

The file stdafx.h in this archive is an imitation of the plain header file. If you are not using precompiled headers in your project and have included SliderGDICtrl.cpp, add stdafx.h too. You will not have to change line 54 in the SliderGDICtrl.cpp file:

54   #include "stdafx.h"

I have also included in this archive the folder Doc. It contains the documentation for the SliderGdiCtrl, generated with Doxygen. To start, you should open the file index.html in your browser. Note that to use the links to the source files, you must first extract them into the folder C:/VS2008/Projects/SliderGdiCtrlLib/SliderGdiCtrlLib/.

I have included three demo projects to show how to use the slider control. All of them are compiled with warning level 4.

The first demo is a dialog based MFC application, SlierGdiT. The dialog features four slider controls, one accepting short integers, two working with doubles, and one working with long integers. You can play with them, like:

  • Enable and disable the slider to see a difference in appearance.
  • Change slider precision.
  • Change slider size and position to see how the slider layout is changing.
  • Change the slider background color. In response to a click on the 'Slider Color' button, the Color dialog box will be displayed. To restore default colors, click 'Cancel'; to change color, select the color and click 'OK'.
  • The 'Test Options' button gives you a menu of options to test and see. Because the tip window disappears when the mouse leaves the slider window, the options come into effect after a two second delay. You may return to the slider and activate the tip window by mouse right click before the changes.
  • It also shows how to throw exceptions when the 'set' functions return false.

I have also included the executable SliderGdiT.exe. This file is a release version, using MFC in a static library. It is a big file, but you might run it right out of box.

The second project is SlierGdiCtrlW, a document-view application to show how to create a slider without a resource editor.

The third project, SliderGdiCtrlLib, shows how to use the CSlierGdiCtrlT as a library.

To compile and link the code, I used MS Visual Studio 2008 Standard with SP1 and Visual Studio 2008 Feature Pack. You can download this Feature Pack from Microsoft for free.

Literature

  1. "N2005: A Maximum Significant Decimal Digits Value for the C++0x Standard Library Numeric Limits", Doc. #JTC 1/SC22/WG21/N2005=06-0075, by Paul A Bristow, 2006.
  2. "Modern C++ Design: Generic Programming and Design Patterns Applied", by Andrei Alexandrescu, Addison-Wesley.

History

  • Initial version: 04/11/2011.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here