Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / MFC
Article

CSliderCtrlEx - A slider with background colors to indicate ranges

Rate me:
Please Sign up or sign in to vote.
4.92/5 (8 votes)
24 Nov 2002CPOL8 min read 151.3K   8K   60   24
This slider allows colors/gradients to be used to indicate good, bad, or marginal ranges

Sample Image - CSliderCtrlEx.gif

Contents

Introduction

In a recent project, I needed to solicit user input via either sliders or editboxes. The value entered in the first slider-editbox controlled what was allowed to be entered into the second slider-editbox. My first pass on this was to display error messages when the supplied values were disallowed (or not recommended). But this was awkward for the user.

Then it occurred to me that sliders are analog and errors messages are discrete; what I needed was a feedback that was also analog in nature. Coloring the parts of the slider with ranges (say, green for good and red for bad) seemed the way to go. You can see the intended effect in the screen shot above.

CSliderCtrlEx is derived from CSliderCtrl and can be dropped into your project easily.

Acknowledgements

Some of the code, particularly for extracting the tic marks and centerline during drawing, is based on an excellent article by Nic Wilson in miscctrl/transparentslider.asp.

I have also used the technique for dynamically loading in <nobr>msimg32.dll and getting GradientFill as demonstrated by Irek Zielinski in staticctrl/gradient_static.asp. Irek also presents an alternative to GradientFill, but the one in CSliderCtrlEx was developed by me completely independently (laboriously, painfully, and before I read Irek's article).

Ales Krajnc wrote an article, gdi/colornames.asp, that I made use of by copying <nobr>ColorNames.h. It sure is easier to read and understand code that has things like colOrange rather than <nobr>RGB(255,165,0).

A function that I found useful for developing this control is based on an article by "gelbert" on www.experts-exchange.com at Programming/Programming_Languages/MFC/Q_20193761.html. It is just a simple little utility to dump a bitmap to a file for later examination in your favorite paint program. Wonderous for those of us not yet comfortable with GDI operations. I have included it in the source code under the name (surprise!) <nobr>SaveBitmap()

Features of CSliderCtrlEx

There are two main features for this control:

  • Colors can be added to the background of the control. The colors are painted in the order given (so you can paint the entire range in one color, say red and then paint a subrange in green). The location of the colors is in terms of slider position values (not in pixels or some other non-portable mechanism). The member functions are:
    BOOL AddColor(int nLow, int nHigh, COLORREF color);
    BOOL AddColor(int nLow, int nHigh, COLORREF strColor, COLORREF endColor);
  • A callback function can be installed to be called whenever the slider's value changes. This was added to make my life easier (I need to update an editbox when the slider changes) and it is easy enough to do, so I include it here. The inverse operation (updating the slider's position upon changing the editbox) is done in the encapsulating framework and I don't have an example of that in this article, but it is quite straightforward.

    The callback function looks like:
    typedef void(*ptr2Func)(void *p2Object, LPARAM data1, int sValue,
                 BOOL IsDragging);
    The intended use of these parameters is that p2Object is a pointer to the class instance (this), and data1 would be the control ID of the slider in question. That way you can have one callback function that would know which slider it is handling. The sValue is just the slider position (which saves having to call GetPos()) and IsDragging just indicates if the left mouse button is down or not.

How it Works

The control is painted in many steps. Fortunately, there is a way to get notified between important steps by using the OnCustomDraw function (documentation for which can be found in NM_CUSTOMDRAW, not in the documentation for CSliderCtrl). This function is called just before the control is painted. If requested, the function will also be called at various stages during the drawing, such as before and after painting the tic marks, the channel, and the thumb. So, making sure the function is called for the subpieces is paramount:

