|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionAlthough Windows comes with a great variety of common controls such Edit controls and Combo Boxes, many times their functionality is limited, their appearance unsuitable, or they simply do not fit our needs. To solve this problem, a new custom control can be created or we can subclass an existing one, in many cases reducing the amount of work. In this tutorial, we will subclass an instance of the The Birth of a New ClassFirst we must start a new project so that we can develop and debug the new class. After it is polished, it can be easily added to any project. In my case, I created an MFC Dialog and named it ListDemo. Now we must create a new derived class with
Now click OK and we are ready to begin. Go to the Dialog Editor and add a list box, the one that we will subclass. We will use this list to test our code. Right click and go to properties and in the Styles tab, where it says Owner Draw, set it to Variable. Whenever we want to subclass a control, we must make it Owner Drawn. Since a list box consists of several items, we set it to variable so that a function,
Go again to the ClassWizard and click on the Member Variables tab. Choose the ID of the list box and click on Add Variable. Make sure you select the Category as control and the Variable type as
Before we begin, let's add the From here on, we will proceed as following:
Coding: The Fun PartNow we are ready to undertake the great journey into the mystifying code. Not really. Thanks to subclassing, it is all relatively easy. In windowless controls such as the one we are using, the On the ClassWizard, select void CListBoxEx::PreSubclassWindow() { // TODO: Add your specialized code here and/or call the base class CListBox::PreSubclassWindow(); } We are now ready to draw the borders. 1. The ListBox FrameWe want to make a border so that it looks as a normal one but when the mouse cursor enters the list box, it will change, thus making it more interactive. You may want to check the picture on the top. As you can see, the border that surrounds the first list, which has the mouse cursor over, seems to be darker and 2D while the second list gives the impression that it is 3D and pushed backwards. The first thing that we must do is to create a variable that will keep track of whether the mouse is over. Let's call it
We must now make a function to draw the borders. Use the following declaration under protected: void CListBoxEx::DrawBorders()
{
}
We now start typing the code: void CListBoxEx::DrawBorders() { //Gets the Controls device context used for drawing CDC *pDC=GetDC(); //Gets the size of the control's client area CRect rect; GetClientRect(rect); /* Inflates the size of rect by the size of the default border Suppose rect is (0,0,100,200) and the default border is 2 pixels, after InflateRect, rect should be (-2,-2, 102,202) and the border will be drawn from -2 to 0, -2 -> 0, 102->100, 202->200. */ rect.InflateRect(CSize(GetSystemMetrics(SM_CXEDGE), GetSystemMetrics(SM_CYEDGE))); //Draws the edge of the border depending on whether the mouse is //over or not if (m_bOver)pDC->DrawEdge(rect,EDGE_BUMP ,BF_RECT ); else pDC->DrawEdge(rect,EDGE_SUNKEN,BF_RECT ); ReleaseDC(pDC); //Frees the DC } The function The code above won't have any effect yet. We still have to determine when the mouse enters and when it leaves so that we can modify m_bOver. We also need to call One of the various methods to figure out when the mouse enters is to use the message handler for WM_MOUSEMOVE and if m_bOver is void CListBoxEx::OnMouseMove(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default //If m_bOver==FALSE, and this function is called, it means that the //mouse entered. if (!m_bOver){ m_bOver=TRUE; //Now the mouse is over DrawBorders(); //Self explanatory } CListBox::OnMouseMove(nFlags, point); } If you run the program now, you'll notice that the border changes when the mouse enters but not when it leaves. That's because we must determine when to set m_bOver to However, it is not that simple to determine when the mouse leaves the area since the listbox won't be notified of outside movement. So far, I know of three ways to do this: using a timer, capturing the mouse, or manually adding a Since ther is no macro implemented for this message, we will have to do a little bit of work. Find: BEGIN_MESSAGE_MAP(CListBoxEx, CListBox) and after BEGIN_MESSAGE_MAP(CListBoxEx, CListBox)
//{{AFX_MSG_MAP(CListBoxEx)
ON_WM_MOUSEMOVE()
ON_MESSAGE(WM_MOUSELEAVE,OnMouseLeave) //Add this
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
Notice that no semicolon is needed. What we basically did was use the //{{AFX_MSG(CListBoxEx) afx_msg void OnMouseMove(UINT nFlags, CPoint point); //}}AFX_MSG Then insert //{{AFX_MSG(CListBoxEx) afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg LRESULT OnMouseLeave(WPARAM wParam, LPARAM lParam); //Add this //}}AFX_MSG We proceed by implementing the function on ListBoxEx.cpp: LRESULT CListBoxEx::OnMouseLeave(WPARAM wParam, LPARAM lParam){
m_bOver=FALSE;
DrawBorders();
return 0;
}
Since the mouse is no longer over, we set if (!m_bOver){ m_bOver=TRUE; //Now the mouse is over DrawBorders(); //Self explanatory //Add here... TRACKMOUSEEVENT track; //Declares structure track.cbSize=sizeof(track); track.dwFlags=TME_LEAVE; //Notify us when the mouse leaves track.hwndTrack=m_hWnd; //Assigns this window's hwnd TrackMouseEvent(&track); //Tracks the events like WM_MOUSELEAVE } To conclude this section, add
2. The BackgroundSetting a color for the background is one of the easiest things. Instead of providing an RGB color, we create a brush using In order to change the color, we must have //Declare under public in header file void SetBkColor( COLORREF crBkColor, COLORREF crSelectedColor = GetSysColor(COLOR_HIGHLIGHT)); Now when we wish to change the color, we can simply call void CListBoxEx::SetBkColor(COLORREF crBkColor,COLORREF crSelectedColor) { //Deletes previous brush. Must do in order to create a new one m_BkBrush.DeleteObject(); //Sets the brush the specified background color m_BkBrush.CreateSolidBrush(crBkColor); Invalidate(); //Forces Redraw } The We should also make sure that we change the brush if the control is not enabled. HBRUSH CListBoxEx::CtlColor(CDC* pDC, UINT nCtlColor)
{
// TODO: Change any attributes of the DC here
if (!IsWindowEnabled()){
CBrush br(GetSysColor(COLOR_INACTIVEBORDER));
return br;
}
// TODO: Return a non-NULL brush if the parent's handler should not
// be called
return m_BkBrush;
}
The last thing is to delete the brush when it exits. We will do this in the destructor ( CListBoxEx::~CListBoxEx()
{
m_BkBrush.DeleteObject(); //Deletes the brush
}
Now that all's done, we must try it. On m_DemoList.SetBkColor(RGB(0,0,128)); This will set the background to dark blue.
There may be times when the control is enabled or disabled at run-time. As a result, we should receive the void CListBoxEx::OnEnable(BOOL bEnable) { CListBox::OnEnable(bEnable); // TODO: Add your message handler code here Invalidate(); } 3. The ItemsThis is probably the longest section. We have several goals. Among them are life, liberty, and the pursue of happiness. Anyways, we want to make the list so it displays colored text and display a bitmap for each item, if desired. When the user makes a selection, the selected item must also be highlighted. We are going to declare various variables now. We need one to track the color of the text, the color of the text when highlighted, the size of each item, the background color of the item when selected, and the dimensions of the bitmaps to be used. All of these should be under the protected section. short m_ItemHeight; //Height of each item COLORREF m_crTextHlt; //Color of the text when highlighted COLORREF m_crTextClr; //Color of the text COLORREF m_HBkColor; //Color of the highlighted item background int m_BmpWidth; //Width of the bitmap int m_BmpHeight; //Height of the bitmap We then set them to an initial value under m_bOver = FALSE; m_ItemHeight=18; m_crTextHlt=GetSysColor(COLOR_HIGHLIGHTTEXT); m_crTextClr=GetSysColor(COLOR_WINDOWTEXT); m_HBkColor=GetSysColor(COLOR_HIGHLIGHT); m_BmpWidth=16; m_BmpHeight=16; To set the height of each item, we must overwrite void CListBoxEx::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // TODO: Add your code to determine the size of specified item lpMeasureItemStruct->itemHeight=m_ItemHeight; } We need to now add a function which allows the modification of void CListBoxEx::SetItemHeight(int newHeight) { m_ItemHeight=newHeight; Invalidate(); } Before we begin painting each individual item, we must create a function that takes a string and the resource ID of the bitmap. We will call it We will use the void CListBoxEx::AddItem(UINT IconID, LPCTSTR lpszText) { //Adds a string ans assigns nIndex the index of the current item int nIndex=AddString(lpszText); //If no error, associates the index with the bitmap ID if (nIndex!=LB_ERR&&nIndex!=LB_ERRSPACE) SetItemData(nIndex, IconID); } To expand its usability, we would also like to give it the ability to insert items in a specified index, thus shifting down the rest of the cells. This one would be similar to AddItem execpt it will receive one more variable, the index where to inserted it. The prototype will be void CListBoxEx::InsertItem(int nIndex, UINT nID, LPCTSTR lpszText) { int result=InsertString(nIndex,lpszText); //Inserts the string //Associates the ID with the index if (result!=LB_ERR||result!=LB_ERRSPACE) SetItemData(nIndex,nID); } If you looked carefully at the image of the three listboxes at the beginning, you should have seen that normal text can be added, just like in any list box. Also, you can add text that is indented but does not have any picture. To do this, you have to enter a special value as the ID. Let's make so that if the ID is #define NO_BMP_ITEM 0 #define BLANK_BMP 1 Note: We assign 0 to The message void CListBoxEx::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // TODO: Add your message handler code here } The structure Here's how we will draw the item:
We first need to get the DC (Device Context) from the structure, along with several other information. Add: CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); //Gets the item DC //Retrieves the ID added using SetItemData UINT nID=(UINT)lpDrawItemStruct->itemData; CRect rect=lpDrawItemStruct->rcItem; //Gets the rect of the item UINT action=lpDrawItemStruct->itemAction; //What it wants to do UINT state=lpDrawItemStruct->itemState; //The item current state COLORREF TextColor=m_crTextClr; //Text color that we'll use The field Here are the actions and states that we should take into account. At first they are a bit confusing but with practice, you should understand them. Insert: //Action statements if ((state & ODS_SELECTED) && (action & ODA_SELECT)) //Used when an item needs to be selected { //Since it will be highlighted, we create a brush with the //highlighted color CBrush brush(m_HBkColor); //Draws the highlighted rect pDC->FillRect(rect, &brush); } if (!(state & ODS_SELECTED) && (action & ODA_SELECT)) //The item needs to be deselected { //We draw the background color pDC->FillRect(rect, &m_BkBrush); } if ((action & ODA_FOCUS) && (state & ODS_FOCUS)&&(state&ODS_SELECTED)){ //It has the focus, //Draws a 3D focus rect pDC->Draw3dRect(rect,RGB(255,255,255),RGB(0,0,0)); TextColor=m_crTextHlt; } if ((action & ODA_FOCUS) && !(state & ODS_FOCUS)&&(state&ODS_SELECTED)){ //If the focus needs to be removed. CBrush brush(m_HBkColor); pDC->FillRect(rect, &brush); TextColor=m_crTextHlt; } //If the control is disabled if (state&ODS_DISABLED) TextColor=GetSysColor(COLOR_3DSHADOW); Now we must retrieve the text to display, and set its color and background mode. CString text; GetText(lpDrawItemStruct->itemID, text); //Gets the item text pDC->SetTextColor(TextColor); //No need to explain pDC->SetBkMode(TRANSPARENT); //Sets text background transparent We are now ready to draw the bitmap and display the text. if (nID!=NO_BMP_ITEM){ //If the item has a bitmap CDC dcMem; //New device context used as the source DC //Creates a deice context compatible to pDC dcMem.CreateCompatibleDC(pDC); CBitmap bmp; //Bitmap object //Loads the bitmap with the specified resource ID bmp.LoadBitmap(nID); //Saves the old bitmap object so that the GDI resources are not //depleted CBitmap* oldbmp=dcMem.SelectObject(&bmp); if (nID!=BLANK_BMP) //Draws the bitmap if it is not blank //Copies the bitmap to the screen pDC->BitBlt(rect.left+5,rect.top,m_BmpWidth,m_BmpHeight, &dcMem,0,0,SRCCOPY); //Selects the saved bitmap object dcMem.SelectObject(oldbmp); bmp.DeleteObject(); //Deletes the bitmap //Displays the text pDC->TextOut(rect.left+10+m_BmpWidth,rect.top,text); } //Displays the text without indenting it else pDC->TextOut(rect.left+5,rect.top,text); We are done with most of the code in this section. Nonetheless, we are still missing some member functions such as that to change the color of the text. Go back to In the header file, add the following prototype in the public entity, void CListBoxEx::SetTextColor(COLORREF crTextColor, COLORREF crHighlight)
{
m_crTextClr=crTextColor;
m_crTextHlt=crHighlight;
Invalidate();
}
Finally, we need to be able to alter the dimensions of the bitmap. Declare the appropriate prototype for: void CListBoxEx::SetBMPSize(int Height, int Width) { m_BmpHeight=Height; m_BmpWidth=Width; Invalidate(); } Done! It's time to test it: Go back to ListDemoDlg.cpp and m_DemoList.SetBkColor(RGB(0,0,128),RGB(190,0,0)); m_DemoList.SetBMPSize(16,30); m_DemoList.SetTextColor(RGB(0,255,10),RGB(255,255,0)); m_DemoList.SetItemHeight(17); m_DemoList.AddString("Hey World"); m_DemoList.AddItem (IDB_COOL,"Hello World!"); m_DemoList.AddItem (NO_BMP_ITEM,"Hi World!"); m_DemoList.InsertItem(2,BLANK_BMP, "Greetings"); This examines every function we have made so far. The ID If you run it, you will get the following when its focused and the mouse is over:
The items are sorted except when you use We can now continue into the final part, the scrollbar. 4. ScrollbarsFor simplicity purposes. the scrollbars that we are going to make are going to be static, always shown regardless of whether they are needed. I don't think we are using the correct term since they don't have bars but who cares. As we all know, we must draw them. However, the problem is how to do it so that it is within the listbox rect and does not cover any item. There's a simple solution, we can resize the client area. This can be done by receiving the messageWM_NCCALCSIZE. Add a function for it, and we get:
void CListBoxEx::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS FAR* lpncsp) { // TODO: Add your message handler code here and/or call default CListBox::OnNcCalcSize(bCalcValidRects, lpncsp); } The argument lpncsp->rgrc[0].top += 16; //Top lpncsp->rgrc[0].bottom -= 16; //Bottom
In most cases, this function will not be called automatically. We will call the void CListBoxEx::OnNcPaint() { // TODO: Add your message handler code here static BOOL before=FALSE; if (!before) { //If first time, the OnNcCalcSize function will be called SetWindowPos(NULL,0,0,0,0, SWP_FRAMECHANGED|SWP_NOMOVE|SWP_NOSIZE); before=TRUE; } DrawBorders(); // Do not call CListBox::OnNcPaint() for painting messages } It it now time to create a protected function that draws the scrollbars: // ListBoxEx.h : header file // #define NO_BMP_ITEM 0 #define BLANK_BMP 1 #define SC_UP 2 //Up scroll #define SC_DOWN 3 //Down Scroll #define SC_NORMAL NULL //Normal scroll #define SC_PRESSED DFCS_PUSHED //The scroll is pressed #define SC_DISABLED DFCS_INACTIVE //The scroll is disabled ///////////////////////////////////////////////////////////////////////////// // CListBoxEx window Things like void CListBoxEx::DrawScrolls(UINT WhichOne, UINT State) { CDC *pDC=GetDC(); CRect rect; GetClientRect(rect); //Gets the dimensions //If the window is not enabled, set state to disabled if (!IsWindowEnabled())State=SC_DISABLED; //Expands the so that it does not draw over the borders rect.left-=GetSystemMetrics(SM_CYEDGE); rect.right+=GetSystemMetrics(SM_CXEDGE); if (WhichOne==SC_UP){ //The one to draw is the up one //Calculates the rect of the up scroll rect.bottom=rect.top-GetSystemMetrics(SM_CXEDGE); rect.top=rect.top-16-GetSystemMetrics(SM_CXEDGE); //Draws the scroll up pDC->DrawFrameControl(rect,DFC_SCROLL,State|DFCS_SCROLLUP); } else{ //Needs to draw down rect.top=rect.bottom+GetSystemMetrics(SM_CXEDGE);; rect.bottom=rect.bottom+16+GetSystemMetrics(SM_CXEDGE); pDC->DrawFrameControl(rect,DFC_SCROLL,State|DFCS_SCROLLDOWN); } ReleaseDC(pDC); }
DrawScrolls(SC_UP,SC_NORMAL);
DrawScrolls(SC_DOWN,SC_NORMAL);
We should get:
Now we must make it scroll and change the appearance of the scrollbar when it is pressed. Since it is not a border or default scroll bar, non-client messages such as The return value is where the mouse located. In order to use UINT CListBoxEx::OnNcHitTest(CPoint point)
{
// TODO: Add your message handler code here and/or call default
CRect rect,top,bottom;
//Gets the windows rect, relative to the parent, so rect.left and
//rect.top might not be 0.
GetWindowRect(rect);
ScreenToClient(rect); //Converts the rect to the client
//Calculates the rect of the bottom and top scrolls
top=bottom=rect;
top.bottom=rect.top+16;
bottom.top=rect.bottom-16;
//Obtains where the mouse is
UINT where = CListBox::OnNcHitTest(point);
//Converts the point so its relative to the client area
ScreenToClient(&point);
if (where == HTNOWHERE) //If mouse is not in a place it recognizes
if (top.PtInRect(point))
//Check to see if the mouse is on the top
where = HTVSCROLL;
else if (bottom.PtInRect(point))
//Check to see if its on the bottom
where=HTHSCROLL;
return where; //Returns where it is
}
Add a handler for We will use the We will have: void CListBoxEx::OnNcLButtonDown(UINT nHitTest, CPoint point) { // TODO: Add your message handler code here and/or call default if (nHitTest==HTVSCROLL) //Up scroll Pressed { DrawScrolls(SC_UP,SC_PRESSED); //Scroll up 1 line SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEUP,0),0); SetTimer(1,100,NULL); //Sets the timer ID 1 } else if (nHitTest==HTHSCROLL) //Down scroll Pressed { DrawScrolls(SC_DOWN,SC_PRESSED); //Scroll down 1 line SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEDOWN,0),0); SetTimer(2,100,NULL); //Sets the timer ID 2 } CListBox::OnNcLButtonDown(nHitTest, point); } Of course, we must now add a void CListBoxEx::OnTimer(UINT nIDEvent) { // TODO: Add your message handler code here and/or call default //Gets the state of the left button to see if it is pressed short result=GetKeyState(VK_LBUTTON); if (nIDEvent==1){ //Up timer //If it returns negative then it is pressed if (result<0){ SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEUP,0),0); } else { //No longer pressed KillTimer(1); DrawScrolls(SC_UP,SC_NORMAL); } } else { //Down timer //If it returns negative then it is pressed if (result<0){ SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEDOWN,0),0); } else { KillTimer(2); DrawScrolls(SC_DOWN,SC_NORMAL); } } CListBox::OnTimer(nIDEvent); } At last, we are finished with We should now check to see if the scrollbars work correctly. Therefore, let's add more items. You can do a loop in m_DemoList.SetBkColor(RGB(0,0,128),RGB(190,0,0)); m_DemoList.SetBMPSize(16,30); m_DemoList.SetTextColor(RGB(0,255,10),RGB(255,255,0)); m_DemoList.SetItemHeight(17); for (int i=0;i<=5;i++){ m_DemoList.AddString("Hey World"); m_DemoList.AddItem (IDB_COOL,"Hello World!"); m_DemoList.AddItem (NO_BMP_ITEM,"Hi World!"); m_DemoList.InsertItem(2,BLANK_BMP, "Greetings"); m_DemoList.AddItem (IDB_COOL,"Vacation's Great!"); } While clicking the down scroll bar, we should get something like this: (*I unchecked the Sort property)
You should remember that throughout the code, we added a few lines in case the control is disabled. Go back to //SC_NORMAL will be changed to SC_DISABLED if the window is disabled DrawScrolls(SC_UP,SC_NORMAL); DrawScrolls(SC_DOWN,SC_NORMAL); We must disable the control and see if it work. We'll get this:
ConclusionNow that the code is complete, it can be easily integrated into other projects by adding the source code files to the project. Then you should include the header file and instead of creating a As you should have seen, subclassing is not as difficult as it might have seemed at first. It just requires a little bit of knowledge, patience, and practice. Sometimes, you need to rely on other tools when subclassing. For instance, there are many cases in which you might not be sure what messages a section might be receiving. For this, you can use Spy++ found in the Tools menu. Other times, the TRACE macro is really useful for catching small bugs that lurk behind most code. Now you should be able to apply this knowledge to other controls since the framework is almost identical. Have fun programming.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||