CSliderCtrlEx - A slider with background colors to indicate ranges






4.92/5 (8 votes)
This slider allows colors/gradients to be used to indicate good, bad, or marginal ranges
Contents
- Introduction
- Acknowledgements
- Features of
CSliderCtrlEx
- How it Works
- Saving the Tics
- Munge in Memory Space, Not Screen Space
- Where to Draw?
- Drawing in the Colors
- ReApplying the Tics
- Using the code
- Adding to the Project
- Adding Color Ranges
- Adding a Notification Callback
- History
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.
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 Ales Krajnc wrote an article, gdi/colornames.asp, that I made use of by copying 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!) There are two main features for this control:
The control is painted in many steps. Fortunately, there is a way to get notified between important steps by using the 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: 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: 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. 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 If there is no gradient (start and end colors are identical) then If I just left things like this, then the colors would be correct, but control would look ugly: 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 To add In The callback stuff is sort of like what Windows does. I have declarations like this:CSliderCtrlEx
is derived from CSliderCtrl
and can be dropped into your project easily.
Acknowledgements
msimg32.dll
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).
ColorNames.h
colOrange
rather than RGB(255,165,0)
SaveBitmap()
Features of
CSliderCtrlEx
BOOL AddColor(int nLow, int nHigh, COLORREF color);
BOOL AddColor(int nLow, int nHigh, COLORREF strColor, COLORREF endColor);
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
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
// 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):
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
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:
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?
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()
GetThumbRect()
// 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
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...
GradientFill
does the reasonable thing: a solid fill.
What I needed to do was fill out the ends: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
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
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
OnInitialUpdate()
// 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()
OnInitialUpdate()
Adding a Notification Callback
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