void CSliderCtrlEx::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) 
{
    int loopMax = colorList.GetSize();	// number of color ranges to process
    LPNMCUSTOMDRAW lpCustDraw = (LPNMCUSTOMDRAW)pNMHDR;

    //////////////////////////////////////////////////////////////////////
    // OnCustomDraw() is called at many different stages during the painting 
    // process of the control. We only care about the PREPAINT state or the
    // ITEMPREPAINT state and not always then.
    //
    // If we want to be notified about subcontrol painting, we have to say
    // so when we get the initial PREPAINT message.
    /////////////////////////////////////////////////////////////////////
    if(lpCustDraw->dwDrawStage == CDDS_PREPAINT)
    {
        int curVal = GetPos();

        // should we report slider's position?
        if((m_Callback != NULL) && (curVal != m_oldPosition))
        {
            m_oldPosition = curVal;
            m_Callback(m_p2Object, m_data1, curVal, m_IsDragging);
        }

        // If we don't have any special coloring to do, skip all the
        // silliness...
        if(loopMax <= 0)
        {
            *pResult = CDRF_DODEFAULT;
        }
        else
        {
            <FONT color=red>// We want to be informed when each part of the control is being
<FONT color=#000000>            </FONT>// processed so we can insert the colors before drawing the thumb
<FONT color=#000000>            </FONT>*pResult = CDRF_NOTIFYITEMDRAW;	// send messages for each
                                                // piece-part</FONT>
        }





        return;
    }
}
The coloring of the background is done after everything except the thumb has been painted so we can ignore everything else:
if((lpCustDraw->dwDrawStage == CDDS_ITEMPREPAINT) &&
    (lpCustDraw->dwItemSpec != TBCD_THUMB))
{
    *pResult = CDRF_DODEFAULT;
    return;
}

Saving the Tics

Now it starts getting into GDI stuff (a weak area for me). Below is the code that saves the tic marks (paraphrased from Nic Wilson's work). I have extracted the following display from the source code and elided comments; the source code is filthy with comments and might be amusing to view:

// Get the coordinates of the control's window
CRect crect;
GetClientRect(crect);

CDC *pDC = CDC::FromHandle(lpCustDraw->hdc);
CDC SaveCDC;
CBitmap SaveCBmp;

//set the colours for the monochrome mask bitmap
COLORREF crOldBack = pDC->SetBkColor(RGB(0,0,0));  // set to Black
COLORREF crOldText = pDC->SetTextColor(RGB(255,255,255)); // set to White

int iWidth  = crect.Width();	// channel width
int iHeight = crect.Height();	// channel height

SaveCDC.CreateCompatibleDC(pDC);
SaveCBmp.CreateCompatibleBitmap(&SaveCDC, iWidth, iHeight);
CBitmap* SaveCBmpOld = (CBitmap *)SaveCDC.SelectObject(SaveCBmp);
SaveCDC.BitBlt(0, 0, iWidth, iHeight, pDC, crect.left, crect.top, SRCCOPY);

if(m_dumpBitmaps)	// debugging stuff
{
    SaveBitmap("MonoTicsMask.bmp",SaveCBmp);
}
Note the call to SaveBitmap. I found this function very useful. In fact, here is the resulting bitmap (enlarged):

Monochrome bitmap showing tics

This bitmap (as contained in the SaveCDC device context) gets used quite a bit later on, after the background colors have been drawn.

Munge in Memory Space, Not Screen Space

A fair number of operations are involved here and while I could do my gradients and rectangles and overlapping colors and ANDing, INVERTing, and so on to the screen, it would be slow and the screen would flicker a lot. So, I make a memory DC to work with:

CDC memDC;


memDC.CreateCompatibleDC(pDC);

CBitmap memBM;


memBM.CreateCompatibleBitmap(pDC,iWidth,iHeight); // create from pDC,
                                                  // not memDC
CBitmap *oldbm = memDC.SelectObject(&memBM);

memDC.BitBlt(0,0,iWidth,iHeight,pDC,0,0,SRCCOPY);
Note that even though I have a DC that is compatible with the screen (memDC) I must create the bitmap using the screen's DC. Otherwise I get a monochrome bitmap. (Don't ask how long it took to figure it out.). The bitmap looks like:

Starting bitmap of the slider control

Actually, this is what it looks like the very first time the control is painted. On subsequent updates, you can see the remnant of previous background colors. It doesn't really matter much as I'm going to completely overwrite it. But using SaveBitmap did allow me to confirm that I was on track.

