Wednesday, December 05, 2012

[ MFC ] - Creating Owner Draw Menu in VC++ using WM_DRAWITEM, WM_MEASUREITEM


1. Introduction


When an owner window of the menu item decides how the menu item should look, then the menu item is known as Owner Drawn menu item. The default windows provided menu has a standard look and feel. We cannot add Red and blue boxes as menu item through the resource editor. In this article, we will see how can we display only color boxes as a menu item under the color menu.



2.  Create the SDI Application


The first thing is creating the MFC SDI Application. In the app wizard, select Single Document Interface option and make sure to uncheck the Document/View architecture support check box. This is shown in the below picture.



In the class files provided by the application wizard, we are going to draw the owner-draw menu items through CMainFrame class. Once the application is created, remove the unwanted menus from the menu bar of the mainframe window. Then add the color menu at the end of the menu bar. To this color menu, add three menu items named Red, Blue, and Green. This is shown in the below video:

Video 1: Adding the new menu items


Note that the menu items added in the above video are not an owner draw menu. We will make these menu items as an owner-draw menu item at runtime. Once the three menu items are added, name it as ID_COLOR_RED, ID_COLOR_BLUE, and ID_COLOR_GREEN. Assigning the ID to the menu item Green is shown in the below picture.


At this stage, running the application will display the standard menu items with texts Red, Blue, and Green. Let us move to coding part. To follow with me, search inside the downloaded sample with the search tag //Sample <no>

3. Change the Menu items as Owner Drawn

Video 1 shows that we already added three menu items to the color menu. However, these menu items are not owner drawn. To make it owner draw we should modify the menu items by calling the ModifyMenu on the CMenu. Add the below-specified code (Specified in blue color. Tag is Sample 01) in the int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)

         if (!m_wndStatusBar.Create(this) ||
                 !m_wndStatusBar.SetIndicators(indicators,
                   sizeof(indicators)/sizeof(UINT)))
         {
                 TRACE0("Failed to create status bar\n");
                 return -1;      // fail to create
         }
         //Sample 01: Modify the Menu as OwnerDrawn
         CMenu * pMainFrame_menu = this->GetMenu();
         pMainFrame_menu->ModifyMenu(ID_COLOR_RED, MF_BYCOMMAND | MF_OWNERDRAW, ID_COLOR_RED);
         pMainFrame_menu->ModifyMenu(ID_COLOR_BLUE, MF_BYCOMMAND | MF_OWNERDRAW, ID_COLOR_BLUE);
         pMainFrame_menu->ModifyMenu(ID_COLOR_GREEN, MF_BYCOMMAND | MF_OWNERDRAW, ID_COLOR_GREEN);
         // TODO: Delete these three lines if you don't want the toolbar to be dockable
         m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);

The code added (Sample 01) above will modify three menu items of the “color” as owner drawn menu. The first parameter says for example ID_COLOR_RED specifies the menu item that we are modifying. In the second parameter, we specified that the first parameter given is a command id of the menu item by supplying the flag MF_BYCOMMAND. You can also specify a menu item by its position in that case a first parameter is a position number like 0,1,2 etc and the second parameter supplies the flag MF_BYPOSITION. In the second parameter, we also supplied one more flag MF_OWNERDRAW that makes the menu item as an owner-drawn menu item. In the third parameter, we can change the command id of the menu item after the modification. In our case, we specified the change in the second parameter and kept the same id.
Here, we created the menu first, then used the ModifyMenu function call to change the menu items as owner drawn. You can also use the AppendMenu or InsertMenu with the MF_OWNERDRAW flag. In both the cases, we are creating the Menu at runtime.

4. One Handler – Three menu items

We are going to add single handler function for all three modified menu items. First thing is checking the order of the menu item ids that is lower to higher order with continues id numbers. In my case, ID_COLOR_RED is lower id and ID_COLOR_GREEN is higher id. You can check the ID ranges from Resource.h as shown in the below picture.


Once we have the ID sequence, make an entry in the message map as shown below:

         //Sample 02: Hanlder for the Owner drawn menu
         ON_COMMAND_RANGE(ID_COLOR_RED, ID_COLOR_GREEN, OnOwnerDMenuClick )

Here, we specified that the handler for all the commands in between the range from ID_COLOR_RED to ID_COLOR_GREEN, the handler function is OnOwnerDMenuClick.

In the MainFrame.h header file add the handler function declaration shown below:

//Sample 03: Declare the Handler function
void OnOwnerDMenuClick(UINT cid);
Below is the implementation for the handler function:
//Sample 04: Handler for the Menu Items
void CMainFrame::OnOwnerDMenuClick(UINT cid)
{
         if (cid == ID_COLOR_RED)
         {
                 AfxMessageBox(_T("Red Button Clicked"));
         }
         if (cid == ID_COLOR_BLUE)
         {
                 AfxMessageBox(_T("Blue Button Clicked"));
         }
         if (cid == ID_COLOR_GREEN)
         {
                 AfxMessageBox(_T("Green Button Clicked"));
         }
}

The above handler function displays the message boxes, which corresponds to the clicked menu item. In the above function we checked the cid and that carries the command id of the clicked menu item.

