Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Cool WTL menu with Gradient Sidebar

0.00/5 (No votes)
29 May 2002 3  
An article about changing the look of WTL icon menu

Sample Image - sidebarmenu.gif

First some history

Some months ago I was wondering how I could get cool menus with icons like Word. I was using MFC then and found the BCMenu class from Brent Corkum. It used the toolbar resource for mapping the icons with the menu. I liked this class and altered it somewhat to make it look like a menu I saw in the application SmartFTP from Mike Walter which inspired me to do this. This resulted in the same result as seen in the screenshot (but without the shading...). After some weeks I found the WTL header files on the Platform SDK and I thought it would be cool to make the same kind of menu for WTL by altering the WTL source. This may not seem the best action but all my WTL apps just needed one recompile and voila, cool looking menus. This article will explain how I altered the WTL source step-by-step. Anyone with some knowledge of C++ can easily follow this article. The alterations are also possible for the BCMenu class from Brent Corkum.

What has WTL got to do with this?

I have to be honest that the alterations are not WTL specific. But WTL already provides you with an iconmenu simular to the BCMenu. First is that I do not want you to force using BCMenu to get icons, second is to make you see how easy it is to make minor adjustments to do cool stuff with WTL.

Analyzing the screenshot

When you take a look at the screenshot you will probably notice the following things:

  • The hotitem has a 3D look border
  • The accellerator keys (shortcuts) are printed in blue
  • It has a gradient sidebar with vertical text (btw. the font is called LCD)

I will explain each of the three seperately. The WTL header file that we will be editing is 'atlctrlw.h' so make a backup of this file in case you screw up.

The 3D look border

At first I did now know where to start looking in the headerfiles so I just started looking for a DrawItem method until I found the correct file which is 'atlctrlw.h'. I then 'read' the source code and here is a textual description of both the DrawItem and MeasureItem methods:

MeasureItem:

  1. It retrieves the itemdata
  2. If its a seperator then it returns the seperators height and a width of 0
  3. It calculates the text width and height depending on it's state (normal, bold, font type).
  4. It adds margins, iconwidth(SM_CXMENUCHECK) and spacing to the width and returns it

DrawItem:

  1. It gets the currect itemdata
  2. It checks if the currect item is a seperator and draws it if it is one
  3. Some checks on the itemdata to draw the menuitem in a certain state
  4. It calculates the 'square' available for text
  5. If it has an icon it draws it in a certain way depending on the itemstate, if not then it check the itemdata if it is 'checked' and draws a checkmark accordingly.
  6. It fills the background with a certain color depending on the state
  7. It calls DrawMenuText(..) with a certain text color depending on the state

Windows calls MeasureItem for each item in a menu. This way it can calculate the width and height of the menu. After this menu window has been created it calls DrawItem for each menuitem and gives it a rectangle with information for where to output the menuitem it's data in the window.

So where do we need to draw the rectangle? The rectangle needs to be drawn after the background has been filled. So step 6 of DrawItem seems appropriate. So we need to add the following two lines of code after the line:

dc.FillRect(&rcBG, (HBRUSH)LongToPtr(bSelected ? (COLOR_HIGHLIGHT + 1) : 
                                                 (COLOR_MENU + 1)));
...
if(bSelected)
    dc.Draw3dRect(&rcBG, GetSysColor(COLOR_3DDKSHADOW), 
                      GetSysColor(COLOR_3DHILIGHT));
...

My first version used dc.DrawEdge(..) but this made the 3D look too '3D' :-). With Draw3DRect the menuitem looks like the 'sunken' toggle/style which most controls have.

So now we have one down but still two to go...

The accellerator keys