Where to Draw?

The first time I did this control, I painted the entire length of the client window. It looked good. It looked right. But later on I noticed that the center of the thumb didn't always correspond to the reported position. I finally figured out that the problem was that the range of the slider is not the entire width of the client rectangle (imagine that!). The range of the slider's values is represented by the range of movement of the center of the thumb.

Components of a slider control

So I needed to confine my colors to that portion covered by the center of the thumb, not the entire width of the client rectangle. Well, there is a <nobr>GetChannelRect() function that returns the channel that the thumb slides in. There is also a <nobr>GetThumbRect() function. Fine, I can do the math. But there this is this little gotcha:

<FONT color=red>// For unknown reasons, GetChannelRect() returns a rectangle
// as though it were a horizonally oriented slider, even if it isn't!</FONT>
if(IsVertical)
{
    CRect n;
    n.left = chanRect.top;
    n.right = chanRect.bottom;
    n.top = chanRect.left;
    n.bottom = chanRect.right;
    n.NormalizeRect();
    chanRect.CopyRect(&n);
}

// Offset into client rectangle for beginning of color range
int Offset = chanRect.left + thmbRect.Width()/2;



if(IsVertical)
{
    Offset = chanRect.top + thmbRect.Height()/2;
}

// Range for center of thumb
int ht = chanRect.Height() - thmbRect.Height();
int wd = chanRect.Width()  - thmbRect.Width();
Now I can get a scaling factor between slider range units and pixels.

Drawing in the Colors

The color ranges are stored in an array of structures with start and end position, and start and end color. Looping through these in order is relatively simple. I scale the positions to pixel values, extract the Red, Green, and Blue values from the start and end colors, and setup a call to GradientFill to do the drawing:

TRIVERTEX vert[2];	// for specifying range to gradient fill
GRADIENT_RECT gRect;

vert[0].Red   = sR<<8;	// expects 16-bit color values!
vert[0].Green = sG<<8;
vert[0].Blue  = sB<<8;
vert[0].Alpha = 0;	// no fading/transparency
				
vert[1].Red   = eR<<8;
vert[1].Green = eG<<8;
vert[1].Blue  = eB<<8;
vert[1].Alpha = 0;

gRect.UpperLeft = 0;
gRect.LowerRight = 1;
				
BOOL retval;
if(IsVertical)	// vertically oriented?
{
    vert[0].x = 0;
    vert[0].y = Offset + minVal;
    vert[1].x = iWidth;
    vert[1].y = Offset + minVal + widthVal;
    retval = GradientFill(memDC,vert,2,&gRect,1,GRADIENT_FILL_RECT_V);
}
else
{
    vert[0].x = Offset + minVal;
    vert[0].y = 0;
    vert[1].x = Offset + minVal + widthVal;
    vert[1].y = iHeight;
    retval = GradientFill(memDC,vert,2,&gRect,1,GRADIENT_FILL_RECT_H);
}
One item that confused me for awhile was the fact that when using the TRIVERTEX structure, the RGB values have to be shifted up a byte. For the longest time I could only get black...

If there is no gradient (start and end colors are identical) then GradientFill does the reasonable thing: a solid fill.

If I just left things like this, then the colors would be correct, but control would look ugly:

Ugly looking background for control

What I needed to do was fill out the ends:

Background colors

if(IsVertical)
{
    if(gotStartColor)
    {
        memDC.FillSolidRect(0, 0, iWidth, Offset, startColor);
    }

    if(gotEndColor)
    {
        memDC.FillSolidRect(0,iHeight - Offset - 1,iWidth, Offset, endColor);
    }
}
else
{
    if(gotStartColor)
    {
        memDC.FillSolidRect(0, 0, Offset, iHeight, startColor);
    }

    if(gotEndColor)
    {
        memDC.FillSolidRect(iWidth - Offset - 1,0,Offset, iHeight, endColor);
    }
}
Obviously, I saved the colors during the coloring loop. If a color range didn't extend to the end of the control's range, I had no reason to extend anything.

ReApplying the Tics

