MFC 9中的新控件Command Link Button及在Vista之前平台上的应用
什么是Command Link?
Command Link在Vista中是样新事物,请看下图:
它实质上有两部分:主文本(Main Text)及注释文本(Note Text),如下图:
一个Command Link,实际上不是一个新的控件类,它只是Button类的一个新样式,只需对Button控件添加BS_COMMANDLINK样式便可看上去像Command Link。
MFC对Command Link的增强之处
在Visual Studio 2008中,MFC得到了一定程度的增强以适应Vista的新特性,现在MFC已做到了与Vista的同步,因此,下列成员函数被添加到CButton类中以实现Command Link:
Ø CButton::GetNote
Ø CButton:GetNoteLength
Ø CButton:SetNote
从MFC函数的命名规则中,就大致可以猜出这些函数的功能来,另外,资源管理器的工具箱也添加了这些新控件,见下图:
问题来了
程序员经常会写一些面向多平台的程序,在典型的开发过程中,通常是一个二进制程序要面向操作系统的多个版本,这就要求,作为一个程序开发者,要么减少在所有操作系统版本上不通用的特性,要么用一些变通方法,在后台写一些特定于某种操作系统平台的代码,让程序功能尽可能地保持一致。
在用户界面这一块,尤其是程序要求有跨所有平台、设计良好的人机界面时,这个问题尤为严重,因为这一点涉及到用户如何与软件进行交互。在外观感觉、软件行为表现方面任何的不同,都会降低用户采用该软件的速度,且客户支持中心的来电也会直线上升,如此等等。
多数时候,大部分的软件都会与纸质用户手册一并发售,所以软件统一的外观感觉是必须的,否则,多个软件版本的维护可不是件轻松事。
那这与Command Link有什么关系呢?Command Link在Vista之前的平台上并不存在,所以,如果一个软件用Command Link实现了更友好的用户界面,那么在Vista之前的平台上,怎能仅是功能上的模仿呢?
当用Visual Studio 2008(MFC 9)创建一个MFC对话框项目时,这个问题就会更加明显,在对话框上放置一个Command Link,生成之后运行它。如果开发环境早于Vista,就会见不到Command Link,但在Vista上运行同一程序,它又出现了。
变通方法
这里讲的“变通方法”并不是一个彻底的解决方案,彻底的解决方案可在Vista之前版本上实现同一行为表现,但这个方法像是重写控件本身。前面的问题说到,在Vista之前的平台上,通用控件DLL不能解释BS_COMMANDLINK,且不管从哪方面看,微软也不像要在Vista之前的平台上提供新版的commctrl.dll,不过,Command Link非常简单,也许有种方法可在Vista之前的平台至少提供相似的功能。
既然BS_COMMANDLINK帮不上忙,那要是在Vista平台上使用它,而在之前的平台上不用它呢,又怎样?当然行得能了,因为它就是一个普通的按钮控件,有以下两种方法:
Ø 通过button.ModifyStyle(BS_COMMANDLINK,0);让程序自己去掉它。
Ø 以MFC方式子类化按钮控件,并修改它。要是编写自己的类,比如说CCGCommandLinkButton,从CButton派生并在类中进行处理呢?用法就非常简单了,使用者只需用CCGCommandLinkButton对象,在模板中关联Command Link资源就行了。
下面就开始了,在VS2008中建一个基于对话框的MFC项目,所有均为默认值,在对话框上放一个“Command Button Control”,生成并运行。出于本文讨论的目的,要在Vista之前的平台上进行测试(Vista自身当然能运行了),这时是一个带有OK及Cancel按钮的对话框,但没有Command Link按钮。
1、关闭VS2008中的对话框资源。找到菜单“项目”——“添加类”,选择右边的“MFC”,选择“MFC类”,单击“添加”,类名写为“CCGCommandLinkButton”。把基类从CWnd改为CButton,单击“完成”。
2、转到资源视图,打开对话框资源,选择Command Link Button,鼠标右键单击选择“添加变量”。把变量名改为,如“m_btnCommand”,单击“完成”。
3、打开对话框类头文件,在最上方,添加:#include "CGCommandLinkButton.h",并把m_btnCommand的声明类型从CButton改为CCGCommandLinkButton。
4、生成并运行。还是看不到Command Link Button。
5、下一步,要在非Vista平台上去掉BS_COMMANDLINK样式属性,为简单起见,在CCGCommandLinkButton类中添加一个private 的BOOL变量m_bPreVista,并在构造函数中,把它设为TRUE;以后,就可通过查询Windows的版本来设置这个值。
CCGCommandLinkButton::CCGCommandLinkButton()
{
m_bPreVista = TRUE;
}
6、打开文件CCGCommandLinkButton.h,如果此时在头文件的类范围(class scope)中,应该可看到带有“覆盖”按钮的属性面板,从列表中选择PreCreateWindow,从下拉框中添加PreCreateWindow,同样地,添加PreSubclassWindow。
7、PreCreateWindow是一个虚函数,其作为CWnd::Create/Ex函数的一部分,在窗口实际创建之前被调用,我们可在此修改CREATESTRUCT,以便其后使用Create/Ex函数创建按钮时,去掉BS_COMMANDLINK样式属性。
BOOL CCGCommandLinkButton::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Add your specialized code here and/or call the base class
if(TRUE == m_bPreVista)
{
//去掉BS_COMMANDLINK样式属性,因为它不能应用于Vista之前的平台
cs.style &= (~BS_COMMANDLINK);
}
return CButton::PreCreateWindow(cs);
}
8、PreSubclassWindow也是一个虚函数,作为MFC DoDataExchange的一部分,在第一次子类化控件时被调用,我们可在窗口创建之后修改任意属性。这在对话框模板中非常有用,尤其是太晚而不能影响创建过程情况时。在此也要去掉BS_COMMANDLINK样式属性。
void CCGCommandLinkButton::PreSubclassWindow()
{
// TODO: Add your specialized code here and/or call the base class
if(TRUE == m_bPreVista)
{
//去掉BS_COMMANDLINK样式属性,因为它不能应用于Vista之前的平台
ModifyStyle(BS_COMMANDLINK,0);
}
CButton::PreSubclassWindow();
}
9、以上的代码非常简单,它们在Vista之前的平台上去掉了BS_COMMANDLINK样式属性,生成并运行,现在应该就可以看到按钮了。
10、现在,打开对话框类的实现文件,并在OnInitDialog后添加代码设置注释信息:
m_btnCommand.SetNote(_T("This is a test note"));
return TRUE; // return TRUE unless you set the focus to a control
生成并运行,咦,什么也没变啊,注释文本并没有出现。
11、现在的任务就是要使按钮看起来至少像个Command Link,这就需要显示按钮文本,并在其下显示注释,有以下两种方法:
Ø 可把注释与实际的窗体文本(Windows Text)组合在一起,在两者之间有插一个新行,之后再把组合后的文本设为窗体文本。然而,在对话框上实验之后并不可行。
Ø 另一种方法是把两段文本分开对待,并把它们“画”到按钮上。这意味着需要一个自绘制按钮(BS_OWNERDRAW),这是可行的,因为使用Command Link按钮的大多数人八成不会用到BS_OWNERDRAW属性,如果要使用BS_OWNERDRAW来自绘制,那干嘛还使用Command Link样式呢?所以可行。
12、修改PreCreateWindow与PreSubclassWindow以添加BS_OWNERDRAW样式属性。
BOOL CCGCommandLinkButton::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Add your specialized code here and/or call the base class
if(TRUE == m_bPreVista)
{
//去掉BS_COMMANDLINK样式属性,因为它不能应用于Vista之前的平台
cs.style &= (~BS_COMMANDLINK);
//添加OWNERDRAW,因为现在要我们自己绘制文本及注释
cs.style |= BS_OWNERDRAW;
}
return CButton::PreCreateWindow(cs);
}
void CCGCommandLinkButton::PreSubclassWindow()
{
// TODO: Add your specialized code here and/or call the base class
if(TRUE == m_bPreVista)
{
//去掉BS_COMMANDLINK样式属性,因为它不能应用于Vista之前的平台
//添加OWNERDRAW,因为现在要我们自己绘制文本及注释
ModifyStyle(BS_COMMANDLINK,BS_OWNERDRAW);
}
CButton::PreSubclassWindow();
}
13、添加BS_OWNERDRAW属性后意味着你要自己绘制按钮,一般来说,这通常是由父窗口完成的,但是在MFC中,通过重载DrawItem,可以把它做成一个自包含的功能类,就像添加PreCreateWindow一样,在CCGCommandLinkButton类中重载DrawItem,下面的代码只是简单地绘制窗口文本:
void CCGCommandLinkButton::DrawItem(LPDRAWITEMSTRUCT
lpDrawItemStruct)
{
// TODO: Add your code to draw the specified item
CString szWindowText;
GetWindowText(szWindowText);
CDC dc;
dc.Attach(lpDrawItemStruct->hDC);
dc.SetBkMode(TRANSPARENT);
dc.DrawText(szWindowText,&lpDrawItemStruct->rcItem,DT_LEFT);
dc.Detach();
}
14、现在,怎样获取注释文本呢?如果你认为是GetNote()的话,将会发现它不起作用,因为在Vista之前的平台上根本就没有,那么要怎样获取文本呢?
15、要知道调用SetNote(),只不过是对SendMessage BCM_SETNOTE的调用,且把文本作为LParam传递。这给了我们一些提示,可以在CCGCommandLinkButton类中处理这个消息,并把它存在一个CString成员变量中以备用,记住只能在Vista之前的平台上这样做。
打开CCGCommandLinkButton.h文件,添加一protected函数:
afx_msg LRESULT OnSetNote(WPARAM wParam, LPARAM lParam);
添加一个名为m_szNote的private CString变量,打开CCGCommandLinkButton.cpp,添加一个消息映射:
BEGIN_MESSAGE_MAP(CCGCommandLinkButton, CButton)
ON_MESSAGE(BCM_SETNOTE,&OnSetNote)
END_MESSAGE_MAP()
添加实现部分:
LRESULT CCGCommandLinkButton::OnSetNote(WPARAM wParam,
LPARAM lParam)
{
if(TRUE == m_bPreVista)
{
m_szNote = (LPTSTR)lParam;
//当注释更改时,强制Invalidate()
UpdateWindow();
}
else
{
//如果是Vista及以上版本,默认就行了。
Default();
}
return TRUE;
}
注释部分需要在DrawItem的实现中更新,最简单的是在窗口文本下绘制注释文本,但在此我们多做一点,让窗口文本为黑体,而注释文本不变,以下是代码:
void CCGCommandLinkButton::DrawItem(LPDRAWITEMSTRUCT
lpDrawItemStruct)
{
// TODO: Add your code to draw the specified item
RECT itemRect = lpDrawItemStruct->rcItem;
CString szWindowText;
GetWindowText(szWindowText);
CDC dc;
dc.Attach(lpDrawItemStruct->hDC);
//绘制背景
CBrush brBtnShadow;
brBtnShadow.CreateSolidBrush(GetSysColor(COLOR_BTNSHADOW));
dc.FrameRect(&itemRect, &brBtnShadow);
//取得当前字体
CFont* pFont = GetFont();
CFont* pOldFont = NULL;
CFont boldFont;
if(pFont)
{
LOGFONT lf;
pFont->GetLogFont(&lf);
lf.lfWeight = FW_BOLD;
boldFont.CreateFontIndirect(&lf);
pOldFont = dc.SelectObject(&boldFont);
}
InflateRect(&itemRect, -5, -5);
RECT oldRect = itemRect;
//首先,取得需绘制窗口文本的矩形区
//可从返回的矩形区知道用于绘制窗口文本的高度是多少
//因此,绘制注释文本的坐标就大致可得出了
dc.DrawText(szWindowText,&itemRect,DT_LEFT | DT_WORDBREAK |
DT_CALCRECT );
//现在开始绘制了
dc.DrawText(szWindowText,&itemRect,DT_LEFT | DT_WORDBREAK);
if(pFont)
{
//设成黑体之后,恢复原字体
dc.SelectObject(pOldFont);
}
//绘制注释文本
oldRect.top = itemRect.bottom + 5;
dc.DrawText(m_szNote,&oldRect,DT_LEFT | DT_WORDBREAK);
dc.Detach();
}
生成并运行,就可看到按钮及下文的注释都以正确的字体显示了。
16、再来看一下SetNote,现在,又多出两个方法了:GetNote及GetNotelength,为BCM_GETNOTE、BCM_GETNOTELENGTH分别添加消息映射,以下是它们的实现部分:
ON_MESSAGE(BCM_GETNOTELENGTH,&OnGetNoteLength)
ON_MESSAGE(BCM_GETNOTE,&OnGetNote)
LRESULT CCGCommandLinkButton::OnGetNoteLength(WPARAM wParam,
LPARAM lParam)
{
if(TRUE == m_bPreVista)
{
return m_szNote.GetLength();
}
else
{
//如果是Vista及以上版本,默认就行了。
return Default();
}
}
LRESULT CCGCommandLinkButton::OnGetNote(WPARAM wParam,
LPARAM lParam)
{
if(TRUE == m_bPreVista)
{
//首先,检查长度是否足够
DWORD dwRequiredLen = m_szNote.GetLength() + 1;
DWORD* pSize = (DWORD*)wParam;
if((*pSize) < dwRequiredLen)
{
//如果空间不够,把 *wParam设为所需长度
//并把最后的错误设为ERROR_INSUFFICIENT_BUFFER
*pSize = dwRequiredLen;
SetLastError(ERROR_INSUFFICIENT_BUFFER);
return FALSE;
}
else
{
//复制文本到lParam
_tcscpy_s((LPTSTR)lParam,(*pSize),m_szNote);
return TRUE;
}
}
else
{
return Default();
}
}
最终结果如下图:
17、剩下的就是在运行时设置m_bPreVista变量了,可用GetVersionEx API来检查主版本,Vista的主版本号为6。下面是CCGCommandLinkButton构造函数中的代码:
CCGCommandLinkButton::CCGCommandLinkButton()
{
OSVERSIONINFO osvi;
ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&osvi);
m_bPreVista = osvi.dwMajorVersion < 6;
}
生成并运行,现在exe就可运行在Vista及之前平台上了。在Vista之前平台上,程序如下图:
在Vista上,程序如下图:
结论
以上绝不是一个彻底的解决方案,它没有对不同状态下的按钮外观进行处理,也没有涉及到系统主题,本文的目的在于为多种操作系统编写通用代码,而无须损失某些新特性,Command Link按钮相对复选框及单选按钮来说非常直观,可以在以后的程序中加以采用。