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

在 WinXP 中正确绘制的主题化对话框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (32投票s)

2004年1月29日

7分钟阅读

viewsIcon

240809

downloadIcon

2745

本文展示了一种解决主题对话框中复选框或单选按钮显示为黑色背景的图形错误的解决方法。

引言

您是否曾需要或想要编写一个使用“全新”且时髦的 Windows XP 主题的应用程序?好吧,您可能正好遇到了一个使用选项卡控件和静态控件的应用程序中的一个小问题。如果您创建了一个包含选项卡控件的对话框,并且在该选项卡控件上放置了静态控件、单选按钮或复选框,那么这些控件的背景颜色将无法正确绘制。它只会绘制旧的 COLOR_BTNFACE 作为这些控件的背景,如下面的示例所示。

As you can see this is totally wrong

第一个想法:属性页

那么,我们将如何处理这个问题?嗯,首先,一定有一种方法可以正确地绘制它,因为有些应用程序(例如 Internet Explorer)就可以正确绘制。经过一番调查,我发现它们使用了属性页。所以我决定自己尝试使用属性页,令我惊讶的是,它确实绘制得很好。然而,它只使用属性页本身提供的那个唯一的选项卡控件绘制得很好!嗯,这也不太好,而且,有时属性页并不是您想要的,配置一个合适的属性页需要花费太多的精力。我试图弄清楚它们是如何让属性页正确绘制的,但不幸的是,我找不到任何东西。但是,如果您不需要任何花哨或自定义的东西,属性页可能就足够了。

第二个想法:EnableThemeDialogTexture

好吧,肯定有一种方法可以让 Windows 绘制出那个漂亮的选项卡控件背景,所以我决定查看 UxTheme 库。我在那里找到了这个函数,它显示了一些有趣的效果。此函数使您能够自动在对话框上绘制一个类似于选项卡背景的背景。那么,为什么这不起作用呢?嗯,它在对话框本身上绘制背景。因此,所有实际上应该显示 COLOR_BTNFACE 的地方,现在都显示为具有选项卡控件背景的样子,而且子控件的背景也没有正确对齐,所以您仍然会看到一个难看的对话框。

第三个想法:透明度

您是否注意到过静态控件上的“透明”属性?如果您注意到了,您可能也注意到它根本不起作用……但这并不意味着我们无法使其工作。我们实际上可以!您问如何做?嗯,通过子类化 WM_CTLCOLORSTATIC 消息。每当子窗口请求绘制背景画笔时,都会向对话框发送此消息。听起来很完美,不是吗?如果我们能返回一个透明的画笔,一切都会好起来的,您知道吗?确实有这样的东西!如果我们使用以下代码,它似乎确实有效。

LRESULT OnCtlColor(UINT /* uMsg */, WPARAM wParam, 
       LPARAM /* lParam */, BOOL& /* bHandled */) throw()
{
    // Set the background mode to transparent
    ::SetBkMode((HDC)(wParam), TRANSPARENT);

    // Return the brush return 
    (LRESULT)(::GetStockObject(HOLLOW_BRUSH));
}

所以,如果我们运行程序,静态控件确实可以工作。但是,总是有但是……单选按钮和复选框显示黑色背景! (在此截图上,单选按钮不知何故仍然保留其灰色背景)

Huh? A little hard to read this way

那么,现在怎么办?嗯,我在互联网上对这个问题进行了广泛的搜索,我确实找到了一些提及此问题的网站,但没有人真正提供除了使用属性页之外的解决方案。所以,我接下来做的是查看如何实现将位图显示为对话框的背景,同时使其透明。我从微软那里得到的一个示例给了我一个非常好的,虽然有点“ hack”的想法。

最后一个也是最终的想法:我们使用位图作为背景

什么?我听到您说。我们使用位图?那么我们是如何做到的呢?嗯,我回答说,我们只需在创建选项卡控件时以及每次它调整大小时截取选项卡控件背景的某种屏幕截图,这样我们就有了透明控件的完美背景位图。有趣的想法,它是如何工作的?我们想要的是获得选项卡控件的背景图像,仅背景,不包括子控件,因为例如,如果子控件更改了其标题,可能会出现奇怪的情况。

  1. 我们想要选项卡控件背景的图像,仅背景,不包括子控件,因为如果子控件更改了其标题,可能会出现奇怪的情况。
  2. 我们需要用这个位图创建一个画笔,而且我们不想每次都重新创建它,因为它效率不高。因此,我们只在选项卡控件创建和调整大小时更新此画笔。
  3. 现在我们有了背景画笔,需要为每个控件对其进行对齐,否则我们将看到选项卡控件的左上角作为每个子控件的背景。
  4. 为了在对话框框中轻松实现此功能,而无需复制粘贴任何内容即可重现此效果,这会很方便。
  5. 正如A sleepy one指出的那样,代码控件在早期版本的 Windows 中显示不正确,因此我们需要找到一种方法仅在启用主题时才使用它。

好吧,解决方案就在眼前。我创建了一个模板类,它执行以下操作。它拦截 WM_INITDIALOG 消息并子类化选项卡控件。它在选项卡控件调整大小时以及我们对其进行了初始状态子类化后,调用 UpdateBackgroundBrush() 函数。该函数看起来像这样。