Here is another place where Nic Wilson's work saved me a lot of trouble. Here are the steps and the intermediate results. The source code has a lot more comments, but doesn't have the bitmaps to view. Remember, the tic marks are saved in SaveCDC:

memDC.SetBkColor(pDC->GetBkColor());	// RGB(0,0,0)
memDC.SetTextColor(pDC->GetTextColor());	// RGB(255,255,255)
memDC.BitBlt(0, 0, iWidth, iHeight, &SaveCDC, 0, 0, SRCINVERT);
This results in the tics being applied, but the colors are "backwards."

Image 7

Now fix the tic marks:
memDC.BitBlt(0, 0, iWidth, iHeight, &SaveCDC, 0, 0, SRCAND);
Image 8

Now invert all the colors for the final image:
memDC.BitBlt(0, 0, iWidth, iHeight, &SaveCDC, 0, 0, SRCINVERT);
Image 9

Blit it to the screen and clean up! The rest of the control drawing is handled by the base class code, which is mostly the thumb and the borders.
// Now copy out to screen
pDC->BitBlt(0,0,iWidth,iHeight,&memDC,0,0,SRCCOPY);

Using the code

Adding to the Project

To add CSliderCtrlEx to your project, add the source files, SliderCtrlEx.h and SliderCtrlEx.cpp, to your project (Project --> Add to Project --> Files...) and build. Then add ordinary Sliders to your dialogs. Then use the ClassWizard to associate a member variable of type CSliderCtrlEx. If ClassWizard doesn't list the new type, just use CSliderCtrl and manually change the type.

Adding Color Ranges

In <nobr>OnInitialUpdate() (or where ever you like, for that matter), add lines like:

// Normal CSliderCtrl init:
m_Slider2.SetBuddy(&m_Edit2,FALSE);  // force edit control to "buddy up"
m_Slider2.SetRange(0,1000);
m_Slider2.SetTicFreq(100);

<FONT color=red>// CSliderCtrlEx-specific stuff:
m_Slider2.AddColor(0,1000,RGB(255,0,0));	// Pure Red

// Make a gradient 
m_Slider2.AddColor(200,300,colRed,colOrange);	// user should stay away 
                                                // from here
m_Slider2.AddColor(300,400,colOrange,colYellow);// not an optimal value for
                                                // user
m_Slider2.AddColor(400,500,colYellow,colGreen);	// optimal range
m_Slider2.AddColor(500,600,colGreen,colYellow);
m_Slider2.AddColor(600,750,colYellow,colOrange);
m_Slider2.AddColor(750,maxRange,colOrange,colRed);// downright dangerous for
                                                  // user!
m_Slider2.Refresh();	// force screen update of newly configured slider

// sItemUpdate() is a static function that dispatches to ItemUpdate()
m_Slider2.setCallback(CSliderClrDemoView::sItemUpdate,this,
                      (LPARAM)IDC_SLIDER2);</FONT>
The <nobr>Refresh() isn't actually necessary in <nobr>OnInitialUpdate() but if you are changing colors during the operation of your program (like in the project of mine that started all of this), then you will need it.

Adding a Notification Callback

The callback stuff is sort of like what Windows does. I have declarations like this:

void ItemUpdate(LPARAM data1, int sValue, BOOL IsDragging);
static void sItemUpdate(CSliderClrDemoView *obj, LPARAM data1, int sValue,
                        BOOL IsDragging);
sItemUpdate is just to convert to the class's space of operations. The implementations are pretty simple:
void CSliderClrDemoView::sItemUpdate(CSliderClrDemoView *obj, LPARAM data1,
	int sValue, BOOL IsDragging)
{
	CSliderClrDemoView *me = (CSliderClrDemoView *)obj;
	me->ItemUpdate(data1, sValue, IsDragging);
}

