通过所有者绘制控件实现的颜色选择器和字体选择器
演示如何通过所有者绘制控件实现 WIN32 颜色选择器和字体选择器
引言
虽然在这个网站上有很多功能齐全的颜色选择器 [1, 2, 3],但这些代码通常是 MFC 或 WTL 的。我想在这里介绍一个完全用 Windows API 编写的颜色选择器。我还想说服您,干净而基础的 API 函数可以产生友好、整洁且强大的控件。
首先,我将详细介绍如何通过使用所有者绘制列表框来生成部分类似 Office 的颜色选择器面板。其次,我将简要介绍如何使用所有者绘制组合框来完成类似的工作。
实现
首先,您必须启动 Visual C++,然后创建一个 Win32 应用程序,其中显示典型的“Hello World”字符串。接下来,我们需要创建一个对话框,然后在其中嵌入一个列表框。列表框的属性表应如下所示:
现在我们需要编写代码来自己绘制列表框。不要惊慌,因为代码行数并不多。我们只需要在对话框的回调函数(即列表框的所有者窗口)中响应一些消息。
/** * Callback function of "Background color" dialog box. */ LRESULT CALLBACK background(HWND hdlg, UINT message, WPARAM wParam, LPARAM lparam) { switch (message) { case WM_INITDIALOG: // Place your own code here case WM_MEASUREITEM: // Place your own code here case WM_DRAWITEM: // Place your own code here case WM_CTLCOLORLISTBOX: // Place your own code here case WM_COMMAND: // Place your own code here } return FALSE; }
为了初始化列表框控件,我们需要处理 WM_INITDIALOG
消息。我们所做的只是将预定义的全局变量颜色值分配给每个列表框项。
case WM_INITDIALOG: int nColor; for (nColor = 0; nColor < sizeof(g_crItems)/sizeof(DWORD); nColor++) { SendDlgItemMessage(hdlg, IDC_BACKGROUND, LB_ADDSTRING, nColor, (LPARAM) ""); SendDlgItemMessage(hdlg, IDC_BACKGROUND, LB_SETITEMDATA, nColor, (LPARAM) g_crItems[nColor]); if (g_bgColor == g_crItems[nColor]) SendDlgItemMessage(hdlg, IDC_BACKGROUND, LB_SETCURSEL, nColor, 0); } return TRUE;
接下来,在接收到 WM_MEASUREITEM
消息时,我们需要告诉对话框每个列表框项的尺寸。当创建列表框控件时会发送此消息。参数 lparam
是指向 MEASUREITEMSTRUCT
类型结构的指针。在演示颜色选择器面板中,我们有 6 行 8 列。
case WM_MEASUREITEM: RECT rc; LPMEASUREITEMSTRUCT lpmis; lpmis = (LPMEASUREITEMSTRUCT) lparam; GetWindowRect(GetDlgItem(hdlg, lpmis->CtlID), &rc); lpmis->itemHeight = (rc.bottom-rc.top)/6; lpmis->itemWidth = (rc.right-rc.left)/8; break;
当所有者绘制控件需要重绘时,它会向其父窗口发送 WM_DRAWITEM
消息。这发生在控件首次创建时、按下或释放时、获得或失去输入焦点时,以及任何其他需要重绘的时候。lparam
消息参数是指向 DRAWITEMSTRUCT
类型结构的指针。它包含程序绘制控件所需的信息。与列表框配合使用的重要结构字段是 hDC
(设备上下文)、rcItem
(一个提供每个列表框项大小的 RECT
结构)、itemID(列表框 ID)和 itemState(指示该项是否被按下或是否获得输入焦点)。
case WM_DRAWITEM: HDC hdc; COLORREF cr; HBRUSH hbrush; DRAWITEMSTRUCT * pdis; pdis = (DRAWITEMSTRUCT *)lparam; hdc = pdis->hDC; rc = pdis->rcItem; // Transparent. SetBkMode(hdc,TRANSPARENT); // NULL object if (pdis->itemID == -1) return 0; switch (pdis->itemAction) { case ODA_DRAWENTIRE: switch (pdis->CtlID) { case IDC_BACKGROUND: rc = pdis->rcItem; cr = (COLORREF) pdis->itemData; InflateRect(&rc, -3, -3); hbrush = CreateSolidBrush((COLORREF)cr); FillRect(hdc, &rc, hbrush); DeleteObject(hbrush); FrameRect(hdc, &rc, (HBRUSH) GetStockObject(GRAY_BRUSH)); break; } // *** FALL THROUGH *** case ODA_SELECT: rc = pdis->rcItem; if (pdis->itemState & ODS_SELECTED) { rc.bottom --; rc.right --; // Draw the lighted side. HPEN hpen = CreatePen(PS_SOLID, 1, GetSysColor(COLOR_BTNSHADOW)); HPEN holdPen = (HPEN)SelectObject(hdc, hpen); MoveToEx(hdc, rc.left, rc.bottom, NULL); LineTo(hdc, rc.left, rc.top); LineTo(hdc, rc.right, rc.top); SelectObject(hdc, holdPen); DeleteObject(hpen); // Draw the darkened side. hpen = CreatePen(PS_SOLID, 1, GetSysColor(COLOR_BTNHIGHLIGHT)); holdPen = (HPEN)SelectObject(hdc, hpen); LineTo(hdc, rc.right, rc.bottom); LineTo(hdc, rc.left, rc.bottom); SelectObject(hdc, holdPen); DeleteObject(hpen); } else { hbrush = CreateSolidBrush(GetSysColor(COLOR_3DFACE)); FrameRect(hdc, &rc, hbrush); DeleteObject(hbrush); } break; case ODA_FOCUS: rc = pdis->rcItem; InflateRect(&rc, -2, -2); DrawFocusRect(hdc, &rc); break; default: break; } return true;
列表框的默认背景颜色是白色,这使得它看起来很奇怪,因为对话框的背景是灰色的。要修改列表框的背景颜色,我们需要响应 WM_CTLCOLORLISTBOX
消息。在系统绘制列表框之前会发送此消息。我们只需返回一个画刷句柄,系统将使用它来绘制列表框的背景。
case WM_CTLCOLORLISTBOX: return (LRESULT) CreateSolidBrush(GetSysColor(COLOR_3DFACE));
最后,当用户单击“确定”按钮时,我们需要检索用户选择的颜色,如果用户单击“取消”按钮,则直接关闭对话框。
case WM_COMMAND: if (LOWORD(wParam) == IDOK) { int nItem; nItem = SendDlgItemMessage(hdlg, IDC_BACKGROUND, LB_GETCURSEL, 0, 0L); g_bgColor = SendDlgItemMessage(hdlg, IDC_BACKGROUND, LB_GETITEMDATA, nItem, 0L); EndDialog(hdlg, LOWORD(wParam)); InvalidateRect(GetParent(hdlg), NULL, true); return TRUE; } else if (LOWORD(wParam) == IDCANCEL) { EndDialog(hdlg, LOWORD(wParam)); return TRUE; } break;
啊哈,就这样。编译代码,您将得到一个颜色选择器,如本文顶部所示。
类似地,如果您想从所有者绘制的组合框创建颜色选择器,只需要进行少量修改。首先,将消息前缀 LB_
替换为 CB_
,其次,不再需要处理 WM_CTLCOLORLISTBOX
。对话框的最终外观如下所示。嗯,这还不够好,因为每个颜色的名称应该放在每项的右侧。Charles Petzold(《Programming Windows》第五版作者)有一个很好的例子来说明如何做到这一点。
然而,这还不是全部。通过更改列表框项的内容并控制它们的大小,还可以通过这种方式创建更多花哨的控件。下图显示了字体选择器。在此字体选择器控件中,演示了一些花哨的技术。
- Windows 中字体列表的枚举。
- 更改按钮控件的字体样式。
- 一个可以记住历史条目的自定义组合框。
问题
对于所有者绘制列表框颜色选择器,如果它是标签顺序中的第一个控件,则焦点矩形可能无法正确绘制。我花了很长时间试图纠正这一点,但最终放弃了。此外,当鼠标光标移动到每个颜色项上时,最好弹出一个显示颜色名称的工具提示。您的反馈和代码改进将不胜感激。