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

CSliderCtrlEx - A slider with background colors to indicate ranges

0.00/5 (No votes)
24 Nov 2002 1  
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 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 ColorNames.h. It sure is easier to read and understand code that has things like colOrange rather than 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!) 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
        {
            // We want to be informed when each part of the control is being

            // processed so we can insert the colors before drawing the thumb

            *pResult = CDRF_NOTIFYITEMDRAW;	// send messages for each

                                                // piece-part

        }





        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 GetChannelRect() function that returns the channel that the thumb slides in. There is also a GetThumbRect() function. Fine, I can do the math. But there this is this little gotcha:

// For unknown reasons, GetChannelRect() returns a rectangle

// as though it were a horizonally oriented slider, even if it isn't!

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."



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


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


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 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);

// 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);
The Refresh() isn't actually necessary in 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 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