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:
- It retrieves the itemdata
- If its a seperator then it returns the seperators height and a width of 0
- It calculates the text width and height depending on it's state (normal, bold, font type).
- It adds margins, iconwidth(SM_CXMENUCHECK) and spacing to the width and returns it
DrawItem:
- It gets the currect itemdata
- It checks if the currect item is a seperator and draws it if it is one
- Some checks on the itemdata to draw the menuitem in a certain state
- It calculates the 'square' available for text
- 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.
- It fills the background with a certain color depending on the state
- 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);
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)
{
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:
- Set the ID of first menurow of the menu you want to change to ID_SIDEBAR
- Enter a shortpiece of text to be shown vertically on the sidebar
- Set the Break dropdownlistbox of the second row to 'column' (or 'bar')
- 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.