void UpdateBackgroundBrush() throw()
{
    HMODULE hinstDll;

    // Check if the application is themed
    hinstDll = ::LoadLibrary(_T("UxTheme.dll"));
    if (hinstDll)
    {
        typedef BOOL (*ISAPPTHEMEDPROC)();
        ISAPPTHEMEDPROC pIsAppThemed;
        pIsAppThemed = 
          (ISAPPTHEMEDPROC) ::GetProcAddress(hinstDll, "IsAppThemed");

        if(pIsAppThemed)
            m_bThemeActive = pIsAppThemed();

        ::FreeLibrary(hinstDll);
    }

    // Destroy old brush
    if (m_hBrush)
        ::DeleteObject(m_hBrush);

    m_hBrush = NULL;

    // Only do this if the theme is active
    if (m_bThemeActive)
    {
        RECT rc;

        // Get tab control dimensions
        m_wndTab.GetWindowRect(&rc);

        // Get the tab control DC
        HDC hDC = m_wndTab.GetDC();

        // Create a compatible DC
        HDC hDCMem = ::CreateCompatibleDC(hDC);
        HBITMAP hBmp = ::CreateCompatibleBitmap(hDC, 
               rc.right - rc.left, rc.bottom - rc.top);
        HBITMAP hBmpOld = (HBITMAP)(::SelectObject(hDCMem, hBmp));

        // Tell the tab control to paint in our DC
        m_wndTab.SendMessage(WM_PRINTCLIENT, (WPARAM)(hDCMem), 
           (LPARAM)(PRF_ERASEBKGND | PRF_CLIENT | PRF_NONCLIENT));

        // Create a pattern brush from the bitmap selected in our DC
        m_hBrush = ::CreatePatternBrush(hBmp);

        // Restore the bitmap
        ::SelectObject(hDCMem, hBmpOld);

        // Cleanup
        ::DeleteObject(hBmp);
        ::DeleteDC(hDCMem);
        m_wndTab.ReleaseDC(hDC);
    }
}

那么,我具体做了什么?这很简单。首先,我动态加载 IsAppThemed 函数,以便此代码与早期版本的 Windows 兼容。默认情况下,m_bThemeActive 设置为 false。如果函数可以加载,它会执行它来验证主题是否已激活。如果主题已激活,我将创建一个内存 DC 和一个与选项卡控件大小相同的位图。创建内存 DC 后,我们向选项卡控件发送 WM_PRINTCLIENT 消息。此消息的作用是什么?嗯,我们可以向选项卡控件发送此消息,它会在我们指定的 DC 中自行绘制。多么巧妙的消息啊!如果我们使用 BitBlt 将位图从选项卡控件复制到我们的位图,最大的优点是我们不会包含子控件。之后,我们只需从位图中创建画笔并清理我们使用的资源。

现在我们有了要使用的画笔,但如何将其用于选项卡控件的子控件呢?嗯,我们回到第三个想法,子类化 WM_CTLCOLORSTATIC 消息。此消息的处理程序看起来像这样。

LRESULT OnCtlColor(UINT /* uMsg */, WPARAM wParam, 
     LPARAM lParam, BOOL& /* bHandled */) throw()
{
    if (m_bThemeActive)
    {
        RECT rc;

        // Set the background mode to transparent
        ::SetBkMode((HDC)(wParam), TRANSPARENT);

        // Get the controls window dimensions
        ::GetWindowRect((HWND)(lParam), &rc);

        // Map the coordinates to coordinates with the upper
        // left corner of dialog control as base
        ::MapWindowPoints(NULL, m_wndTab, (LPPOINT)(&rc), 2);

        // Adjust the position of the brush for this control
        // (else we see the top left of the brush as background)
        ::SetBrushOrgEx((HDC)(wParam), -rc.left, -rc.top, NULL);

        // Return the brush
        return (LRESULT)(m_hBrush);
    }

    return FALSE;
}

它看起来与我第三个想法中的很相似。它再次将背景模式设置为透明,并从 UpdateBackgroundBrush() 函数返回创建的画笔。不过,增加了一些代码,以确保画笔已正确对齐,这样我们就不会看到选项卡控件的左上角作为子控件的背景。此外,它会检查应用程序是否已主题化,以便不执行不必要的任务。那么,它在现实中看起来怎么样?

Woah! It works!

嗯,正如您所见,它奏效了!这是一种解决丑陋故障的相当粗糙的方法 :-) 但它确实奏效了,而且到目前为止我还没有看到其他解决方案。那么,如何在您的代码中使用这个类?这很简单。

class CSomeDialog: public CThemedDialog<CSomeDialog, IDD_DIALOG1, IDC_TAB1>
{
public:
    typedef CThemedDialog<CSomeDialog, IDD_DIALOG1, IDC_TAB1> SUPERCLASS;

    BEGIN_MSG_MAP(CNiceDialog)
        CHAIN_MSG_MAP(SUPERCLASS)
    ALT_MSG_MAP(1)
        CHAIN_MSG_MAP_ALT(SUPERCLASS, 1)
    END_MSG_MAP()
};

这就是您所需要做的。如果您不想子类化对话框,可以完全删除消息映射。如果您这样做,您必须记住也要链接备用消息映射。此备用消息映射用于子类化选项卡控件。作为模板的参数,您必须传递类本身、对话框的资源标识符以及对话框中文本控件的资源标识符。它只支持一个选项卡控件,如果您想支持更多,您需要稍微调整一下代码。祝您使用愉快!

修订历史

  • 2004-01-29:更新了示例和屏幕截图,以同时显示编辑控件之间的差异,并且应用程序会检测它是否已主题化,这样您就可以在早期版本的 Windows 上使用相同的代码。还更新了文章以反映示例代码中的更改。
  • 2004-01-30:添加了 Win32 代码和示例。
© . All rights reserved.