The accellerator keys needed some more attention. It needed to alter the DrawMenuText(..) method. I just set the color to the systems Highlight color just before drawing the shortcut and set it back to original color afterwards. But this has one bad side effect. When you 'hovered' above a menuitem you would not see the shortcut-key because it had the same color as the fillcolor. A better way is to draw the shortcut-key with the same color if it is highlighted and in the highlight color if it's normal. But DrawMenuText does not have access to the menuitemdata. I decided to make a copy of the DrawMenuText method and added an extra parameter which contained the color to draw the shortcut-key with. This way I can pass the correct shortcut-key color in the DrawItem method based on the itemdata.

void DrawMenuText(CDCHandle& dc, RECT& rc, LPCTSTR lpstrText, 
                  COLORREF colorText, COLORREF colorAccellerator)
{
    int nTab = -1;
    for(int i = 0; i < lstrlen(lpstrText); i++)
    {
        if(lpstrText[i] == '\t')
        {
            nTab = i;
            break;
        }
    }
    dc.SetTextColor(colorText);
    dc.DrawText(lpstrText, nTab, &rc, 
                DT_SINGLELINE | DT_LEFT | DT_VCENTER);
    dc.SetTextColor( colorAccellerator );
    if(nTab != -1)
        dc.DrawText(&lpstrText[nTab + 1], -1, &rc, 
                    DT_SINGLELINE | DT_RIGHT | DT_VCENTER);
    dc.SetTextColor(colorText);
}

Now we only need to pass an extra parameter. I added the result of the code

::GetSysColor(bDisabled ? COLOR_GRAYTEXT : 
             (bSelected ? COLOR_HIGHLIGHTTEXT : COLOR_HIGHLIGHT))
as a parameter.

Well still one to go.

The gradient sidebar with text

The most work to do was the sidebar. I started thinking about how to make room to draw it. So I just added a certain width to each MeasureItem, but then I had to chop the bar in different draw pieces for eacht menuitem. So this was clearly not the way to go. I then found out how I could determine the size of the menu with dc.GetClipBox(..). I then did the same width trick as mentioned a few lines back. But I just needed to draw the sidebar once and not for each menuitem which had to be drawn. So I checked for a certain ID (=666) in the itemdata to make sure it was only drawn once. This still did not work as I expected, it totally messed up because of onmouseover messages (this is called hottracking right?). Then I just found the Break dropdownlistbox in the menuresourceeditor. I used a special ID for the sidebar in the first row of every menu (ID_SIDEBAR), set the Break option to 'column' for each second row and changed the resource value of ID_SIDEBAR to 666. Now I only needed to check for 666 in MeasureItem and DrawItem methods to do my own stuff. If I encountered 666 in MeasureItem I returned 0 for the menuitem height and the width of the sidebar minus the iconwidth for the menuitem width. If I encountered 666 in the DrawItem method I called my own added method DrawSidebar to draw the sidebar. DrawSidebar also printed the menuitem its string in a vertical position so I did not had to use special bitmaps anymore.

I thought it would be cool to add a gradient background just like the startmenu (althought that is just a bitmap). So I used the ::GradientFill call to do this. This is why it only works with Windows98 and Windows2000. If you just do a regular fill or add your own gradient method (the later is not that hard to write) then you are version independant. The other option is to distribute msimg32.dll with your application but I do not think Microsofts license agreement allows this.

