在文档窗口创建的时候 ,它缺省总是会新建一个新文档 ,如果是单文档,则直接新建一个文档,如果是多文档,则会弹出文件类型选择对话框。 那么怎么让它不新建文档呢?就这个问题 , 我对文档视图窗口应用程序启动时的文档创建机制 , 稍稍的浅浅挖了一下 , 做了一个详细的分析 , 希望能够对初学者有所帮助 。 在App文件的InitInstance()函数中,有如下几行代码: CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); if (!ProcessShellCommand(cmdInfo)) return FALSE; 这几行代码是程序启动时创建新文档的关键代码 。 1: 我们首先来看看让CCommandLineInfo类是个什么东西:( 部分源代码 ) //in afxwin.h class CCommandLineInfo : public CObject { public: // Sets default values CCommandLineInfo(); BOOL m_bShowSplash; BOOL m_bRunEmbedded; BOOL m_bRunAutomated; enum { FileNew, FileOpen, FilePrint, FilePrintTo, FileDDE, AppRegister, AppUnregister, FileNothing = -1 } m_nShellCommand; // not valid for FileNew CString m_strFileName; . . . ~CCommandLineInfo(); . . . }; 这里要重点注意enum {FileNew, . . . , FileNothing = -1 }m_nShellCommand; 这里联合类型定义的m_nShellCommand 就是外壳程序执行的命令类型 , 如果m_nShellCommand设置为FileNew ,那么程序就会创建新文档。如果想在文档开始时不创建新文档 , 就必须将m_nShellCommand设置为FilleNothing 。下面我们再看看CCommandLineInfo的构造函数 。 //in appcore.cpp CCommandLineInfo::CCommandLineInfo() { m_bShowSplash = TRUE; m_bRunEmbedded = FALSE; m_bRunAutomated = FALSE; m_nShellCommand = FileNew; } 这里很明白的看出 , 构造函数中 , 缺省将 m_nShellCommand设置为 FileNew 。 2:再来看看ParseCommandLine(cmdInfo); 函数 。 void CWinApp::ParseCommandLine(CCommandLineInfo& rCmdInfo) { for (int i = 1; i < __argc; i++) { LPCTSTR pszParam = __targv[i]; BOOL bFlag = FALSE; BOOL bLast = ((i + 1) == __argc); if (pszParam[0] == '-' || pszParam[0] == '/') { // remove flag specifier bFlag = TRUE; ++pszParam; } rCmdInfo.ParseParam(pszParam, bFlag, bLast); } } 可以看出ParseCommandLine主要是对输入的命令行参数做一些分析 , 并调用ParseParam来进行处理 .继续分析 ParseParam函数 , 查看如下源代码: void CCommandLineInfo::ParseParam(const TCHAR* pszParam,BOOL bFlag,BOOL bLast) { if (bFlag) { USES_CONVERSION; ParseParamFlag(T2CA(pszParam)); } else ParseParamNotFlag(pszParam); ParseLast(bLast); } 其它的函数撇开不看 , 我们重点来分析一下ParseParamFlag()和ParseLast()函数 。 void CCommandLineInfo::ParseParamFlag(const char* pszParam) { // OLE command switches are case insensitive, while // shell command switches are case sensitive if (lstrcmpA(pszParam, "pt") == 0) m_nShellCommand = FilePrintTo; else if (lstrcmpA(pszParam, "p") == 0) m_nShellCommand = FilePrint; else if (lstrcmpiA(pszParam, "Unregister") == 0 || lstrcmpiA(pszParam, "Unregserver") == 0) m_nShellCommand = AppUnregister; else if (lstrcmpA(pszParam, "dde") == 0) { AfxOleSetUserCtrl(FALSE); m_nShellCommand = FileDDE; } else if (lstrcmpiA(pszParam, "Embedding") == 0) { AfxOleSetUserCtrl(FALSE); m_bRunEmbedded = TRUE; m_bShowSplash = FALSE; } else if (lstrcmpiA(pszParam, "Automation") == 0) { AfxOleSetUserCtrl(FALSE); m_bRunAutomated = TRUE; m_bShowSplash = FALSE; } } ParseParamFlag判断传过来的字符串 ,判断它的参数类型 , 并根据参数类型做不同的处理 。 void CCommandLineInfo::ParseLast(BOOL bLast) { if (bLast) { if (m_nShellCommand == FileNew && !m_strFileName.IsEmpty()) m_nShellCommand = FileOpen; m_bShowSplash = !m_bRunEmbedded && !m_bRunAutomated; } } ParseLast会判断是否是是FileNew打开新文档 , 如果是打开新文档 , 并且打开的文档名不为空的话, 就假定用户想打开这个文档 , 把命令设置为FileOpen 。 最后 , 我们可以总结一下ParseCommandLine的作用 . ParseCommandLine的作用主要是分析命令行参数,如果没有命令行参数 ,ParseCommandLine()就假定用户想新建一个文档,于是设置一个FileNew命令,如果命令行参数中有一个文件名,ParseCommandLine()就假定用户想打开该文件,于是设置一个FileOpen命令。 3: 最后 , 我们来重点看看外壳命令解析的主角 : ProcessShellCommand ();(部分源代码) BOOL CWinApp::ProcessShellCommand(CCommandLineInfo& rCmdInfo) { BOOL bResult = TRUE; switch (rCmdInfo.m_nShellCommand) { case CCommandLineInfo::FileNew: if (!AfxGetApp()->OnCmdMsg(ID_FILE_NEW, 0, NULL, NULL)) OnFileNew(); if (m_pMainWnd == NULL) bResult = FALSE; break; case CCommandLineInfo::FileOpen: . . . case CCommandLineInfo::FilePrintTo: . . . case CCommandLineInfo::FilePrint: . . . case CCommandLineInfo::FileDDE: . . . case CCommandLineInfo::AppRegister: . . . case CCommandLineInfo::AppUnregister: . . . . . . } } 代码看到这里 , 一切都很明白了 . ProcessShellCommand分析m_nShellCommand ,并根据m_nShellCommand不同的类型值进行不同的处理 。 再来分析下面两行代码: CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); if (!ProcessShellCommand(cmdInfo)) return FALSE; 1: 当CCommandLineInfo cmdInfo进行定义时 , 首先调用构造函数 , 构造函数中m_nShellCommand被设置为FileNew 2: 然后执行ParseCommandLine(cmdInfo);对命令进行分析 。 3: 最后执行ProcessShellCommand (cmdInfo) , ProcessShellCommand ()判断m_nShellCommand为FileNew , 于是调用OnFileNew()创建了一个新的文档 。 这也就是创建新文档的来龙去脉 。 最后, 我们看怎么样解决不想在应用程序启动时的创建新文档的问题: 直接在InitInstance()函数中用如下代码代替原来的几行即可: CCommandLineInfo cmdInfo; cmdInfo.m_nShellCommand = CCommandLineInfo::FileNothing; ParseCommandLine(cmdInfo); if (!ProcessShellCommand(cmdInfo)) return FALSE; 在多文档情况下,此时再单击新建,则会弹出文件类型选择对话框。我们经常需要变更的就是这个默认的"新建文件"操作, 毕竟那个mfc默认的"文档类型选择"对话框无论是从外观上还是功能上都很难满足我们的需要。
默认"文档类型选择"对话框的定义为: class CNewTypeDlg : public CDialog 可以在VC6.0安装目录的vc98/mfc/src/docmgr.cpp文件中找到. 下面将会仿照这个对话框来设计自定义的"文档类型选择"对话框, 并可以添加自己需要的各种功能。 首先观察mfc"新建文件"操作的过程源码. 对于ID_FILE_NEW消息, 利用VC6.0 IDE的搜索功能,找到: /VC98/MFC/SRC/APPDLG.CPP(25):void CWinApp::OnFileNew()如下: void CWinApp::OnFileNew() { if (m_pDocManager != NULL) m_pDocManager->OnFileNew(); } m_pDocManager是CWinApp的成员. 类型为CDocManager* 然后继续找到: /VC98/MFC/SRC/DOCMGR.CPP(802):void CDocManager::OnFileNew()如下: void CDocManager::OnFileNew() { if (m_templateList.IsEmpty()) { TRACE0("Error: no document templates registered with CWinApp./n"); AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC); return; }
CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetHead(); if (m_templateList.GetCount() > 1) { // more than one document template to choose from // bring up dialog prompting user CNewTypeDlg dlg(&m_templateList); int nID = dlg.DoModal(); if (nID == IDOK) pTemplate = dlg.m_pSelectedTemplate; else return; // none - cancel operation }
ASSERT(pTemplate != NULL); ASSERT_KINDOF(CDocTemplate, pTemplate);
pTemplate->OpenDocumentFile(NULL); // if returns NULL, the user has already been alerted } 在这个函数中, 就可以发现CNewTypeDlg的踪迹。
工程的名字假定为My, 则我们有CMyApp派生类。 我们所需要做的工作是: 1, 在菜单中添加一个新的菜单项例如ID_FILE_NEW_1, 并添加消息响应函数例如OnFileNew1(); 或者直接为原来的ID_FILE_NEW添加消息响应函数OnFileNew. 这里采用后一种。 2, 添加CMyApp::OnFileNew()后, 不要忘记删除My.cpp中MESSAGE_MAP里的这一项: ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew) 3, 在CMyApp::OnFileNew()中, 把上面的CWinApp::OnFileNew()和CDocManager::OnFileNew()的代码拷贝过去, 并加以改造如下: void CMyApp::OnFileNew() { // from CWinApp::OnFileNew(): if (m_pDocManager == NULL) { return; }
// from CDocManager::OnFileNew(): // 由于无法访问CDocManager的保护数据成员m_templateList // 故要使用CDocManager::GetFirstDocTemplatePosition() // 和CDocManager::GetNextDocTemplate()接口来获得所有文档模板类指针 // 并将其添加到自定义的列表m_DocTplList中 POSITION pos = m_pDocManager->GetFirstDocTemplatePosition(); if (pos == NULL) { TRACE0("Error: no document templates registered with CWinApp./n"); AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC); return; }
// 遍历获得各个文档模板类指针 // 注意, 在CMyApp的定义中添加成员CPtrList m_DocTplList用以放置这些指针 CDocTemplate* pTemplate = NULL;
while (pos != NULL) { pTemplate = m_pDocManager->GetNextDocTemplate(pos); ASSERT(pTemplate != NULL); this->m_DocTplList.AddTail(pTemplate); }
// 如果文档类型超过1个则使用自定义的"文档类型选择"对话框CDlgChooseClass // 在资源编辑器中添加一个对话框, 并创建对话框类CDlgChooseClass // 在对话框中放置一个ListBox控件, 用来容纳多个文档类型 // 稍后有具体说明 if (m_DocTplList.GetCount() > 1) { CDlgChooseClass dlg(&m_DocTplList); // 输入文档模板类指针的列表 int nID = dlg.DoModal(); // 对话框CDlgChooseClass完成选择文档类型 // 并且可以有其他自定义的操作 if (nID == IDOK) { pTemplate = dlg.m_pSelectedTemplate; }
else { m_DocTplList.RemoveAll(); // 注意: 清空自定义的列表 return; // none - cancel operation } }
m_DocTplList.RemoveAll(); // 注意: 清空自定义的列表
// 检查返回的值 ASSERT(pTemplate != NULL); ASSERT_KINDOF(CDocTemplate, pTemplate);
// 选定的文档模板类: 创建新文档 pTemplate->OpenDocumentFile(NULL); // if returns NULL, the user has already been alerted }
接下来是设计CDlgChooseClass, 仿照CNewTypeDlg的定义进行(vc98/mfc/src/docmgr.cpp). class CDlgChooseClass : public CDialog { // Construction public: // 修改默认构造函数, 增加一个参数CPtrList* pList CDlgChooseClass(CPtrList* pList, CWnd* pParent = NULL);
// Dialog Data
public: CDocTemplate* m_pSelectedTemplate; // 选定的文档模板类 //{{AFX_DATA(CDlgChooseClass) enum { IDD = IDD_DLG_CHOOSE_CLASS }; // NOTE: the ClassWizard will add data members here //}}AFX_DATA
// Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDlgChooseClass) protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL
// Implementation protected: CPtrList* m_pList; // 文档模板类指针列表
// Generated message map functions //{{AFX_MSG(CDlgChooseClass) virtual BOOL OnInitDialog(); virtual void OnOK(); //}}AFX_MSG DECLARE_MESSAGE_MAP() };
实现OnInitDialog()和OnOk()如下:
基本上是拷贝CNewTypeDlg::OnInitDialog()和CNewTypeDlg::OnOk()的代码, 并略做修改 BOOL CDlgChooseClass::OnInitDialog() { // IDC_DOC_TPL_LIST是ListBox的ID CListBox* pListBox = (CListBox*)GetDlgItem(IDC_DOC_TPL_LIST); ASSERT(pListBox != NULL);
// fill with document templates in list pListBox->ResetContent();
POSITION pos = m_pList->GetHeadPosition(); // add all the CDocTemplates in the list by name while (pos != NULL) { CDocTemplate* pTemplate = (CDocTemplate*)m_pList->GetNext(pos); ASSERT_KINDOF(CDocTemplate, pTemplate);
// 这里是添加fileNewName来代表各个文档模板。也可以设计其他的显示方式 CString strTypeName; if (pTemplate->GetDocString(strTypeName, CDocTemplate::fileNewName) && !strTypeName.IsEmpty()) { // add it to the listbox int nIndex = pListBox->AddString(strTypeName); if (nIndex == -1) { EndDialog(-1); return FALSE; } pListBox->SetItemDataPtr(nIndex, pTemplate); }
}
int nTemplates = pListBox->GetCount(); if (nTemplates == 0) { TRACE0("Error: no document templates to select from!/n"); EndDialog(-1); // abort } else if (nTemplates == 1) { // get the first/only item m_pSelectedTemplate = (CDocTemplate*)pListBox->GetItemDataPtr(0); ASSERT_VALID(m_pSelectedTemplate); ASSERT_KINDOF(CDocTemplate, m_pSelectedTemplate); EndDialog(IDOK); // done } else { // set selection to the first one (NOT SORTED) pListBox->SetCurSel(0); }
return CDialog::OnInitDialog(); }
void CDlgChooseClass::OnOK()
{ // TODO: Add extra validation here CListBox* pListBox = (CListBox*)GetDlgItem(IDC_DOC_TPL_LIST); ASSERT(pListBox != NULL); // if listbox has selection, set the selected template int nIndex; if ((nIndex = pListBox->GetCurSel()) == -1) { // no selection m_pSelectedTemplate = NULL; } else { m_pSelectedTemplate = (CDocTemplate*)pListBox->GetItemDataPtr(nIndex); ASSERT_VALID(m_pSelectedTemplate); ASSERT_KINDOF(CDocTemplate, m_pSelectedTemplate); }
CDialog::OnOK(); }
当然, 构造函数也要注意修改: CDlgChooseClass::CDlgChooseClass(CPtrList* pList, CWnd* pParent /* = NULL */) : CDialog(CDlgChooseClass::IDD, pParent) { //{{AFX_DATA_INIT(CDlgChooseClass) // NOTE: the ClassWizard will add member initialization here //}}AFX_DATA_INIT
m_pList = pList; // 文档模板类指针列表 m_pSelectedTemplate = NULL; // 最后选择的文档模板类
}
到这里就完成了. 进入程序, 选择"文件"-"新建", 或者直接Ctrl+N, 出来的已经是我们自定义的CDlgChooseClass对话框了, 且基本功能和原来默认的完全一样(选择文档类型). 要增加新的功能, 或者改善外观? 这就是对话框设计的问题了。