介绍 自画按钮一般来说很酷。然而,这种酷也有代价:按钮不再像普通按钮那样处理默认状态。本文介绍了一种几乎没有文档记载的技术,它可以帮助所有者绘制按钮,使其表现得像标准按钮一样。它还提供了一个新的基类来派生所有者绘制按钮,以便轻松启用此功能。 这里是文章的一个简短总结,所以你可以跳到有趣的部分: 这个问题 解决方案:OddButton 这个故事 演示 意外的结局 许可 问题:默认按钮 自画按钮是不同的。它们通常看起来很漂亮,是GUI设计中的艺术杰作。但是,当它作为默认按钮时,它们的行为就不正确了。根据特定的实现,它可以以许多不同的方式表现出来。 例如,看看这张照片,试着说如果按下Enter键会发生什么: 正如你所看到的,也许自画按钮最危险的“特性”是当它们被聚焦时,它们不会成为默认按钮。它经常会导致过早的“点击OK”。虽然用户期望,如果使用Tab键导航,聚焦按钮将处理Enter键的输入,但按下对话框的默认按钮——通常是OK按钮。如果不同的按钮混合在一起,情况会变得更加复杂,因为有些按钮可以工作,有些则不能。 好吧,我们再拍一张(猜猜看?) 这说明了第二个问题:多个按钮可以指示默认状态,从而造成了关于哪个按钮将处理请求的混乱。如果以编程方式设置默认按钮,通常会发生这种情况。 有趣的是,位图或图标按钮工作正常。这是带有BS_BITMAP或BS_ICON样式集的按钮,而不是MFC提供的CBitmapButton。 总而言之,用户不能告诉将发生什么,如果他按回车键! 为了理解为什么会出现这个问题,让我们总结一下系统在处理一个标准按钮的默认状态时的行为: 用户按Tab键或单击控件或执行导致输入焦点更改的操作 系统发送WM_GETDLGCODE到当前焦点控件 如果它声称是默认的按钮(返回DLGC_DEFPUSHBUTTON),系统会发送一个BM_SETSTYLE来删除它的默认状态(wParam == BS_PUSHBUTTON)。 然后系统发送WM_GETDLGCODE到接收焦点的控件 如果它声称是一个非默认的按钮(返回DLGC_UNDEFPUSHBUTTON),系统会发送一个BM_SETSTYLE来设置它的默认状态(wParam == BS_DEFPUSHBUTTON)。 系统发送WM_KILLFOCUS到当前被聚焦的控件 系统发送WM_SETFOCUS到接收焦点的控件 如果任何以前的BM_SETSTYLE消息涉及到重绘(lParam != 0), WM_PAINT消息将由系统发布 消息不一定按这个顺序发送,WM_GETDLGCODE可以发送多次。 了解了这一点,需要做什么才能拥有一个行为良好的所有者绘制的按钮?正如下面所解释的,除了它可以以按钮的样式存储默认状态标志之外,系统应该对标准按钮做什么,而我们必须提供自己的空间。 问题是,所有者绘制按钮为WM_GETDLGCODE消息返回DLGC_BUTTON,告诉系统他们不想干扰默认状态。另一方面,如果除了DLGC_DEFPUSHBUTTON之外还返回DLGC_UNDEFPUSHBUTTON或DLGC_UNDEFPUSHBUTTON,您将收到BM_SETSTYLE消息并丢失所有者绘制的样式。 BS_OWNERDRAW样式丢失了,因为它与BS_PUSHBUTTON、BS_DEFPUSHBUTTON以及指定不同类型按钮控件(单选、组框等)的所有其他样式互斥,所以看起来所有者绘制样式无法与默认状态兼容。 更重要的是,正确回复WM_GETDLGCODE消息,你必须知道如果你的按钮是默认的。一个标准的按钮通过查看当前窗口的样式来执行此任务,但是一个所有者绘制的按钮不能使用相同的方法。 您可能认为应该向父节点发送DM_GETDEFID消息(它应该是一个对话框),然后将结果与按钮的ID进行比较,但这不起作用。对话框的默认按钮与当前的默认按钮不同,当前的默认按钮可以随着焦点的变化而变化。 所以,总的来说,你必须在一个内部变量中保持默认状态,因为在窗口的样式中没有它的空间。你应该使用那个变量来知道如何回复WM_GETDLGCODE消息,当你不得不绘制按钮周围的默认状态边界。当您收到BM_SETSTYLE消息时,您需要更新变量并使控件无效。 解决方案:一个所有者绘制的默认按钮 这里是COddButton,它是一个为所有者绘制按钮提供基本s的新基类支持默认状态处理。它应该从一开始就简单,而我们最终使它变得简单。这个按钮是你见过的最特别的自画按钮,因为…它没有绘图代码!它应该作为所有者绘制按钮的基类来代替CButton。 它所拥有的是支持其他所有者绘制按钮的代码。所有丑陋的细节都隐藏在类内部,因此从COddButton派生的按钮可以自己获得默认状态。在派生类中所要做的就是在适当的时候指示默认状态。这可以通过简单的调用COddButton::IsDefault()来完成,以确定默认状态,并通常在按钮变为默认时在其周围绘制黑色框架。 COddButton: IsDefaultHide,复制Code
BOOL IsDefault()
这个函数应该用于知道按钮何时处于默认状态,以便您可以相应地重新绘制控件。默认按钮返回值为真,否则返回值为假。 大多数情况下,COddButton::IsDefault()足以让事情正常运行,但是还有一些补充方法允许更多的灵活性和控制,如下所述。 COddButton: EnableDefaultHide,复制Code
BOOL EnableDefault(BOOL bEnable)
这个函数可以用来确定控件是否支持/需要默认状态。如果参数值为TRUE,则控件启用接收默认状态,FALSE禁用处理,按钮的行为将类似于非固定的所有者绘制。 COddButton: GetControlTypeHide,复制Code
UINT GetControlType()
应该使用这个函数而不是GetStyle()或GetButtonStyle()来知道在DrawItem()覆盖中绘制哪种类型的控件。在PreSubclassWindow()中设置所有者绘制样式之前,这个类存储按钮的初始样式。 返回值是指定各种控件类型的按钮样式之一,除了值BS_DEFPUSHBUTTON(它被映射到BS_PUSHBUTTON)和明显的BS_OWNERDRAW之外。 使用类 使用这个类非常简单。简单地使用COddButton而不是CButton作为所有者绘制按钮的基类。隐藏,复制Code
#include "OddButton.h" class CMyOwnerDrawButton : public COddButton { // ... }
在资源编辑器中,不应该设置“Owner draw”复选框,样式将在运行时正确设置。控件的类型在创建后不应更改,而其他样式位可以更改。 接下来要做的是在适当的时候处理默认状态的绘制。这只是简单地在CButton::DrawItem()覆盖中完成,使用COddButton::IsDefault()调用来找出默认标志的状态并画一条细黑线作为指示器。例如,如果按钮是矩形的,那么它就会是这样的:Hide 收缩,复制Code
void CMyOwnerDrawButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { const BOOL bDefault = IsDefault(); if( bDefault ) { //deflate rectangleCRect oRect(lpDrawItemStruct->rcItem); oRect.DeflateRect(1, 1); lpDrawItemStruct->rcItem = oRect; } /*your drawing code goes here*/ if( bDefault ) { //inflate rectangleCRect oRect(lpDrawItemStruct->rcItem); oRect.InflateRect(1, 1); lpDrawItemStruct->rcItem = oRect; //draw the default frame CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); CPen *pOldPen = (CPen*)pDC->SelectStockObject(BLACK_PEN); CBrush *pOldBrush= (CBrush*)pDC->SelectStockObject(NULL_BRUSH); pDC->Rectangle(lpDrawItemStruct->rcItem); pDC->SelectObject( pOldPen ); pDC->SelectObject( pOldBrush ); } }
此外,所有关键设置都使用断言进行验证,因此至少在调试版本中,您将知道是否出现了错误。 故事:一切是如何开始的(保罗·墨西拿一章) 我经常使用WinCVS来维护重要的项目和我所有的CodeProject文章,我发现CvsIn插件在这方面非常有用,特别是向导对话框(还因为它是可调整大小的)。 像每个优秀的程序员一样,点击所有这些漂亮的按钮,在我的小桌面中打开所有的窗口,我发现有些按钮没有正确地重新绘制。仔细一看,实际上有两个默认按钮。“这是不可能的!”是我的第一个想法,并证明我将对话框部分移出屏幕,然后又移回来,看看哪个按钮仍然拥有它周围的默认边界。 我是对的,只有一个按钮可以同时拥有默认状态,所以下一个出现在我脑海中的问题是:“发生了什么?” “它们是所有者自己绘制的按钮,所有这类按钮在默认状态下都有相同的问题”。这是Jerzy Kaczorowski的答案(CvsIn的创造者),它可能是相同的答案,你们中的许多人已经听到或会给出。但在放弃并接受使用多个看似默认的按钮之前,我不得不尝试一下。 我有CvsIn的来源,假期的时间,还有一点高估了自尊。更不用说Jerzy的鼓励,我的vc++编译器和不可或缺的spy++。 MSDN没有太大的帮助,它只是含糊地解释了标准按钮是如何接收默认状态和过程中涉及到的消息,但这足以开始使用spy++,过滤掉不需要的消息。 监视窗口 使用spy++,我可以注意到业主绘制按钮回答WM_GETDLGCODE消息不同于标准按钮,所以我认为也许返回相同的代码可以解决这个问题。然后,我建立了一个基于MFC对话框的快速项目和一个按钮衍生类来做实验的所有者绘制。 第一个尝试是回复WM_GETDLGCODE消息,以模仿一个标准的按钮,它返回DLGC_BUTTON与DLGC_DEFPUSHBUTTON或DLGC_UNDEFPUSHBUTTON组合,无论它是否有th默认状态。 不幸的是,我有一个糟糕的想法,把我的自画按钮画成一个标准按钮,没有特殊的标志,现在按钮有一个漂亮的黑色边框,只要它应该。“这太容易了!”我感叹道,脸上露出一丝微笑。但我错得不能再错了。 实际上,MSDN对DM_SETDEFID消息简单地说,当系统更改默认按钮时,还有另一条消息被发送到按钮,这就是BM_SETSTYLE。系统改变了我的业主绘制按钮的风格,恢复到一个标准的按钮,因为他们有类似的方面,我是错误的。 那么我如何才能告诉系统我想要我的按钮的默认状态而不失去所有者绘制样式?正如你可能争辩的那样,答案很容易,但又不是太容易,因此我们有了这篇文章…… 演示:它能工作! 在所有的谈话之后,是时候看看奇怪的按钮的行动了。我们包含了示例应用程序,演示了COddButton的使用以及所有者绘制按钮本身的问题。 如你所见,demo的测试区有三个按钮: 固定所有者draw(派生自COddButton) 简单的所有者绘图(源自CButton) 标准按钮 当用户输入数据并按enter处理输入时,有一个编辑框允许您模拟真实的情况。最初默认按钮被设置为固定的所有者绘制按钮,但是您可以通过使用Tab键和默认按钮部分中的控件来改变这一点。如果任何按钮被按下,一个消息框将弹出告诉你发生了什么。尝试不同的焦点和默认值组合,看看它是否总是符合你的期望,是否绘制正确。 除了基本的测试区域,还有一个迷你版的Spy,它会在对话框中显示发送和接收消息的详细日志,这样你就可以分析发生了什么。对话框是可调整大小的,因此您可以扩展它,为消息细节腾出更多空间。 意外结局:麻烦就这样结束了吗? 嗯,几乎……,) 从owner-draw按钮的角度来看,这是最好的。然而,我们对这个问题了解得越多,奇怪的事情就会发生得越多。 使用CDialog::SetDefID方法为对话框设置默认按钮时,发现正常的(!)按钮存在一些问题。正如MSDN上描述的,当描述DM_SETDEFID消息(这是CDialog::SetDefID用来做它的工作)时,有可能有多个按钮在发送消息后指示默认状态。还有一个简短的建议,在这种情况下,应用程序应该自己发送BM_SETSTYLE,以确保指示是准确的。 那时我们正考虑写一篇关于这个主题的全新文章。但进一步搜索就会发现微软知识库文章Q67655(如何:更改或设置对话框中的默认按钮)包含一个演示上述技术的伪代码。提出的解决方案有其不足之处: 如果代码是从按钮单击处理程序中执行的,它就不起作用 它破坏了所有者绘制按钮,并把他们变成正常的按钮 我们已经找到了克服(或者说是解决)这些问题的方法,因为代码已经变得复杂,我们包装了它,并提供了一个静态方法的COddButton类: COddButton: SetDefIDHide,复制Code
static void SetDefID(CDialog* pDialog, const UINT nID)
这个函数应该用来为你的对话框设置新的默认按钮,而不是CDialog::SetDefID。 许可:开源(当然) COddButton和演示应用程序是在艺术许可条款下发布的。 这个项目是用CVS开发的,它的存储库托管在Source Forge上。 要使用CVS获取最新的开发代码(可能编译也可能不编译),请在命令提示符中键入以下一行(输入密码):复制Code
cvs -d:pserver:anonymous@cvs.oddbutton.sourceforge.net:/cvsroot/oddbutton login
下一步是签出模块。要获得演示源代码,请使用模块OddButtonDemoSrc—只需键入以下行(它将把代码放入ButtonsDemo目录):复制Code
cvs -z3 -d:pserver:anonymous@cvs.oddbutton.sourceforge.net:/cvsroot/oddbutton co OddButtonDemoSrc
要仅使用模块OddButtonSrc获取OddButton文件,键入以下行(文件将在OddButtonSrc目录中):复制Code
cvs -z3 -d:pserver:anonymous@cvs.oddbutton.sourceforge.net:/cvsroot/oddbutton co OddButtonSrc
就是这样! 历史 2001年10月27日-为对话框添加、更新源和演示设置默认按钮。 2001年8月28日-更新来源和演示。 2001年9月5日-更新资料来源及示范。 本文转载于:http://www.diyabc.com/frontweb/news1026.html