CStatusBar 禁用自动状态提示
2024-07-04 20:17:44

今天在改一个MFC程序时,在对状态栏设置文本这里耗费了两小时时间,症状是对第一栏设置文本能成功,但是程序不显示。代码大概是这样:

1
2
3
4
5
6
7
static UINT arr[] = {
ID_SEPARATOR,
ID_SEPARATOR,
ID_SEPARATOR
};
SetIndicators(arr, _countof(arr));
SetPaneText(0, _T("Hello"));

原来问题出在指示器上,一切源于我没有理解ID_SEPARATOR的含义。
它的值等于零,用于占位,MFC会用第一个ID_SEPARATOR窗格显示命令提示信息(鼠标停留在菜单、工具栏时状态栏给出的提示)

MFC在窗口初始化时会用ID为AFX_IDS_IDLEMESSAGE的字符串作为窗格初始内容,正好由于我的程序中未定义AFX_IDS_IDLEMESSAGE,导致MFC将空文本覆盖了我设置的文本。

解决方案

只要不用ID_SEPARATOR就行了,但我不想为了不用它就特地去定义一个字符串。
可以将ID指定为一个预定义的字符串,比如AFX_IDS_IDLEMESSAGE,但程序要在资源中给出它的定义,否则SetIndicators方法将会返回FALSE

1
2
3
4
5
6
7
static UINT arr[] = {
AFX_IDS_IDLEMESSAGE,
AFX_IDS_IDLEMESSAGE,
AFX_IDS_IDLEMESSAGE
};
SetIndicators(arr, _countof(arr));
SetPaneText(0, _T("Hello"));

这样就可以愉快地使用SetPaneText了。

另类的解决方案

MFC强制将命令提示写到状态栏这个特性,看看源码是怎样实现的
winfrm.cpp文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
LRESULT CFrameWnd::OnSetMessageString(WPARAM wParam, LPARAM lParam)
{
UINT nIDLast = m_nIDLastMessage;
m_nFlags &= ~WF_NOPOPMSG;

CWnd* pMessageBar = GetMessageBar();
if (pMessageBar != NULL)
{
LPCTSTR lpsz = NULL;
CString strMessage;

// set the message bar text
if (lParam != 0)
{
ASSERT(wParam == 0); // can't have both an ID and a string
lpsz = (LPCTSTR)lParam; // set an explicit string
}
else if (wParam != 0)
{
// map SC_CLOSE to PREVIEW_CLOSE when in print preview mode
if (wParam == AFX_IDS_SCCLOSE && m_lpfnCloseProc != NULL)
wParam = AFX_IDS_PREVIEW_CLOSE;

// get message associated with the ID indicated by wParam
//NT64: Assume IDs are still 32-bit
GetMessageString((UINT)wParam, strMessage);
lpsz = strMessage;
}
pMessageBar->SetWindowText(lpsz);

// update owner of the bar in terms of last message selected
CFrameWnd* pFrameWnd = pMessageBar->GetParentFrame();
if (pFrameWnd != NULL)
{
pFrameWnd->m_nIDLastMessage = (UINT)wParam;
pFrameWnd->m_nIDTracking = (UINT)wParam;
}
}

m_nIDLastMessage = (UINT)wParam; // new ID (or 0)
m_nIDTracking = (UINT)wParam; // so F1 on toolbar buttons work
return nIDLast;
}

重要的就在这一句

1
pMessageBar->SetWindowText(lpsz);

实质上就是修改状态栏控件的标题,所以我们要看看CStatusBar是如何处理的,在barstat.cpp文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BEGIN_MESSAGE_MAP(CStatusBar, CControlBar)
...
ON_MESSAGE(WM_SETTEXT, &CStatusBar::OnSetText)
...
END_MESSAGE_MAP()

...

LRESULT CStatusBar::OnSetText(WPARAM, LPARAM lParam)
{
ASSERT_VALID(this);
ASSERT(::IsWindow(m_hWnd));

int nIndex = CommandToIndex(0);
if (nIndex < 0)
return -1;
return SetPaneText(nIndex, (LPCTSTR)lParam) ? 0 : -1;
}

很明朗了,通过调用 CommandToIndex 获取ID为ID_SEPARATOR的窗格索引,然后设置窗格内容。

回到最初的问题,我不想将指示器指向一个字符串ID,只想用ID_SEPARATOR占位,但又能用SetPaneText去设置面板文本怎么办?解决办法就是拦截CStatusBar控件的 WM_SETTEXT 消息,屏蔽掉这个特性。

1
2
3
4
5
6
7
8
9
BOOL CStatusBarEx::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
if (message == WM_SETTEXT)
{
*pResult = TRUE;
return TRUE;
}
return __super::OnWndMsg(message, wParam, lParam, pResult);
}

不过这个方法要求子类化CStatusBar或者继承它来拦截消息。其实自动提示这个功能的源头在CFrameWnd::OnSetMessageString,而它是对MFC内部消息 WM_SETMESSAGESTRING 的响应,所以我们也可以在 Frame 中拦截WM_SETMESSAGESTRING消息。

1
2
3
4
5
6
7
8
9
#include <afxpriv.h>

BOOL MainFrame::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
if (message == WM_SETMESSAGESTRING)
return __super::OnWndMsg(message, 0, 0, pResult);

return __super::OnWndMsg(message, wParam, lParam, pResult);
}

这个拦截操作将wParamlParam参数置为0,这将导致传递给SetWindowText的参数是空,就能达到屏蔽状态栏自动提示的特性。