65.9K
CodeProject 正在变化。 阅读更多。
Home

带渐变侧边栏的炫酷WTL菜单

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (4投票s)

2000年4月28日

viewsIcon

225242

downloadIcon

2545

一篇关于改变WTL图标菜单外观的文章。

Sample Image - sidebarmenu.gif

首先是一些历史

几个月前,我还在思考如何实现像Word那样带有图标的炫酷菜单。那时我使用的是MFC,并找到了Brent Corkum的BCMenu类。它使用工具栏资源将图标与菜单进行映射。我喜欢这个类,并对其进行了一些修改,使其看起来像我在Mike Walter的SmartFTP应用程序中看到的菜单,这启发了我这样做。结果与截图中显示的一样(但没有阴影...)。几周后,我在Platform SDK上找到了WTL头文件,我想,通过修改WTL源代码,为WTL制作同样类型的菜单会很酷。这可能不是最佳做法,但我所有的WTL应用程序只需要重新编译一次,瞧,菜单就变得很酷了。本文将逐步解释我如何修改WTL源代码。任何对C++有一定了解的人都可以轻松地阅读本文。这些修改也适用于Brent Corkum的BCMenu类

这与WTL有什么关系?

我必须承认,这些修改并非WTL特有。但是WTL已经为你提供了一个与BCMenu类似的图标菜单。首先,我不想强迫你使用BCMenu来获取图标;其次,是让你看到对WTL进行微小调整以实现酷炫功能是多么容易。

分析截图

当你查看截图时,你可能会注意到以下几点:

  • 热点项目具有3D边框
  • 加速键(快捷方式)以蓝色显示
  • 它有一个带有垂直文本的渐变侧边栏(顺便说一句,字体叫做LCD)

我将分别解释这三点。我们将要编辑的WTL头文件是“atlctrlw.h”,因此请备份此文件,以防万一你搞砸了。

3D外观边框

一开始我不知道从头文件中哪里开始找,所以我只是开始寻找`DrawItem`方法,直到我找到了正确的文件,即'atlctrlw.h'。然后我“阅读”了源代码,这里是`DrawItem`和`MeasureItem`方法的文本描述:

MeasureItem

  1. 它检索项数据。
  2. 如果它是分隔符,则返回分隔符的高度和宽度为0。
  3. 它根据文本的状态(普通、粗体、字体类型)计算文本宽度和高度。
  4. 它将边距、图标宽度(SM_CXMENUCHECK)和间距添加到宽度中并返回。

DrawItem

  1. 它获取当前项目数据。
  2. 它检查当前项是否是分隔符,如果是,则绘制它。
  3. 对项目数据进行一些检查,以特定状态绘制菜单项。
  4. 它计算可用于文本的“正方形”区域。
  5. 如果它有图标,则根据项目状态以特定方式绘制;如果没有,则检查项目数据是否“选中”并相应地绘制一个复选标记。
  6. 它根据状态填充背景色。
  7. 它根据状态调用`DrawMenuText(..)`并使用特定的文本颜色。

Windows 为菜单中的每个项目调用 `MeasureItem`。通过这种方式,它可以计算菜单的宽度和高度。在此菜单窗口创建后,它为每个菜单项调用 `DrawItem`,并提供一个矩形,其中包含在窗口中输出菜单项数据的信息。

那么我们应该在哪里绘制矩形呢?矩形需要在背景填充后绘制。因此,`DrawItem` 的第6步似乎是合适的。所以我们需要在以下行之后添加以下两行代码:

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

我的第一个版本使用了`dc.DrawEdge(..)`,但这使得3D效果显得过于“3D”:-)。而使用`Draw3DRect`,菜单项看起来更像大多数控件所具有的“内凹”切换/样式。

现在我们完成了一项,还有两项待完成……

加速键

