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.
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!)
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
if(loopMax <= 0)
*pResult = CDRF_DODEFAULT;
<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
}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;
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
CDC *pDC = CDC::FromHandle(lpCustDraw->hdc);
//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
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
}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
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:
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?
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.
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:
<FONT color=red>// For unknown reasons, GetChannelRect() returns a rectangle
// as though it were a horizonally oriented slider, even if it isn't!</FONT>
n.left = chanRect.top;
n.right = chanRect.bottom;
n.top = chanRect.left;
n.bottom = chanRect.right;
// Offset into client rectangle for beginning of color range
int Offset = chanRect.left + thmbRect.Width()/2;
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; // for specifying range to gradient fill
vert.Red = sR<<8; // expects 16-bit color values!
vert.Green = sG<<8;
vert.Blue = sB<<8;
vert.Alpha = 0; // no fading/transparency
vert.Red = eR<<8;
vert.Green = eG<<8;
vert.Blue = eB<<8;
vert.Alpha = 0;
gRect.UpperLeft = 0;
gRect.LowerRight = 1;
if(IsVertical) // vertically oriented?
vert.x = 0;
vert.y = Offset + minVal;
vert.x = iWidth;
vert.y = Offset + minVal + widthVal;
retval = GradientFill(memDC,vert,2,&gRect,1,GRADIENT_FILL_RECT_V);
vert.x = Offset + minVal;
vert.y = 0;
vert.x = Offset + minVal + widthVal;
vert.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:
What I needed to do was fill out the ends:
memDC.FillSolidRect(0, 0, iWidth, Offset, startColor);
memDC.FillSolidRect(0,iHeight - Offset - 1,iWidth, Offset, endColor);
memDC.FillSolidRect(0, 0, Offset, iHeight, startColor);
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
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
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"
<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
m_Slider2.AddColor(400,500,colYellow,colGreen); // optimal range
m_Slider2.AddColor(750,maxRange,colOrange,colRed);// downright dangerous for
m_Slider2.Refresh(); // force screen update of newly configured slider
// sItemUpdate() is a static function that dispatches to ItemUpdate()
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,
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;
val.Format("%6.2lf", (slope1 * double(sValue)) + intercept1);
val.Format("%6.2lf", (slope2 * double(sValue)) + intercept2);
val.Format("%6.2lf", (slope2 * double(sValue)) + intercept2);