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

纯 C 中的属主绘制图标按钮( 无 MFC)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (27投票s)

2007年8月7日

CPOL

7分钟阅读

viewsIcon

147339

downloadIcon

4672

使用图标绘制具有所有者绘制风格的按钮。

Screenshot - odib_screenshot.png

目录

介绍

虽然 Windows 拥有非常酷的图形用户界面,但有时标准按钮的外观可能无法满足我们的需求。这时,我们可以选择一种称为“所有者绘制”的按钮样式。所有者必须在每次需要绘制的(单击、启用、焦点等)所有方面(背景、边框、文本、图像)绘制按钮。MFC/C++ 中有很多类(许多都可以在 CodeProject 上找到)可以帮助我们处理需要从头设计的按钮。但是,如果我们使用 Win32 API 和纯 C 语言,关于这个主题的内容并不多。我花了好几天才弄出一些功能性的东西,这要感谢那些懂 Windows 编程的人(Ch. Petzold、J. Newcomer、B. Rector)。现在,这里有一些简单可靠的代码:如果证明有用,我将写一些关于带有文本的所有者绘制按钮的内容(涉及字体、形状、边框、colorrefs、画笔和钢笔)。

背景

此代码使用 Win32 API,因此您应该熟悉消息循环、Windows 消息、通知、句柄等。我们将使用 CreateWindow() 在运行时创建控件,使用 MoveWindow() 调整它们的大小,使用 SendMessage() 与它们通信,等等。将调用一些 GDI 函数来设置只读编辑控件的颜色(SetBkColor()SetTextColor())。为了允许键盘快捷键,我们还将加载加速器。

这个(微型)应用程序可以使用 ANSI 字符集或 Unicode。首先,我们在主头文件中、源代码的顶部定义两个几乎相同的宏(UNICODE_UNICODE)(或者将它们注释掉以坚持使用 ANSI 字符集)。然后,我们包含 <tchar.h> 头文件,并使用通用类型和函数。以下是简要说明:

  • 就 Windows 而言,我们 #define UNICODE(这个没有下划线),并将所有 char 声明为 TCHAR(还使用 LPTSTR 代替 LPSTRLPCTSTR 代替 LPCSTR 等)。如果 Unicode(或 ANSI),TCHAR 被视为 wchar_t(或 char)。接下来,Windows 将选择宽字符函数,例如,如果 Unicode 则为 SendMessageW()(或 ANSI 则默认为 SendMessageA())——请参阅 winuser.h 头文件。
  • 至于 C 语言,我们 #define _UNICODE(这个有一个下划线),并使用通用函数(例如 _tsprintf()),这些函数将被转换为宽字符版本,这里是 swprintf()(而不是 ANSI 版本——在我们的示例中是 sprintf())。
  • 我们这样声明字符串常量:_T("string constant")_T 是一个宏,如果定义了 Unicode,它会将由 char 组成的 ANSI "string constant" 转换为由 wchar_t 组成的 Unicode L"string constant"

所有者绘制。嗯。怎么样?

不,“所有者绘制”中的“所有者”不是你或我:它是子窗口的所有者绘制按钮的父窗口。这决定了哪个回调函数将处理由 Windows 操作系统发送给父窗口的消息(WM_DRAWITEM)。在本文中,处理此消息的函数(myManageOwnerDrawIconButton())由默认窗口回调过程(WndProc())调用。这是简单的方法,并且足以解释本文的主题。

现在,如果您想编写一个独立的自定义控件环境,您将需要将 WM_DRAWITEM 消息转发到另一个回调函数,该函数将设计用于处理来自您的控件的消息(和通知),例如使用 FORWARD_WM_DRAWITEM 宏(请参阅 windowsx.h 头文件)。如果您使用 MFC,您会熟悉这种机制,因为为了处理它,您将向按钮类的实例添加成员函数,而不是其父窗口的实例。

这里,我们讨论的是按钮,一种类型为 ODT_BUTTON 的所有者绘制控件,但其他控件也符合条件:ODT_LISTBOXODT_COMBOBOXODT_STATIC。转发 WM_DRAWITEM 的问题有一个例外,那就是菜单项(类型为 ODT_MENU),其处理应由父窗口完成。请注意,类型在运行时由 Windows 确定,而样式则由我们自己编译时定义。

DRAWITEMSTRUCT 结构

在整个代码中,我们使用一个结构,该结构包含处理 WM_DRAWITEM 消息所需的所有信息。这是简要说明(MSDN 参考):

typedef struct tagDRAWITEMSTRUCT {
    UINT CtlType;  /* ODT_BUTTON, etc. */
    UINT CtlID;  /* The control's specific constant */
    UINT itemID;  /* Same as above, but for a menu item */
    UINT itemAction;  /* Job to do: ODA_DRAWENTIRE, etc. */
    UINT itemState;  /* Checked, focus, selected, etc. */
    HWND hwndItem;  /* The control's handle */
    HDC hDC;  /* The device context (to draw with) */
    RECT rcItem;  /* The control's rectangle boundaries */
    ULONG_PTR itemData;  /* For menu items */
} DRAWITEMSTRUCT;

在 WndProc() 内部