加速键需要更多的关注。它需要修改`DrawMenuText(..)`方法。我只是在绘制快捷方式之前将颜色设置为系统的强调色,然后将其恢复为原始颜色。但这有一个副作用。当你“悬停”在菜单项上方时,你将看不到快捷键,因为它与填充色相同。一个更好的方法是,如果快捷键是高亮的,则用相同的颜色绘制;如果快捷键是正常的,则用强调色绘制。但是`DrawMenuText`无法访问菜单项数据。我决定复制`DrawMenuText`方法,并添加一个额外的参数,其中包含绘制快捷键的颜色。这样,我就可以在`DrawItem`方法中根据项数据传递正确的快捷键颜色。

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);
}

现在我们只需要传递一个额外的参数。我添加了以下代码的结果:

::GetSysColor(bDisabled ? COLOR_GRAYTEXT : 
             (bSelected ? COLOR_HIGHLIGHTTEXT : COLOR_HIGHLIGHT))
作为参数。

嗯,还有一个。

带文本的渐变侧边栏

侧边栏是最需要做的工作。我开始思考如何为绘制它腾出空间。于是我只是为每个`MeasureItem`添加了一定的宽度,但那样我就必须为每个菜单项将侧边栏切割成不同的绘制部分。所以这显然不是办法。然后我发现如何用`dc.GetClipBox(..)`来确定菜单的大小。然后我做了前面提到的相同宽度技巧。但我只需要绘制侧边栏一次,而不是为每个需要绘制的菜单项。所以我检查了项目数据中是否存在某个ID(=666),以确保它只绘制一次。但这仍然没有达到我预期的效果,它完全混乱了,因为有mouseover消息(这叫热追踪,对吗?)。然后我刚刚在菜单资源编辑器中找到了“Break”下拉列表框。我在每个菜单的第一行(ID_SIDEBAR)中为侧边栏使用了一个特殊ID,将第二行的“Break”选项设置为“column”,并将ID_SIDEBAR的资源值更改为666。现在我只需要在`MeasureItem`和`DrawItem`方法中检查666即可做我自己的事情。如果我在`MeasureItem`中遇到666,我将菜单项的高度返回为0,菜单项的宽度返回为侧边栏的宽度减去图标宽度。如果我在`DrawItem`方法中遇到666,我调用了我自己添加的`DrawSidebar`方法来绘制侧边栏。`DrawSidebar`还将菜单项的字符串垂直打印,这样我就不必再使用特殊的位图了。

我认为添加一个渐变背景会很酷,就像开始菜单一样(尽管那只是一个位图)。所以我使用了`::GradientFill`调用来完成此操作。这就是为什么它只适用于Windows98和Windows2000。如果你只进行常规填充或添加自己的渐变方法(后者编写起来并不那么困难),那么你的版本就独立了。另一种选择是随应用程序分发msimg32.dll,但我不认为Microsoft的许可协议允许这样做。

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 );
    }
}

此方法需要在`MeasureItem`中像这样调用:

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

SIDEBAR_ID 在我的源代码中只是一个定义,其值为 666(与 ID_SIDEBAR 值相同)。你甚至可以在检查它是否是分隔符之前添加它,但我选择在分隔符的 else 部分开始后立即添加它。

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;
}
同样,上述代码也需要添加到`MeasureItem`方法中。

所以,要为菜单添加侧边栏,你应该:

  1. 将要更改的菜单的第一行菜单项ID设置为ID_SIDEBAR。
  2. 输入一小段文本,以便在侧边栏上垂直显示。
  3. 将第二行的“Break”下拉列表框设置为“column”(或“bar”)。
  4. 将 resource.h 中 ID_SIDEBAR 的值更改为 666。

如果一切编译和链接正常,那么您的应用程序现在将在编辑后的菜单左侧显示一个侧边栏。

历史

2002年5月30日 - 更新了源代码


修改后的WTL头文件和LCD字体已包含在源代码中。如果您使用此代码,请给我发邮件告知您正在使用它,这样我就可以知道撰写本文是否值得。

嗯,希望你也能从这篇文章中学到一些东西。给我发邮件,报告 bug、批评、评论、建议、改进,或者只是为了好玩。

© . All rights reserved.