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();
LPNMCUSTOMDRAW lpCustDraw = (LPNMCUSTOMDRAW)pNMHDR;
if(lpCustDraw->dwDrawStage == CDDS_PREPAINT)
{
int curVal = GetPos();
if((m_Callback != NULL) && (curVal != m_oldPosition))
{
m_oldPosition = curVal;
m_Callback(m_p2Object, m_data1, curVal, m_IsDragging);
}
if(loopMax <= 0)
{
*pResult = CDRF_DODEFAULT;
}
else
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
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:
CRect crect;
GetClientRect(crect);
CDC *pDC = CDC::FromHandle(lpCustDraw->hdc);
CDC SaveCDC;
CBitmap SaveCBmp;
COLORREF crOldBack = pDC->SetBkColor(RGB(0,0,0));
COLORREF crOldText = pDC->SetTextColor(RGB(255,255,255));
int iWidth = crect.Width();
int iHeight = crect.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)
{
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
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);
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:
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);
}
int Offset = chanRect.left + thmbRect.Width()/2;
if(IsVertical)
{
Offset = chanRect.top + thmbRect.Height()/2;
}
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];
GRADIENT_RECT gRect;
vert[0].Red = sR<<8;
vert[0].Green = sG<<8;
vert[0].Blue = sB<<8;
vert[0].Alpha = 0;
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)
{
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:
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
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());
memDC.SetTextColor(pDC->GetTextColor());
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.
pDC->BitBlt(0,0,iWidth,iHeight,&memDC,0,0,SRCCOPY);