MFC学习:设置窗口标题
2024-03-17 17:33:41

默认情况下,MFC的主窗口标题由文档标题和Frame标题组成,格式为:file - frame

设置标题

设置窗口标题用 CWnd::SetWindowText 方法。

设置文档标题

新建一个文档时,MFC 会使用字符串资源AFX_IDS_UNTITLED作为文档的默认标题,接着会触发 CDocument::OnNewDocument 方法,我们可以在文件新建成功后使用 CDocument::SetTitle 方法来设置标题。

1
2
3
4
5
6
7
8
BOOL CMFCApplication2Doc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;

this->SetTitle(_T("新文件"));
return TRUE;
}

如果是打开文档就不能在 CDocument::OnOpenDocument 中处理,因为 MFC 在打开文档后会使用文件名作为标题

1
2
3
4
5
...
TCHAR szTitle[_MAX_FNAME];
if (AfxGetFileTitle(szFullPath, szTitle, _MAX_FNAME) == 0)
SetTitle(szTitle);
...

最早的修改时机是在 CDocument::SetPathName 中,我们可以重载它

1
2
3
4
5
void SetPathName(LPCTSTR lpszPathName, BOOL bAddToMRU) override
{
CDocument::SetPathName(lpszPathName, bAddToMRU);
this->SetTitle(_T("new title"));
}

但是更建议在 CDocument::OnDocumentEvent 中处理

1
2
3
4
5
6
void OnDocumentEvent(DocumentEvent deEvent) override
{
if (deEvent == onAfterNewDocument || deEvent == onAfterOpenDocument) {
this->SetTitle(_T("new title"));
}
}

固定窗口标题

我们有时候希望应用程序标题是固定的,不会随着文档变化,可以通过以下几种方式来做。

方式一

因为文档在设置标题后会触发 CFrameWndEx::OnUpdateFrameTitle 方法,而这个方法会改变窗口标题,所以我们覆盖它即可避免被影响。
CFrameWndEx::OnCreate 中设置固定标题

1
this->SetWindowText(_T("title"));

重载 CFrameWndEx::OnUpdateFrameTitle 方法,不调用父类操作。

1
2
3
4
void CMainFrame::OnUpdateFrameTitle(BOOL bAddToTitle)
{
// Ignore
}

方式二

跟进OnUpdateFrameTitle后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CFrameWnd::OnUpdateFrameTitle(BOOL bAddToTitle)
{
if ((GetStyle() & FWS_ADDTOTITLE) == 0)
return; // leave it alone!

// allow hook to set the title (used for OLE support)
if (m_pNotifyHook != NULL && m_pNotifyHook->OnUpdateFrameTitle())
return;

CDocument* pDocument = GetActiveDocument();
if (bAddToTitle && pDocument != NULL)
UpdateFrameTitleForDocument(pDocument->GetTitle());
else
UpdateFrameTitleForDocument(NULL);
}

这里发现了一个新东西FWS_ADDTOTITLE,如果窗口样式不包含它的话就退出函数了,所以我们也可以在创建Frame时去除这个Flag

1
2
3
4
5
6
7
8
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if (!CFrameWndEx::PreCreateWindow(cs))
return FALSE;

cs.style &= ~FWS_ADDTOTITLE;
return TRUE;
}

所以这个方式和方式一是一样的,本质上就是忽略OnUpdateFrameTitle的默认行为。

方式三

继续跟进源码,创建窗口的地方

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
44
45
46
47
48
49
BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
LPCTSTR lpszWindowName, DWORD dwStyle,
int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
{
ASSERT(lpszClassName == NULL || AfxIsValidString(lpszClassName) ||
AfxIsValidAtom(lpszClassName));
ENSURE_ARG(lpszWindowName == NULL || AfxIsValidString(lpszWindowName));

// allow modification of several common create parameters
CREATESTRUCT cs;
cs.dwExStyle = dwExStyle;
cs.lpszClass = lpszClassName;
cs.lpszName = lpszWindowName;
cs.style = dwStyle;
cs.x = x;
cs.y = y;
cs.cx = nWidth;
cs.cy = nHeight;
cs.hwndParent = hWndParent;
cs.hMenu = nIDorHMenu;
cs.hInstance = AfxGetInstanceHandle();
cs.lpCreateParams = lpParam;

if (!PreCreateWindow(cs)) {
PostNcDestroy();
return FALSE;
}

AfxHookWindowCreate(this);
HWND hWnd = CreateWindowEx(cs.dwExStyle, cs.lpszClass,
cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);

#ifdef _DEBUG
if (hWnd == NULL) {
TRACE(traceAppMsg, 0, "Warning: Window creation failed: GetLastError returns 0x%8.8X\n",
GetLastError());
}
#endif

if (!AfxUnhookWindowCreate())
PostNcDestroy(); // cleanup if CreateWindowEx fails too soon

if (hWnd == NULL)
return FALSE;
ASSERT(hWnd == m_hWnd); // should have been set in send msg hook
return TRUE;
}

在调用CreateWindowEx创建窗口之前调用过了一次PreCreateWindow,然后用cs.lpszName作为窗口标题,那么就可以在PreCreateWindow中设置固定标题

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if (!CFrameWnd::PreCreateWindow(cs))
return FALSE;

// 在整个创建窗口过程中,PreCreateWindow 会被调用两次。仅在第二次实际创建窗口前再操作
if (cs.hInstance) {
cs.style &= ~FWS_ADDTOTITLE; // 禁止被文档改变标题
cs.lpszName = _T("Hello");
}

return TRUE;
}

比前两种方式简单直观,推荐使用这种方式。

Frame 默认标题从哪来?

如果我们不做任何设置,Frame 始终会有一个默认标题,这个标题字符串从哪来?还是看源码

1
2
3
4
5
6
7
8
9
BOOL CFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
CWnd* pParentWnd, CCreateContext* pContext)
{
...
CString strFullString;
if (strFullString.LoadString(nIDResource))
AfxExtractSubString(m_strTitle, strFullString, 0); // first sub-string
...
}

原来在创建 MFC 工程时,资源文件中生成了它

1
2
3
4
STRINGTABLE
BEGIN
IDR_MAINFRAME "MFCApplication2\n\nMFCApplication2\n\n\nMFCApplication2.Document\nMFCApplication2.Document"
END

这是一个用\n分隔的字符串列表,第一个子串就是默认标题。
关于其他子串的解释可以查看 CDocTemplate::GetDocString 的解释。