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






4.67/5 (27投票s)
使用图标绘制具有所有者绘制风格的按钮。
目录
介绍
虽然 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
代替LPSTR
,LPCTSTR
代替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
组成的 UnicodeL"string constant"
。
所有者绘制。嗯。怎么样?
不,“所有者绘制”中的“所有者”不是你或我:它是子窗口的所有者绘制按钮的父窗口。这决定了哪个回调函数将处理由 Windows 操作系统发送给父窗口的消息(WM_DRAWITEM
)。在本文中,处理此消息的函数(myManageOwnerDrawIconButton()
)由默认窗口回调过程(WndProc()
)调用。这是简单的方法,并且足以解释本文的主题。
现在,如果您想编写一个独立的自定义控件环境,您将需要将 WM_DRAWITEM
消息转发到另一个回调函数,该函数将设计用于处理来自您的控件的消息(和通知),例如使用 FORWARD_WM_DRAWITEM
宏(请参阅 windowsx.h 头文件)。如果您使用 MFC,您会熟悉这种机制,因为为了处理它,您将向按钮类的实例添加成员函数,而不是其父窗口的实例。
这里,我们讨论的是按钮,一种类型为 ODT_BUTTON
的所有者绘制控件,但其他控件也符合条件:ODT_LISTBOX
、ODT_COMBOBOX
和 ODT_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_LEVELUPBUTTON
、IDC_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 日:初版。