Windows 向应用程序发送各种消息,应用程序将通过其回调函数逐个处理这些消息;在此,WndProc() 是唯一的回调函数。在此程序中,我们有:

  • WM_CREATE:在这里,我们调用 CreateWindow() 来创建两个带有 BS_OWNERDRAW 样式的按钮,再加上一个(只读)编辑控件和一个作为背景的静态控件。此时所有控件的宽度和高度都为零。
  • WM_SIZE:这发生在窗口创建后立即发生,并且在用户调整主窗口大小时也经常发生。坐标 x 和 y,宽度和高度,都相对于客户区,并取决于头文件中定义的常量。维护更容易,我们只陈述一次(重新)大小的指令。
  • WM_COMMAND:这会处理用户对控件和/或菜单项所做的任何操作。例如,BN_CLICKED 是在单击按钮时发生的。单击按钮、选择“视图”菜单项或按 F3 或 F4 会向编辑控件发送一条消息,告诉它显示一个字符串常量。
  • WM_CTLCOLORSTATIC:我们处理此消息以选择静态控件以及禁用或只读编辑控件的颜色。现在,如果编辑控件既不是禁用的也不是只读的,那么我们就必须处理 WM_CTLCOLOREDIT
  • WM_CLOSE:在用户选择“文件”>“退出”菜单项、单击右上角的小红框或按 Alt+F4 时收到。
  • WM_DESTROY:在为主窗口调用 DestroyWindow() 时触发。
  • WM_DRAWITEM:收到此消息后,我们使用 lParam 获取指向 DRAWITEMSTRUCT 结构的指针。声明在 WndProc() 函数中:static DRAWITEMSTRUCT* pdis。然后,我们将自定义函数 myManageOwnerDrawIconButton()pdis 作为参数之一进行调用。

以下是相关代码

// ...

switch(message) {
    case WM_DRAWITEM:
        pdis = (DRAWITEMSTRUCT*) lParam;
        switch(pdis->CtlID) {
            case IDC_LEVELUPBUTTON:
                // Fall through (you would use a "break" otherwise):

            case IDC_LEVELDNBUTTON:
                iResult = myManageOwnerDrawIconButton(pdis, hInst);
                if (RET_OK != iResult) return(FALSE);
                break;
            default:
                break;
        }
        return(TRUE);
// ...

处理 WM_DRAWITEM 的函数

四次调用 LoadIcon() 返回四个图标的句柄,每个按钮两个——一个活动(按下),一个非活动(等待)。第一次调用时,一个静态计数器会递增,因此图标只加载一次。现在,此函数实际上只加载一次,后续调用仅检索现有图标的句柄。根据 MSDN,LoadIcon() 已被 LoadImage() 取代,但 LoadImage() 会在每次调用时加载,并且要求应用程序在每次加载某物后调用一些销毁函数。因此,我们使用 LoadIcon()——但如果您更喜欢后者,您将需要一个计数器,所以这里是。哦,静态意味着变量只创建一次,因此它的值会从一次调用保留到下一次调用——这通常是您需要的计数器。

一旦我们有了所需的四个图标的句柄,其余的就很简单了:DrawIconEx() 将使用设备上下文,在由图标左上角的 x、y 坐标确定的位置绘制图标,并具有一定的宽度和高度。每个图标都居中在 DRAWITEMSTRUCT 的矩形内。用于绘制按钮的图标根据控件的标识符(IDC_LEVELUPBUTTONIDC_LEVELUPBUTTON)及其当前状态(ODS_SELECTED)来选择。DrawIconEx()DrawIcon() 更有趣,因为它允许您选择图标的大小——而 DrawIcon() 只会以固定的宽度 GetSystemMetrics(SM_CXICON) 和高度 GetSystemMetrics(SM_CYICON)(即 32x32 像素)绘制图标。当然,我们这样做,但如果您需要灵活性,请选择 DrawIconEx(),这样 Windows 就会按照您想要的方式调整图标视图大小。

以下是相关代码

// Declaration:

int myManageOwnerDrawIconButton(DRAWITEMSTRUCT* pdis, HINSTANCE hInstance);

// Load an icon handle:

hIcon = (HICON) LoadIcon(hInstance, MAKEINTRESOURCE(ID_ICON));

// And draw the icon into the device context:

DrawIconEx(
    pdis->hDC,
    (int) 0.5 * (rect.right - rect.left - ICON_WIDTH),
    (int) 0.5 * (rect.bottom - rect.top - ICON_HEIGHT),
    (HICON) hIcon,
    ICON_WIDTH,
    ICON_HEIGHT,
    0, NULL, DI_NORMAL);
// ...

附加信息

Voilà。如果您喜欢这个示例程序,如果您认为可以使用它,请登录并评价文章。当然,如果您认为它可以改进,请告诉我。

我包含两个 zip 压缩包:一个 Visual Studio 项目和一个 MinGW(基于 GCC)文件集,因此请选择您熟悉的解决方案。

最后,有一个函数(myWriteToLog()),它将一个字符串写入日志文件(odib_log.txt),如果需要(与 exe 在同一目录)则创建该文件,并在程序退出后立即删除。程序仅在发生错误时调用此函数。如果您不希望发生这种情况,请将所有 myWriteToLog() 调用注释掉。

历史

  • 2007 年 8 月 8 日:修正了 HTML 和源代码。
  • 2007 年 8 月 7 日:初版。
© . All rights reserved.