如何定制对话框中的回车键
基于对话框的程序中,每次按下回车键时,程序都退出。去掉按钮的 BS_DEFPUSHBUTTON 属性并重写OnOK函数也没用。那么如何定制回车键的行为呢?这个问题很easy,但是要说明白,却要费点时间。
这个问题在Windows的开发中由来已久,对于初学者来说,这是个恼人的问题,幸运的是,人们找到了多种解决这个问题的方案。本文将告诉你定制回车键行为的方法。
一. 实现方法
如果你想要disable回车键,最简单的方法是重载OnOK函数,这固然是个不坏的主意,但如果你重载OnOK,让它什么事情也不干,那麽当用户用鼠标按下回车键想真正做些什么的时候怎么办呢?你可以改变回车键的ID,如:ID_MY_OK,并写一个调用EndDialog的处理器,这个方法虽然也能行得通,但显得有点不专业。
另外一种方法是disable回车键的“默认”属性。这也是本文开始所提出的方法,之所以没有成功,是因为仅仅uncheck 回车键的 BS_DEFPUSHBUTTON 属性是不够的,你可以利用Spy++仔细地观察控制和实验就能发现回车键仍然我行我素发送退出消息。
问题出在哪呢?你必须区分OK键和回车键,你可以写一个OnOK处理器调用GetCurrentMessage函数获取最后发送的消息,应该是WM_COMMAND,再检查WPARAM的低位字(low-order word)看看命令来自何处。
要解决问题,必须搞清楚背后所发生的一切,在Spy++中可以看到,当用户按下回车键时,Windows发送一个特殊的WM_GETDEFID消息来获得缺省的命令ID,Windows再将它作为WM_COMMAND发送。所以,你要做的就是重载WM_GETDEFID消息,在有关Windows的文档中是这样描述WM_GETDEFID返回值的:“如果有缺省得按钮,则返回值的高位字包含DC_HASDEFID,低位字包含控制的标识符。否则,返回值是零。”
根据这段描述,假设如果没有缺省得按钮,则返回值应该是零。如果想要disable缺省得ID,必须在高位字中返回DC_HASDEFID。 BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
ON_MESSAGE(DM_GETDEFID, OnGetDefID)
...
END_MESSAGE_MAP()
LRESULT CMyDlg::OnGetDefID(WPARAM wp, LPARAM lp)
{
return MAKELONG(0,DC_HASDEFID);
}
因为MFC没有对应DM_GETDEFID的宏,你必须使用通用的ON_MASSAGE宏。这样用户可以随意按回车键,但什么事都不会发生。上面的做法是解决了按回车键程序退出的问题。但是又产生了另外一个问题:如果想要回车键做些事情怎么办呢?有一些人曾经问过如何将回车键映射到TAB键,既按下回车键就象按下TAB键一样-也就是说输入焦点移动到下一个对话框控制。这需要做一些工作才行,但最简单的方式是使用加速键。许多程序员试图用OnChar,我会对他们说:No,no,no! OnChar是一个低级趣味的东西,你应该想方设法避免它,更糟的还有WM_KEYDOWN,WM_KEYUP之类的东西。谁能处理这些东西?OnChar可以用来限制允许输入编辑框的字符,如:数字,字母等。如果想要将一个键映射到一个命令,加速键才是最好的方法。
在本文的例子为VK_RETURN创建了一个加速键,将它映射到命令ID_MY_ENTER,并写一个命令处理器来做你想做的事情。 BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
ON_COMMAND(ID_MY_ENTER, OnMyEnter)
......
END_MESSAGE_MAP()
void CMyDlg::OnMyEnter()
{
NextInTabOrder();
}
下图是本文例子的对话框和代码,代码中的NextInTabOrder是实际起作用的函数。它使用GetNextDlgTabItem来获得Tab顺序的下一个控制焦点。
while (GetMessage(...)) {
TranslateMessage(...);
DispatchMessage(...);
}
在这里细节不是重要的,重要的是消息并不到达程序的流程,你必须请求消息。这是一种人为的非抢先式多任务方法,这种方法通过每一个任务精诚协作来仿造多任务环境,随着增加的功能越来越多,有人想到了加速键表的主意,这个表用来映射按键和命令IDs。为了实现这个目的,他们发明了一个叫TranslateAccelerator的函数。现在这个消息泵变成了如下的样子: while (GetMessage(...)) {
if (TranslateAccelerator(hAccel...)) {
// handled, continue looping
} else {
TranslateMessage(...);
DispatchMessage(...);
}
}
hAccel是个加速键表句柄,在这里细节同样不是重要的,重要的是如何利用加速键表,也就是要有一个专门的函数将按键消息解释为WM_COMMAND消息。TranslateAccelerator寻找WM_KEYDOWN,WM_CHAR,WM_KEYUP序列与表中键值匹配的字符。如果找到,它插入一条WM_COMMAND到消息队列,在消息队列中的命令ID可以是加速键表定义的任何入口。这样你只要设置加速键表(在资源中)并记住调用对应的函数TranslateAccelerator,就什么都不用担心了。
时间转眼间进入了21世纪,随着C++和MFC的日臻成熟,现在几乎整个消息循环(但不是全部)都被隐藏到了MFC中,为了能让任何窗口都有机会获得一点消息泵的行为,MFC提供了一个专门的虚函数PreTranslateMessage,如果你有足够的勇气去探究CWinThread中的消息处理机制的话,你会遇到类似如下的代码: // 简化后的 CWinThread
while (GetMessage(...)) {
if (PreTranslateMessage(...)) {
// continue looping
} else {
TranslateMessage(...);
DispatchMessage(...);
}
}
CWinThread::PreTranslateMessage是个虚函数,在应用中,其缺省的实现以相同的名字调用另一个虚函数,CWnd::PreTranslateMessage。因此,如果你需要在消息循环中做些什么的话-如解释加速键-你只要重载PreTranslateMessage即可。实际上,这就是CFrameWnd处理加速键的方法。 BOOL CFrameWnd::PreTranslateMessage(MSG* pMsg)
{
......
if (pMsg->message >= WM_KEYFIRST &&
pMsg->message <= WM_KEYLAST)
{
::TranslateAccelerator(m_hAccelTable,...);
}
}
CFrameWnd 从哪里获得加速键表呢?当你加载框架时,CFrameWnd::LoadFrame用与文档模板相同的ID(如IDR_MAINFRAME)查找加速键表,并将他加载到m_hAccelTable。所有的处理细节在MFC中都是自动的、隐蔽的,你不用去操心-仅对主框架而言,如果是对话框,则是另外一种情况。因为CDialog不是从CFrameWnd派生而来,所以不继承任何有关加速键的内容。
不用担心,我们可以模仿CFrameWnd的工作,很容易为对话框增加加速键的功能。第一步是加载加速键,加载加速键最好的地方是在对话框的OnInitDialog函数中: BOOL CMyDlg::OnInitDialog()
{
CDialog::OnInitDialog();
......
// Load accelerators
m_hAccel = ::LoadAccelerators(AfxGetResourceHandle(),
m_lpszTemplateName);
ASSERT(m_hAccel);
return TRUE;
}
在加速键表中,你可以使用任何ID。这里我使用的是对话框本身的ID,(m_lpszTemplateName既可以是一个串名,也可以是一个MAKEINTRESOURCE使用的整型ID): // 本文例子中的加速键(In DlgKeys.rc )
IDD_MYDIALOG ACCELERATORS DISCARDABLE
BEGIN
VK_RETURN, ID_MY_ENTER, VIRTKEY, NOINVERT
END
一旦你已经加载加速键,剩下的事情是重载PreTranslateMessage函数: BOOL CMyDlg::PreTranslateMessage(MSG* pMsg)
{
if (WM_KEYFIRST <= pMsg->message &&
pMsg->message <= WM_KEYLAST)
{
HACCEL hAccel = m_hAccel;
if (hAccel &&
::TranslateAccelerator(m_hWnd, hAccel, pMsg))
return TRUE;
}
return CDialog::PreTranslateMessage(pMsg);
}
之所以要检查按键类的消息(从WM_ KEYFIRST 到 WM_KEYLAST)是为了提高速度。如果你知道不是一个按键消息,你就不用浪费时间去调用TranslateAccelerator。再说TranslateAccelerator是一个虚拟函数,不用增加一个消息映射入口。仅仅写这个函数就可以了。
二、编程步骤
1、 启动Visual C++6.0,生成一个Win32应用程序,将该程序命名为"DlgKeys";
2、 使用CLASSWIZARD为应用程序添加CdlgWithAccelerators和CmyDlg类;
3、 在程序的资源中添加添加加速键资源,内容如下:ID_MY_ENTER, VK_RETURNVIRTKEY,VIRTKEY;
4、添加代码,编译运行程序。
三、程序代码
/////////////////////////////////////////
#include "stdafx.h"
// Generic dialog-that-uses-accelerators.
class CDlgWithAccelerators : public CDialog {
public:
CDlgWithAccelerators(UINT nIDTemplate, CWnd* pParentWnd = NULL);
CDlgWithAccelerators(LPCTSTR lpszTemplateName, CWnd* pParentWnd = NULL);
~CDlgWithAccelerators();
protected:
HACCEL m_hAccel; // accelerator table
// MFC overrides
virtual BOOL OnInitDialog();
virtual BOOL PreTranslateMessage(MSG* pMsg);
DECLARE_MESSAGE_MAP()
};
///////////////////////////////////////////
// CDlgWithAccelerators is a general-purpose class that adds accelerators to CDialog.
#include "stdafx.h"
#include "dlgaccel.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
BEGIN_MESSAGE_MAP(CDlgWithAccelerators,CDialog)
END_MESSAGE_MAP()
CDlgWithAccelerators::CDlgWithAccelerators(LPCTSTR lpszTemplateName, CWnd* pParentWnd) : CDialog(lpszTemplateName, pParentWnd)
{}
CDlgWithAccelerators::CDlgWithAccelerators(UINT nIDTemplate,
CWnd* pParentWnd) : CDialog(nIDTemplate, pParentWnd)
{}
CDlgWithAccelerators::~CDlgWithAccelerators()
{}
/////////////////////////// Pre-translate message: translate keystrokes using acclerator table.
BOOL CDlgWithAccelerators::PreTranslateMessage(MSG* pMsg)
{
if (WM_KEYFIRST <= pMsg->message && pMsg->message <= WM_KEYLAST) {
HACCEL hAccel = m_hAccel;
if (hAccel && ::TranslateAccelerator(m_hWnd, hAccel, pMsg))
return TRUE;
}
return CDialog::PreTranslateMessage(pMsg);
}
//////////////////// Initialize dialog: load accelerators
BOOL CDlgWithAccelerators::OnInitDialog()
{
BOOL bRet = CDialog::OnInitDialog();
// Load dialog's accelerators
m_hAccel = ::LoadAccelerators(AfxGetResourceHandle(),
m_lpszTemplateName); // use same resource name as dialog
return bRet;
}
/////////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "resource.h"
#include "dlgaccel.h"
#include "TraceWin.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////MFC app
class CMyApp : public CWinApp {
public:
CMyApp();
~CMyApp();
virtual BOOL InitInstance();
DECLARE_MESSAGE_MAP()
};
CMyApp theApp; // THE one-and-only app
//////////// frame window
class CMainFrame : public CFrameWnd {
protected:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
public:
CMainFrame();
~CMainFrame();
};
//////////////////// Typical dialog
class CMyDlg : public CDlgWithAccelerators {
public:
CMyDlg(CWnd* pParent = NULL); // standard constructor
protected:
HICON m_hIcon;
void NextInTabOrder();
// MFC overrides
virtual BOOL OnInitDialog();
afx_msg void OnMyEnter();
afx_msg LRESULT OnGetDefID(WPARAM wp, LPARAM lp);
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyApp, CWinApp)
END_MESSAGE_MAP()
CMyApp::CMyApp()
{
// nothing to do
}
CMyApp::~CMyApp()
{
// nothing to do
}
//////////////////// InitInstance: create dialog as child
BOOL CMyApp::InitInstance()
{
// create frame window and load it
CMainFrame* pFrame = new CMainFrame;
m_pMainWnd = pFrame;
pFrame->LoadFrame(IDR_MAINFRAME, WS_OVERLAPPED, NULL, NULL);
CMyDlg dlg(pFrame); // create dialog and run it
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{}
else if (nResponse == IDCANCEL)
{}
return FALSE; // quit
}
CMainFrame::CMainFrame()
{
// nothing to do
}
CMainFrame::~CMainFrame()
{
// nothing to do
}
///////////// Pre-create window: set WS_EX_TOOLWINDOW style to hide dialog from task bar
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if (CFrameWnd::PreCreateWindow(cs)) {
cs.dwExStyle |= WS_EX_TOOLWINDOW;
return TRUE;
}
return FALSE;
}
BEGIN_MESSAGE_MAP(CMyDlg, CDlgWithAccelerators)
ON_COMMAND(ID_MY_ENTER, OnMyEnter)
// The following is NOT needed since I am using accelerators to map
// ENTER to ID_MY_ENTER. But if all you want to do is ignore the Enter key,
// you can handle DM_GETDEFID as below.
// ON_MESSAGE(DM_GETDEFID, OnGetDefID) // not used
END_MESSAGE_MAP()
CMyDlg::CMyDlg(CWnd* pParent) : CDlgWithAccelerators(IDD_MYDIALOG, pParent)
{}
//////////////////// Initialize dialog:
BOOL CMyDlg::OnInitDialog()
{
CDlgWithAccelerators::OnInitDialog();
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
ASSERT(m_hIcon);
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// use same resource name as dialog to load dialog's accelerators
m_hAccel = ::LoadAccelerators(AfxGetResourceHandle(),m_lpszTemplateName);
ASSERT(m_hAccel);
return TRUE; // return TRUE unless you set the focus to a control
}
//////////////////// This is called to handle ID_MY_ENTER--ie, Enter key.
void CMyDlg::OnMyEnter()
{
TRACE(_T("CMyDlg::OnMyEnter\n"));
NextInTabOrder(); // move to next control
}
//////////////////// Helper function to move focus to the next control.
void CMyDlg::NextInTabOrder()
{
CWnd* pWndNext = GetNextDlgTabItem(GetFocus());
if (pWndNext) {
pWndNext->SetFocus();
}
}
//////////////////
// This function is not used, since its message map entry is commented out.
// If all you want to do is ignore the Enter key (not map it to a command),
// then all you have to do is return zero here. Note that you MUST return
// the special code DC_HASDEFID in the high-order word!!
LRESULT CMyDlg::OnGetDefID(WPARAM wp, LPARAM lp)
{
TRACE(_T("CMyDlg::OnGetDefID\n"));
return MAKELONG(0,DC_HASDEFID);
}
四、小结
综上所述,MFC中为对话框添加加速键功能的方法就是:加载加速键和重载PreTranslateMessage函数。也就是说,如果你决定使用加速键,不用去操心OnGetDefID,而是将没有命令处理器的ID映射到VK_RETURN。本文的例子代码中封装了一个又加速键的新对话框类:CdlgWinAccelerators,它是一个通用类。希望大家喜欢它。最后祝大伙编程愉快。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
摘要 :本文讲述了在指定的编辑框上能响应从键盘输入回车键的一种方法,对进程内消息的解析、动态获取指定资源ID等技术也作了简要描述。
关键字 :Microsoft Visual C++ 6.0、编辑框、回车键、消息、资源
一、引言
在通常的以CEditView为基类的单文档/多文档视图程序中,可以很好的响应键盘输入的回车键,只需比较最近两次的输入的字符,看看最新输入的字符是否内码是13(0x0d,回车键的内码)即可识别出来,而要单独把一个编辑框放入对话框中却根本不响应,这个看似简单的问题在实际应用中还是解决起来比较困难的。尤其是当一个充当表单录入的对话框上有若干个编辑框,这就要求在一个编辑框添完一项表单后用习惯的回车键将该编辑框上的数据读取到内存中去,并自动将光标移动到下一个编辑框中准备填写下一栏表单。无疑这种界面是十分人机友好的,使录入人员不必去执行每填一下表单就去按一下执行读入到缓存功能的按钮的烦琐操作。但上述功能的实现却并不象其演示的功能那样简单,下面本文就对这项技术的实现及附带的其他技术作简要的介绍。
二、不能响应回车键的原因分析
之所以在以CEditView作为基类的程序中可以响应回车键,是由于该程序的视类本身就是一个Edit控件,这就是问题的关键所在。CEditView作为CView的派生类能响应从键盘输入的各种消息,其中有和键盘输入相关的WM_CHAR、WM_KEYDOWN、WM_KEYUP等消息。我们就可以在这些消息的响应函数中灵活地设计程序去捕捉到回车键的输入,并执行响应的操作。
当我们将编辑框作为一个普通的控件放到对话框上时情况就发生了变化。在此我们以CFormView为例,它也是CView的一个派生类,视是一个Form窗体(即对话框),当放有编辑框的窗体有回车键输入时,由于只有编辑框可以接受从键盘输入的字符,所以当键盘按下时统统把消息都发给了编辑框(在Windows下每个窗口、按钮、编辑框都看作一个窗口,都可以接受消息),可以通过ClassWizard在"Object IDs"选中编辑框所对应的ID号,在右边的消息框中可以看出该编辑框并不能响应WM_CHAR等消息,只能用EN_CHANGE事件来做类似的响应。可当我们加入了对该事件的处理函数时,却又将回车键当作控制字符,当输入回车键并不会激发EN_CHANGE事件,也就是说用这种方法仍旧无法捕获回车键的输入。
三、拦截回车键的思路与方法
Windows操作系统下各个窗口、控件归根结底都是通过系统的各种各样的消息来相互协调、相互联系的,而我们所遇到的这个问题换到消息的角度说就是"如何使程序能响应在编辑框上输入的回车键所发出的消息",只要能响应到这个消息,剩下的工作都可以在消息处理函数中完成。所以有必要对Windows系统的消息机制做些了解。
每个Windows应用程序开始执行后,Windows都为该程序创建一个"消息队列(message queue)",用来存放邮寄给该程序可能创建的各种不同窗口的消息。消息队列中消息的结构(MSG)为:
typedef struct tagMSG {
/*msg*/ HWND hwnd;//窗口句柄,标识接收消息的窗口。
UINT message;//消息标识号,如WM_TIMER等。
WPARAM wParam;//消息参数,当为键盘消息时,表示虚拟键码如VK_RETURN等。
LPARAM lParam;//消息参数。
DWORD time;//邮寄消息的时间。
POINT pt;//邮寄消息时的光标位置,用屏幕坐标表示。
}MSG;
在系统下最常用的消息循环是调用GetMessage()函数从消息队列中取出消息,然后调用DespatchMessage() 函数让系统把消息发送给窗口函数,一般情况下其结果是把窗口的所有消息都传送给窗口函数。但特殊情况下可以在GetMessage()函数获得消息而又没发送出去之前,通过TranslateMessage()函数可以中途对消息进行解析,可以对指定的消息进行拦截,拦截后即可以照样发送出去,也可以不继续发送,完成对该消息的拦截,下面代码是该过程的示例:
MSG msg;
while(GetMessage(&msg,NULL,NULL,NULL,NULL)
{
TranslateMessage(&msg);…… //对拦截的消息进行处理
DispathchMessage(&msg);
}
由于按下回车键时把产生的消息加入到消息队列中了,也传给了编辑框,但仅仅是由于编辑框没有能力处理该消息而造成了无法对回车键的响应,所以可以在消息循环里在把消息发送到编辑框之前就对消息进行拦截,并对其进行处理。其效果同编辑框响应回车键是一样的,仅在时序上有所提前而已。上述代码是在SDK(Software Develope Kits)下使用的,在MFC(Microsoft Foundation Class)下早已对其进行了封装,可以通过重载虚函数PreTranslateMessage()对所关心的消息进行解析:
BOOL CTestView::PreTranslateMessage(MSG* pMsg)
{
if (WM_KEYFIRST <= pMsg-> message && pMsg-> message <= WM_KEYLAST)
{
if(pMsg-> wParam==VK_RETURN )
{
UpdateData(TRUE);
AfxMessageBox(m_Text);
}
}
return CFormView::PreTranslateMessage(pMsg);
}
在上面的代码中,首先将pMsg-> message所表示的消息同WM_KEYFIRST 和WM_KEYLAST比较,确定是键盘消息,然后通过消息参数pMsg-> wParam的值来判断是否是回车键(VK_RETURN,虚拟键码可以从SDK相关资料查到)。如是,则可以将已输入到编辑框中的字符读取到m_Text中,并将其显示出来
四、对编辑框的识别
前面已经可以对回车键响应了,可一个表单窗体有若干个编辑框,其各自的处理方式不尽相同,这就有必要对编辑框进行识别、对不同的编辑框做不同的处理。而且当按下回车键时必须保证只有当前有焦点的编辑框能完成对回车键的响应动作,否则也就失去了实际意义。
在Windows下的程序中,所有的资源都是有唯一标号的,使每个资源对象能唯一的区别于其他资源,所以我们可以通过资源ID来对编辑框做出区别,使之完成各自的响应处理。在Microsoft Visual C++ 6.0下可以通过"View"菜单的"ID= Resource Symboles…"查到指定ID的资源标识号的实际数值,如在本例中的两个编辑框IDC_EDIT1和IDC_EDIT2所对应的数值分别为1000和1001,对前面的解析消息的代码做些改动,主要如下所示:
……
if(pMsg-> wParam==VK_RETURN )
{
HWND hWnd=::GetFocus();
int iID=::GetDlgCtrlID(hWnd);
if(iID==1000)//第一个编辑框的标识为1000
{
UpdateData(TRUE);
AfxMessageBox(m_Text1);//显示第一个编辑框的内容
}
if(iID==1001) //第二个编辑框的标识为 1001
{
UpdateData(TRUE);
AfxMessageBox(m_Text2);//显示第二个编辑框的内容
}
}
……
在此通过API函数::GetFocus()(注意前面的"::",标识是全局API函数,而非某个类中的成员函数)取得当前光标所处的(即有焦点的)编辑框的句柄,然后通过API函数::GetDlgCtrlID()根据这个句柄返回此窗口资源的ID 号,该ID号是动态获取的,使之同预先查看好的编辑框的ID作下比较即可区分出是需要哪个编辑框对回车键作出响应。
小结:
本文通过对消息的解析实现了对特定编辑框的回车键的响应,在对消息机制有了基本的了解之后,可以用与本文类似的方法,对代码稍作改动,就可以使其他一些不能响应特殊消息的控件能接收、处理特定的消息。
这个问题在Windows的开发中由来已久,对于初学者来说,这是个恼人的问题,幸运的是,人们找到了多种解决这个问题的方案。本文将告诉你定制回车键行为的方法。
一. 实现方法
如果你想要disable回车键,最简单的方法是重载OnOK函数,这固然是个不坏的主意,但如果你重载OnOK,让它什么事情也不干,那麽当用户用鼠标按下回车键想真正做些什么的时候怎么办呢?你可以改变回车键的ID,如:ID_MY_OK,并写一个调用EndDialog的处理器,这个方法虽然也能行得通,但显得有点不专业。
另外一种方法是disable回车键的“默认”属性。这也是本文开始所提出的方法,之所以没有成功,是因为仅仅uncheck 回车键的 BS_DEFPUSHBUTTON 属性是不够的,你可以利用Spy++仔细地观察控制和实验就能发现回车键仍然我行我素发送退出消息。
问题出在哪呢?你必须区分OK键和回车键,你可以写一个OnOK处理器调用GetCurrentMessage函数获取最后发送的消息,应该是WM_COMMAND,再检查WPARAM的低位字(low-order word)看看命令来自何处。
要解决问题,必须搞清楚背后所发生的一切,在Spy++中可以看到,当用户按下回车键时,Windows发送一个特殊的WM_GETDEFID消息来获得缺省的命令ID,Windows再将它作为WM_COMMAND发送。所以,你要做的就是重载WM_GETDEFID消息,在有关Windows的文档中是这样描述WM_GETDEFID返回值的:“如果有缺省得按钮,则返回值的高位字包含DC_HASDEFID,低位字包含控制的标识符。否则,返回值是零。”
根据这段描述,假设如果没有缺省得按钮,则返回值应该是零。如果想要disable缺省得ID,必须在高位字中返回DC_HASDEFID。 BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
ON_MESSAGE(DM_GETDEFID, OnGetDefID)
...
END_MESSAGE_MAP()
LRESULT CMyDlg::OnGetDefID(WPARAM wp, LPARAM lp)
{
return MAKELONG(0,DC_HASDEFID);
}
因为MFC没有对应DM_GETDEFID的宏,你必须使用通用的ON_MASSAGE宏。这样用户可以随意按回车键,但什么事都不会发生。上面的做法是解决了按回车键程序退出的问题。但是又产生了另外一个问题:如果想要回车键做些事情怎么办呢?有一些人曾经问过如何将回车键映射到TAB键,既按下回车键就象按下TAB键一样-也就是说输入焦点移动到下一个对话框控制。这需要做一些工作才行,但最简单的方式是使用加速键。许多程序员试图用OnChar,我会对他们说:No,no,no! OnChar是一个低级趣味的东西,你应该想方设法避免它,更糟的还有WM_KEYDOWN,WM_KEYUP之类的东西。谁能处理这些东西?OnChar可以用来限制允许输入编辑框的字符,如:数字,字母等。如果想要将一个键映射到一个命令,加速键才是最好的方法。
在本文的例子为VK_RETURN创建了一个加速键,将它映射到命令ID_MY_ENTER,并写一个命令处理器来做你想做的事情。 BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
ON_COMMAND(ID_MY_ENTER, OnMyEnter)
......
END_MESSAGE_MAP()
void CMyDlg::OnMyEnter()
{
NextInTabOrder();
}
下图是本文例子的对话框和代码,代码中的NextInTabOrder是实际起作用的函数。它使用GetNextDlgTabItem来获得Tab顺序的下一个控制焦点。
![]() |
while (GetMessage(...)) {
TranslateMessage(...);
DispatchMessage(...);
}
在这里细节不是重要的,重要的是消息并不到达程序的流程,你必须请求消息。这是一种人为的非抢先式多任务方法,这种方法通过每一个任务精诚协作来仿造多任务环境,随着增加的功能越来越多,有人想到了加速键表的主意,这个表用来映射按键和命令IDs。为了实现这个目的,他们发明了一个叫TranslateAccelerator的函数。现在这个消息泵变成了如下的样子: while (GetMessage(...)) {
if (TranslateAccelerator(hAccel...)) {
// handled, continue looping
} else {
TranslateMessage(...);
DispatchMessage(...);
}
}
hAccel是个加速键表句柄,在这里细节同样不是重要的,重要的是如何利用加速键表,也就是要有一个专门的函数将按键消息解释为WM_COMMAND消息。TranslateAccelerator寻找WM_KEYDOWN,WM_CHAR,WM_KEYUP序列与表中键值匹配的字符。如果找到,它插入一条WM_COMMAND到消息队列,在消息队列中的命令ID可以是加速键表定义的任何入口。这样你只要设置加速键表(在资源中)并记住调用对应的函数TranslateAccelerator,就什么都不用担心了。
时间转眼间进入了21世纪,随着C++和MFC的日臻成熟,现在几乎整个消息循环(但不是全部)都被隐藏到了MFC中,为了能让任何窗口都有机会获得一点消息泵的行为,MFC提供了一个专门的虚函数PreTranslateMessage,如果你有足够的勇气去探究CWinThread中的消息处理机制的话,你会遇到类似如下的代码: // 简化后的 CWinThread
while (GetMessage(...)) {
if (PreTranslateMessage(...)) {
// continue looping
} else {
TranslateMessage(...);
DispatchMessage(...);
}
}
CWinThread::PreTranslateMessage是个虚函数,在应用中,其缺省的实现以相同的名字调用另一个虚函数,CWnd::PreTranslateMessage。因此,如果你需要在消息循环中做些什么的话-如解释加速键-你只要重载PreTranslateMessage即可。实际上,这就是CFrameWnd处理加速键的方法。 BOOL CFrameWnd::PreTranslateMessage(MSG* pMsg)
{
......
if (pMsg->message >= WM_KEYFIRST &&
pMsg->message <= WM_KEYLAST)
{
::TranslateAccelerator(m_hAccelTable,...);
}
}
CFrameWnd 从哪里获得加速键表呢?当你加载框架时,CFrameWnd::LoadFrame用与文档模板相同的ID(如IDR_MAINFRAME)查找加速键表,并将他加载到m_hAccelTable。所有的处理细节在MFC中都是自动的、隐蔽的,你不用去操心-仅对主框架而言,如果是对话框,则是另外一种情况。因为CDialog不是从CFrameWnd派生而来,所以不继承任何有关加速键的内容。
不用担心,我们可以模仿CFrameWnd的工作,很容易为对话框增加加速键的功能。第一步是加载加速键,加载加速键最好的地方是在对话框的OnInitDialog函数中: BOOL CMyDlg::OnInitDialog()
{
CDialog::OnInitDialog();
......
// Load accelerators
m_hAccel = ::LoadAccelerators(AfxGetResourceHandle(),
m_lpszTemplateName);
ASSERT(m_hAccel);
return TRUE;
}
在加速键表中,你可以使用任何ID。这里我使用的是对话框本身的ID,(m_lpszTemplateName既可以是一个串名,也可以是一个MAKEINTRESOURCE使用的整型ID): // 本文例子中的加速键(In DlgKeys.rc )
IDD_MYDIALOG ACCELERATORS DISCARDABLE
BEGIN
VK_RETURN, ID_MY_ENTER, VIRTKEY, NOINVERT
END
一旦你已经加载加速键,剩下的事情是重载PreTranslateMessage函数: BOOL CMyDlg::PreTranslateMessage(MSG* pMsg)
{
if (WM_KEYFIRST <= pMsg->message &&
pMsg->message <= WM_KEYLAST)
{
HACCEL hAccel = m_hAccel;
if (hAccel &&
::TranslateAccelerator(m_hWnd, hAccel, pMsg))
return TRUE;
}
return CDialog::PreTranslateMessage(pMsg);
}
之所以要检查按键类的消息(从WM_ KEYFIRST 到 WM_KEYLAST)是为了提高速度。如果你知道不是一个按键消息,你就不用浪费时间去调用TranslateAccelerator。再说TranslateAccelerator是一个虚拟函数,不用增加一个消息映射入口。仅仅写这个函数就可以了。
二、编程步骤
1、 启动Visual C++6.0,生成一个Win32应用程序,将该程序命名为"DlgKeys";
2、 使用CLASSWIZARD为应用程序添加CdlgWithAccelerators和CmyDlg类;
3、 在程序的资源中添加添加加速键资源,内容如下:ID_MY_ENTER, VK_RETURNVIRTKEY,VIRTKEY;
4、添加代码,编译运行程序。
三、程序代码
/////////////////////////////////////////
#include "stdafx.h"
// Generic dialog-that-uses-accelerators.
class CDlgWithAccelerators : public CDialog {
public:
CDlgWithAccelerators(UINT nIDTemplate, CWnd* pParentWnd = NULL);
CDlgWithAccelerators(LPCTSTR lpszTemplateName, CWnd* pParentWnd = NULL);
~CDlgWithAccelerators();
protected:
HACCEL m_hAccel; // accelerator table
// MFC overrides
virtual BOOL OnInitDialog();
virtual BOOL PreTranslateMessage(MSG* pMsg);
DECLARE_MESSAGE_MAP()
};
///////////////////////////////////////////
// CDlgWithAccelerators is a general-purpose class that adds accelerators to CDialog.
#include "stdafx.h"
#include "dlgaccel.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
BEGIN_MESSAGE_MAP(CDlgWithAccelerators,CDialog)
END_MESSAGE_MAP()
CDlgWithAccelerators::CDlgWithAccelerators(LPCTSTR lpszTemplateName, CWnd* pParentWnd) : CDialog(lpszTemplateName, pParentWnd)
{}
CDlgWithAccelerators::CDlgWithAccelerators(UINT nIDTemplate,
CWnd* pParentWnd) : CDialog(nIDTemplate, pParentWnd)
{}
CDlgWithAccelerators::~CDlgWithAccelerators()
{}
/////////////////////////// Pre-translate message: translate keystrokes using acclerator table.
BOOL CDlgWithAccelerators::PreTranslateMessage(MSG* pMsg)
{
if (WM_KEYFIRST <= pMsg->message && pMsg->message <= WM_KEYLAST) {
HACCEL hAccel = m_hAccel;
if (hAccel && ::TranslateAccelerator(m_hWnd, hAccel, pMsg))
return TRUE;
}
return CDialog::PreTranslateMessage(pMsg);
}
//////////////////// Initialize dialog: load accelerators
BOOL CDlgWithAccelerators::OnInitDialog()
{
BOOL bRet = CDialog::OnInitDialog();
// Load dialog's accelerators
m_hAccel = ::LoadAccelerators(AfxGetResourceHandle(),
m_lpszTemplateName); // use same resource name as dialog
return bRet;
}
/////////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "resource.h"
#include "dlgaccel.h"
#include "TraceWin.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////MFC app
class CMyApp : public CWinApp {
public:
CMyApp();
~CMyApp();
virtual BOOL InitInstance();
DECLARE_MESSAGE_MAP()
};
CMyApp theApp; // THE one-and-only app
//////////// frame window
class CMainFrame : public CFrameWnd {
protected:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
public:
CMainFrame();
~CMainFrame();
};
//////////////////// Typical dialog
class CMyDlg : public CDlgWithAccelerators {
public:
CMyDlg(CWnd* pParent = NULL); // standard constructor
protected:
HICON m_hIcon;
void NextInTabOrder();
// MFC overrides
virtual BOOL OnInitDialog();
afx_msg void OnMyEnter();
afx_msg LRESULT OnGetDefID(WPARAM wp, LPARAM lp);
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyApp, CWinApp)
END_MESSAGE_MAP()
CMyApp::CMyApp()
{
// nothing to do
}
CMyApp::~CMyApp()
{
// nothing to do
}
//////////////////// InitInstance: create dialog as child
BOOL CMyApp::InitInstance()
{
// create frame window and load it
CMainFrame* pFrame = new CMainFrame;
m_pMainWnd = pFrame;
pFrame->LoadFrame(IDR_MAINFRAME, WS_OVERLAPPED, NULL, NULL);
CMyDlg dlg(pFrame); // create dialog and run it
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{}
else if (nResponse == IDCANCEL)
{}
return FALSE; // quit
}
CMainFrame::CMainFrame()
{
// nothing to do
}
CMainFrame::~CMainFrame()
{
// nothing to do
}
///////////// Pre-create window: set WS_EX_TOOLWINDOW style to hide dialog from task bar
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if (CFrameWnd::PreCreateWindow(cs)) {
cs.dwExStyle |= WS_EX_TOOLWINDOW;
return TRUE;
}
return FALSE;
}
BEGIN_MESSAGE_MAP(CMyDlg, CDlgWithAccelerators)
ON_COMMAND(ID_MY_ENTER, OnMyEnter)
// The following is NOT needed since I am using accelerators to map
// ENTER to ID_MY_ENTER. But if all you want to do is ignore the Enter key,
// you can handle DM_GETDEFID as below.
// ON_MESSAGE(DM_GETDEFID, OnGetDefID) // not used
END_MESSAGE_MAP()
CMyDlg::CMyDlg(CWnd* pParent) : CDlgWithAccelerators(IDD_MYDIALOG, pParent)
{}
//////////////////// Initialize dialog:
BOOL CMyDlg::OnInitDialog()
{
CDlgWithAccelerators::OnInitDialog();
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
ASSERT(m_hIcon);
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// use same resource name as dialog to load dialog's accelerators
m_hAccel = ::LoadAccelerators(AfxGetResourceHandle(),m_lpszTemplateName);
ASSERT(m_hAccel);
return TRUE; // return TRUE unless you set the focus to a control
}
//////////////////// This is called to handle ID_MY_ENTER--ie, Enter key.
void CMyDlg::OnMyEnter()
{
TRACE(_T("CMyDlg::OnMyEnter\n"));
NextInTabOrder(); // move to next control
}
//////////////////// Helper function to move focus to the next control.
void CMyDlg::NextInTabOrder()
{
CWnd* pWndNext = GetNextDlgTabItem(GetFocus());
if (pWndNext) {
pWndNext->SetFocus();
}
}
//////////////////
// This function is not used, since its message map entry is commented out.
// If all you want to do is ignore the Enter key (not map it to a command),
// then all you have to do is return zero here. Note that you MUST return
// the special code DC_HASDEFID in the high-order word!!
LRESULT CMyDlg::OnGetDefID(WPARAM wp, LPARAM lp)
{
TRACE(_T("CMyDlg::OnGetDefID\n"));
return MAKELONG(0,DC_HASDEFID);
}
四、小结
综上所述,MFC中为对话框添加加速键功能的方法就是:加载加速键和重载PreTranslateMessage函数。也就是说,如果你决定使用加速键,不用去操心OnGetDefID,而是将没有命令处理器的ID映射到VK_RETURN。本文的例子代码中封装了一个又加速键的新对话框类:CdlgWinAccelerators,它是一个通用类。希望大家喜欢它。最后祝大伙编程愉快。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
摘要 :本文讲述了在指定的编辑框上能响应从键盘输入回车键的一种方法,对进程内消息的解析、动态获取指定资源ID等技术也作了简要描述。
关键字 :Microsoft Visual C++ 6.0、编辑框、回车键、消息、资源
一、引言
在通常的以CEditView为基类的单文档/多文档视图程序中,可以很好的响应键盘输入的回车键,只需比较最近两次的输入的字符,看看最新输入的字符是否内码是13(0x0d,回车键的内码)即可识别出来,而要单独把一个编辑框放入对话框中却根本不响应,这个看似简单的问题在实际应用中还是解决起来比较困难的。尤其是当一个充当表单录入的对话框上有若干个编辑框,这就要求在一个编辑框添完一项表单后用习惯的回车键将该编辑框上的数据读取到内存中去,并自动将光标移动到下一个编辑框中准备填写下一栏表单。无疑这种界面是十分人机友好的,使录入人员不必去执行每填一下表单就去按一下执行读入到缓存功能的按钮的烦琐操作。但上述功能的实现却并不象其演示的功能那样简单,下面本文就对这项技术的实现及附带的其他技术作简要的介绍。
二、不能响应回车键的原因分析
之所以在以CEditView作为基类的程序中可以响应回车键,是由于该程序的视类本身就是一个Edit控件,这就是问题的关键所在。CEditView作为CView的派生类能响应从键盘输入的各种消息,其中有和键盘输入相关的WM_CHAR、WM_KEYDOWN、WM_KEYUP等消息。我们就可以在这些消息的响应函数中灵活地设计程序去捕捉到回车键的输入,并执行响应的操作。
当我们将编辑框作为一个普通的控件放到对话框上时情况就发生了变化。在此我们以CFormView为例,它也是CView的一个派生类,视是一个Form窗体(即对话框),当放有编辑框的窗体有回车键输入时,由于只有编辑框可以接受从键盘输入的字符,所以当键盘按下时统统把消息都发给了编辑框(在Windows下每个窗口、按钮、编辑框都看作一个窗口,都可以接受消息),可以通过ClassWizard在"Object IDs"选中编辑框所对应的ID号,在右边的消息框中可以看出该编辑框并不能响应WM_CHAR等消息,只能用EN_CHANGE事件来做类似的响应。可当我们加入了对该事件的处理函数时,却又将回车键当作控制字符,当输入回车键并不会激发EN_CHANGE事件,也就是说用这种方法仍旧无法捕获回车键的输入。
三、拦截回车键的思路与方法
Windows操作系统下各个窗口、控件归根结底都是通过系统的各种各样的消息来相互协调、相互联系的,而我们所遇到的这个问题换到消息的角度说就是"如何使程序能响应在编辑框上输入的回车键所发出的消息",只要能响应到这个消息,剩下的工作都可以在消息处理函数中完成。所以有必要对Windows系统的消息机制做些了解。
每个Windows应用程序开始执行后,Windows都为该程序创建一个"消息队列(message queue)",用来存放邮寄给该程序可能创建的各种不同窗口的消息。消息队列中消息的结构(MSG)为:
typedef struct tagMSG {
/*msg*/ HWND hwnd;//窗口句柄,标识接收消息的窗口。
UINT message;//消息标识号,如WM_TIMER等。
WPARAM wParam;//消息参数,当为键盘消息时,表示虚拟键码如VK_RETURN等。
LPARAM lParam;//消息参数。
DWORD time;//邮寄消息的时间。
POINT pt;//邮寄消息时的光标位置,用屏幕坐标表示。
}MSG;
在系统下最常用的消息循环是调用GetMessage()函数从消息队列中取出消息,然后调用DespatchMessage() 函数让系统把消息发送给窗口函数,一般情况下其结果是把窗口的所有消息都传送给窗口函数。但特殊情况下可以在GetMessage()函数获得消息而又没发送出去之前,通过TranslateMessage()函数可以中途对消息进行解析,可以对指定的消息进行拦截,拦截后即可以照样发送出去,也可以不继续发送,完成对该消息的拦截,下面代码是该过程的示例:
MSG msg;
while(GetMessage(&msg,NULL,NULL,NULL,NULL)
{
TranslateMessage(&msg);…… //对拦截的消息进行处理
DispathchMessage(&msg);
}
由于按下回车键时把产生的消息加入到消息队列中了,也传给了编辑框,但仅仅是由于编辑框没有能力处理该消息而造成了无法对回车键的响应,所以可以在消息循环里在把消息发送到编辑框之前就对消息进行拦截,并对其进行处理。其效果同编辑框响应回车键是一样的,仅在时序上有所提前而已。上述代码是在SDK(Software Develope Kits)下使用的,在MFC(Microsoft Foundation Class)下早已对其进行了封装,可以通过重载虚函数PreTranslateMessage()对所关心的消息进行解析:
BOOL CTestView::PreTranslateMessage(MSG* pMsg)
{
if (WM_KEYFIRST <= pMsg-> message && pMsg-> message <= WM_KEYLAST)
{
if(pMsg-> wParam==VK_RETURN )
{
UpdateData(TRUE);
AfxMessageBox(m_Text);
}
}
return CFormView::PreTranslateMessage(pMsg);
}
在上面的代码中,首先将pMsg-> message所表示的消息同WM_KEYFIRST 和WM_KEYLAST比较,确定是键盘消息,然后通过消息参数pMsg-> wParam的值来判断是否是回车键(VK_RETURN,虚拟键码可以从SDK相关资料查到)。如是,则可以将已输入到编辑框中的字符读取到m_Text中,并将其显示出来
四、对编辑框的识别
前面已经可以对回车键响应了,可一个表单窗体有若干个编辑框,其各自的处理方式不尽相同,这就有必要对编辑框进行识别、对不同的编辑框做不同的处理。而且当按下回车键时必须保证只有当前有焦点的编辑框能完成对回车键的响应动作,否则也就失去了实际意义。
在Windows下的程序中,所有的资源都是有唯一标号的,使每个资源对象能唯一的区别于其他资源,所以我们可以通过资源ID来对编辑框做出区别,使之完成各自的响应处理。在Microsoft Visual C++ 6.0下可以通过"View"菜单的"ID= Resource Symboles…"查到指定ID的资源标识号的实际数值,如在本例中的两个编辑框IDC_EDIT1和IDC_EDIT2所对应的数值分别为1000和1001,对前面的解析消息的代码做些改动,主要如下所示:
……
if(pMsg-> wParam==VK_RETURN )
{
HWND hWnd=::GetFocus();
int iID=::GetDlgCtrlID(hWnd);
if(iID==1000)//第一个编辑框的标识为1000
{
UpdateData(TRUE);
AfxMessageBox(m_Text1);//显示第一个编辑框的内容
}
if(iID==1001) //第二个编辑框的标识为 1001
{
UpdateData(TRUE);
AfxMessageBox(m_Text2);//显示第二个编辑框的内容
}
}
……
在此通过API函数::GetFocus()(注意前面的"::",标识是全局API函数,而非某个类中的成员函数)取得当前光标所处的(即有焦点的)编辑框的句柄,然后通过API函数::GetDlgCtrlID()根据这个句柄返回此窗口资源的ID 号,该ID号是动态获取的,使之同预先查看好的编辑框的ID作下比较即可区分出是需要哪个编辑框对回车键作出响应。
小结:
本文通过对消息的解析实现了对特定编辑框的回车键的响应,在对消息机制有了基本的了解之后,可以用与本文类似的方法,对代码稍作改动,就可以使其他一些不能响应特殊消息的控件能接收、处理特定的消息。
热门话题 · · · · · · ( 去话题广场 )
- 暑期档电影repo326篇内容 · 5.7万次浏览
- 想做的事,别等“以后”1.0万+篇内容 · 1161.9万次浏览
- 暑期档追剧推荐76篇内容 · 1.8万次浏览
- 不花钱也能拥有的松弛感89篇内容 · 12.9万次浏览
- 后悔没有早点知道的职场道理29篇内容 · 2.7万次浏览
- 身为女性你有哪些想要逃离的瞬间67篇内容 · 35.9万次浏览
- 我的消暑菜单715篇内容 · 56.9万次浏览
- 让人生变开阔的方法1.0万+篇内容 · 705.8万次浏览
http://www.vckbase.com/document/viewdoc/?id=481