void CSliderClrDemoView::ItemUpdate(LPARAM data1, int sValue, 
                                    BOOL /* IsDragging */)
{
    double slope1 = 0.05;
    double intercept1 = -25.0;
    double slope2 = 0.08;
    double intercept2 = -15.0;
    CString val;

    switch(data1)
    {
    case IDC_SLIDER1:
        val.Format("%6.2lf", (slope1 * double(sValue)) + intercept1);
        m_Edit1.SetWindowText(val);
        break;

    case IDC_SLIDER2:
        val.Format("%6.2lf", (slope2 * double(sValue)) + intercept2);
        m_Edit2.SetWindowText(val);
        break;

    case IDC_SLIDER3:
        val.Format("%6.2lf", (slope2 * double(sValue)) + intercept2);
        m_Edit3.SetWindowText(val);
        break;
    }
}

History

  • November 25, 2002 -- Initial posting to (an unsuspecting) CodeProject

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Thales Visionix
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionI want to appreciate you and your code Pin
Member 112155136-Jan-15 18:48
Member 112155136-Jan-15 18:48 
GeneralDoesn't support UNICODE [modified] Pin
chocm5-Jun-07 5:46
chocm5-Jun-07 5:46 
GeneralRe: Doesn't support UNICODE Pin
Harold Bamford5-Jun-07 5:52
Harold Bamford5-Jun-07 5:52 
GeneralRe: Doesn't support UNICODE Pin
chocm12-Jul-07 19:14
chocm12-Jul-07 19:14 
GeneralRe: Doesn't support UNICODE Pin
Harold Bamford13-Jul-07 4:13
Harold Bamford13-Jul-07 4:13 
GeneralCenter Line and Tick Marks Pin
Member 211465319-Jul-05 3:36
Member 211465319-Jul-05 3:36 
GeneralRe: Center Line and Tick Marks Pin
Harold Bamford19-Jul-05 6:22
Harold Bamford19-Jul-05 6:22 
GeneralRe: Center Line and Tick Marks Pin
Member 211465319-Jul-05 22:23
Member 211465319-Jul-05 22:23 
GeneralRe: Center Line and Tick Marks Pin
Harold Bamford21-Jul-05 4:18
Harold Bamford21-Jul-05 4:18 
GeneralDLL in VB Pin
pichbe10-Mar-05 21:43
pichbe10-Mar-05 21:43 
GeneralRe: DLL in VB Pin
Harold Bamford21-Mar-05 4:11
Harold Bamford21-Mar-05 4:11 
GeneralWorks beautifully for a motion threshold selection dialog. Pin
Member 7997384-Jan-04 22:38
Member 7997384-Jan-04 22:38 
GeneralVC++.NET problem Pin
Jason Troitsky (was Hattingh)26-Aug-03 23:41
Jason Troitsky (was Hattingh)26-Aug-03 23:41 
GeneralRe: VC++.NET problem Pin
Harold Bamford3-Sep-03 7:13
Harold Bamford3-Sep-03 7:13 
GeneralRe: VC++.NET problem Pin
Anonymous28-Nov-03 22:20
Anonymous28-Nov-03 22:20 
GeneralThank you, works fine! Pin
Spolm3-Jul-03 21:41
Spolm3-Jul-03 21:41 
Generalchanging size of ThumbRect Pin
luedi11-Mar-03 4:56
luedi11-Mar-03 4:56 
GeneralRe: changing size of ThumbRect Pin
Harold Bamford11-Mar-03 11:04
Harold Bamford11-Mar-03 11:04 
GeneralRe: changing size of ThumbRect Pin
luedi12-Mar-03 1:07
luedi12-Mar-03 1:07 
GeneralChannel sometimes does not display Pin
Harold Bamford16-Jan-03 4:51
Harold Bamford16-Jan-03 4:51 
GeneralRe: Channel sometimes does not display Pin
Ocrana20-Apr-03 7:39
Ocrana20-Apr-03 7:39 
GeneralCongratulations! Pin
Miguel Lopes2-Dec-02 13:41
Miguel Lopes2-Dec-02 13:41 
GeneralRe: Congratulations! Pin
Duc Truong3-Dec-02 12:12
Duc Truong3-Dec-02 12:12 
GeneralRe: Congratulations! Pin
joyjjjz22-Sep-08 22:30
joyjjjz22-Sep-08 22:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.