void DrawSideBar (CDCHandle& dc, RECT& grrect, LPCTSTR lpstrSidebarText)
{
    RECT rct;
    dc.GetClipBox(&rct);
    int iWidth = grrect.right - grrect.left;
    int iHeight = grrect.top - grrect.bottom;
    int iSideBarHeight = rct.bottom-rct.top;
    int iSideBarWidth = rct.right-rct.left;

    COLORREF right    = GetSysColor(COLOR_ACTIVECAPTION);
    COLORREF left    = GetSysColor(27); // COLOR_GRADIENTACTIVECAPTION


    COLOR16 r = (COLOR16) ((left & 0x000000FF)<<8);
    COLOR16 g = (COLOR16) (left & 0x0000FF00);
    COLOR16 b = (COLOR16) ((left & 0x00FF0000)>>8);

    TRIVERTEX        vert[2] ;
    GRADIENT_RECT    gRect;
    vert [0] .x      = 0;
    vert [0] .y      = 0;
    vert [0] .Red    = r;
    vert [0] .Green  = g;
    vert [0] .Blue   = b;
    vert [0] .Alpha  = 0x0000;

    r = (COLOR16) ((right & 0x000000FF)<<8);
    g = (COLOR16) (right & 0x0000FF00);
    b = (COLOR16) ((right & 0x00FF0000)>>8);

    vert [1] .x      = iWidth;
    vert [1] .y      = iSideBarHeight;
    vert [1] .Red    = r;
    vert [1] .Green  = g;
    vert [1] .Blue   = b;
    vert [1] .Alpha  = 0x0000;
    gRect.UpperLeft  = 0;
    gRect.LowerRight = 1;
        
    GradientFill(dc.m_hDC,vert,2,&gRect,1,GRADIENT_FILL_RECT_V);

    HFONT hFont;

    hFont = CreateFont(iWidth, 0, 900,900,0,FALSE,FALSE,FALSE,0,
        OUT_DEFAULT_PRECIS,CLIP_MASK, PROOF_QUALITY, FF_DONTCARE, 
        _T(SIDEBAR_FONT));

    if (lpstrSidebarText)
    {
        dc.SetBkMode(TRANSPARENT);
        HFONT fontold = dc.SelectFont( hFont );

        RECT dims;

        dims.left = dims.top = 1;
        dims.right = iWidth;
        dims.bottom = iSideBarHeight;

        dc.SetTextColor( 0x0 );
        dc.DrawText(lpstrSidebarText, strlen(lpstrSidebarText),
            &dims,
            DT_SINGLELINE|DT_BOTTOM);

        dims.top -= 1;
        dims.left -= 1;
        dims.right -= 1;
        dims.bottom -= 1;

        dc.SetTextColor( GetSysColor(COLOR_CAPTIONTEXT) );
        dc.DrawText(lpstrSidebarText, strlen(lpstrSidebarText),
            &dims,
            DT_SINGLELINE|DT_BOTTOM);
        dc.SelectFont( fontold );
    }
}

This method needs to be called from MeasureItem like:

if(lpDrawItemStruct->itemID == SIDEBAR_ID)
{
    DrawSideBar(dc,(RECT)rcItem, pmd->lpstrText);
    return;
}

SIDEBAR_ID is just a define which is 666 in my source (the same as the ID_SIDEBAR value). You can add it even before you check if it is a seperator, but I choose to add it right after the start of the seperator its else part.

if(lpMeasureItemStruct->itemID == SIDEBAR_ID)
{
    // We only need to return the width of the sidebar to let windows

    // know the width of sidebar. It doesn't need to reserve height

    // because we determine the height ourselfs in the DrawItem method

    lpMeasureItemStruct->itemWidth = SIDEBAR_WIDTH - 
                                     GetSystemMetrics(SM_CXMENUCHECK);
    lpMeasureItemStruct->itemHeight= 0;
    return;
}
The same can be said for the above code which needs to be added to the MeasureItem method.

So to add a SideBar to a menu you should:

  1. Set the ID of first menurow of the menu you want to change to ID_SIDEBAR
  2. Enter a shortpiece of text to be shown vertically on the sidebar
  3. Set the Break dropdownlistbox of the second row to 'column' (or 'bar')
  4. Change the value of ID_SIDEBAR in resource.h to 666

If everything compiles and links ok then your application will now have a sidebar on the left in the edited menu.

History

30 May 2002 - updated source


The altered WTL header file and LCD font are included in the source. If you use this code then please mail me that you are using it so I can see if it was worth the trouble of writing this article.

Well, I hope you also have learned something of this article. Mail me bugs, flames, comments, suggestions, improvements or just for fun.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here