5. WM_MEASUREITEM and WM_DRAWITEM

When a menu item is specified as owner drawn menu, the owner that is the CMainFrame window will receive the WM_DRAWITEM and WM_MEASUREITEM window messages. The WM_MEASUREITEM message will be sent only once for each owner draw menu item that means for the first time the menu containing the menu items is opened. The handler for the measure item will set the dimensions required for drawing those menus.
The window message WM_DRAWITEM will be sent for every owner draw menu item whenever the menu containing the owner draw menu item is opened. Therefore, the owner of the menu items actually draws the menu item here by making the measurements taken in the handler for the WM_MEASUREITEM.
For example, let us take that user clicks the color menu item three times. For the first time the CFrameWnd will receive both the window messages. For second and third times, it will receive only the WM_DRAWITEM windows message.

The Below video shows providing the handler function in CFrameWnd for both the window messages:

Video 2: Providing the Handler functions

6. Implementing OnMeasureItem

In the previous video, we saw the handler function for both the window messages discussed in the previous section of this article. Add the below piece of code in the OnMeasureItem Handler.

void CMainFrame::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
         //Sample 05: Provide Height and widths
         UINT height = 20;
         UINT width = 40;
         lpMeasureItemStruct->itemHeight = height;
         lpMeasureItemStruct->itemWidth = width;
         CFrameWnd::OnMeasureItem(nIDCtl, lpMeasureItemStruct);
}

In the above handler function, we specified the width and height of our owner drawn menu item with a hard-coded value in the LPMEASUREITEMSTRUCT.  Therefore, in our case, the three menu items will have same width and height. In some cases the width and height changes for each menu item. For Ex: displaying the name of the font in the same Font. This changes the font width and height based on the displayed font even though the text displayed is same.

7. Implementing OnDrawItem

Look at the handler function signature of the OnDrawItem function shown below:

void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
The measurement done for each menu item in the OnMeasureItem() can be retrieved here using the lpDrawItemStruct. Before we write the code inside the OnDrawItem(), we explore some important members of the lpDrawItemStruct.

·       The itemId member of this structure holds menu item id. Using this we can know what menu item we are drawing now. If we want to draw some special effect for a particular menu item, this member will be useful.
·       The rcItem member provides the rectangular structure, which can be used as the dimension required for drawing the menu item.
·       The hDC member is used for Drawing the shapes using the GDI objects.
·       The itemState member informs the state of the menu item. We can check the state using the bitwise & operator with constants ODS_CHECKED, ODS_DISABLED, ODS_GRAYED and ODS_SELECTED.

1) First thing we did was creating a device context for the drawing. Once the device context dc is created in the stack, we attach this MDC object (dc) to the win32 handle hDC taken from the lpDrawItemStruct.

//6.1: Get the device context from the Draw Item structure and attach that to a dc object
CDC menu_dc;
menu_dc.Attach(lpDrawItemStruct->hDC);

2) When the menu item is selected, we are going to draw a border in black color around it. In addition, we will draw the border around non-selected menu items in the background color of the menu. The Win32 API call, GetSysColor(COLOR_MENU); will give use the background menu color.  Drawing around the Non-Selected menu item is required to clear the already drawn black rectangle. To draw the rectangle FrameRect function is called on the device context. The ODS_SELECTED constant is checked against the itemstate of the menu item to decide the color of the brush.

//6.2: Test item state. When the item is selected, draw a black border over it.
//          When item is not selected draw the border in menu's background color
//          (Clears previous drawn border)
CBrush * brush;
RECT menu_item_rct = lpDrawItemStruct->rcItem ;
if ( lpDrawItemStruct->itemState & ODS_SELECTED )
         brush = new CBrush(RGB(0,0,0));
else
{
         DWORD color_index = ::GetSysColor(COLOR_MENU);
         brush = new CBrush(color_index);
}
menu_dc.FrameRect(&menu_item_rct, brush);
delete brush;

3) We check the itemID member with the menu item id constant for creating the color brushes matching the selected menu item. Once the required color brush is created, we fill the slightly diminished rectangle, measured in the OnMeasureItem with the color brush we created by checking the menu item id. The DeflateRect function will reduce the dimension of the rectangle and FillRect paints the rectangle using the brush specified. Once we are done with the drawing, the Win32 handle is detached from CDC object. Below is the code:

//6.3: Create the Item color and draw it
if (lpDrawItemStruct->itemID == ID_COLOR_RED)
         brush = new CBrush(RGB(255,0,0));
if (lpDrawItemStruct->itemID == ID_COLOR_BLUE)
         brush = new CBrush(RGB(0,0,255));
if (lpDrawItemStruct->itemID == ID_COLOR_GREEN)
         brush = new CBrush(RGB(0,255,0));
CRect menu_rct(menu_item_rct);
menu_rct.DeflateRect(1,2);
menu_dc.FillRect( menu_rct, brush );
delete brush;
//6.4: Detach win32 handle
menu_dc.Detach();
CFrameWnd::OnDrawItem(nIDCtl, lpDrawItemStruct);


Video 3 : Sample in action

Source Code: Download

No comments:

Post a Comment

Leave your comment(s) here.

Like this site? Tell it to your Firend :)