Sunday, May 30, 2004

 

Some notes on Doc/View

1.
改变MFC默认文档操作方式

经过分析MFC源代码可知。其默认菜单中的"新建""打开"消息分别对应CwinApp::OnFileNew()与CwinApp::OnFileOpen(),而这两个函数又直接调用CDocManager::OnFileNew与CDocManager::OnFileOpen。CDocManager::OnFileOpen的工作过程如下:

1. 调用CDocManager:oPromptFileName,而DoPromptFileName的作用是显示文件打开对话框获得文件名。

2. 调用CWinApp::OpenDocumentFile(LPCTSTR lpszfileName)。

如果我们要改变文档打开方式,只需在CWinApp的继承类中重载DoPromptFileName与OpenDocumentFile即可。例如要改变默认的文件打开对话框。只需自建一个文件打开对话框,在重载的DoPromptFileName中调用他,获得一个文件名。

下面分析CWinApp::OpenDocumentFile。OpenDocumentFile的工作过程是:

1. 由参数lpszFileName的后缀判断用哪个文档模板。

2. 对该文档模板的的文档,视图,框架窗口作出调整。

3. 调用CDocTemplate::OpenDocumentFile

CDocTemplate::OpenDocumentFile是纯虚函数,由其派生类CsingleDocTemplate及CMultiDocTemplate实现,以CSingleDocTemplate::OpenDocumentFile举例说明之。

1. 判断有无现存文档,有则判断其是否已保存。

2. 新建框架窗口。

3. 由文件名路径判断该文件是否存在,存在则调用自定义的CMyDoc::OnOpenDocument,否则调用CMyDoc::OnNewDocument。

从以上分析我们可以对MFC默认的文档操作方式做多种改变,如改变文件打开保存方式,改变文件打开保存对话框等。

以上所示的各种源代码都可以从VC安装目录下的MFC\include及mfc\src子目录下得到。CWinApp类及CDocManager类CDocTemplate类及CsingleDocManager,CDocument类的头文件是afxwin.h,CWinApp类的源代码在AppCore.cpp中,CDocManager源代码在Docmgr.cpp中,CDocTemplate类及CSingleDocManager类的源代码分别位于doctempl.cpp与docsingl.cpp中。

具体应用参见

Changing default file open/save dialogs
Jorge Lodos Vigil August 7, 1998
http://www.codeguru.com/Cpp/W-D/dislog/commondialogs/article.php/c1967/

而下面的方法对SDI也可,不过不推荐

CDocument::DoSave revealed
By Nishant S

http://www.codeproject.com/docview/dosaverevealed.asp

Explains how you can suppress the File-Save-As dialog in a Doc/View app, how to save files to multiple formats, and how DoSave is implemented.

2.
关于"建立空文档失败"的问题的分析

作者:checkyvc6
出处:不详

许多新手在遇到此类问题时总是措手无策,如果谁有耐心就看看我写的下面这片文章吧。

这类问题的出现主要在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:
if (!OpenDocumentFile(rCmdInfo.m_strFileName))
bResult = FALSE;
break;
通过上面的内容我们可以看出:如果没有对ID_FILE_NEW做映射的话出现问题就在OnFileNew();
CWinApp对OnFileNew的默认实现是调用m_pDocManager->OnFileNew();

我们继续解析CDocManager,它究竟干了些什么?(首先说明一下CDocManager它主要的功能是帮助CWinApp是管理文档模板链表和注册文件类型.)

//如果模板列表为空的话
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);

通过上面的代码我们可以看出,CWinApp的OnFileNew和OnFileOpen分别调用CDocManager的虚拟函数OnFileNew和OnFileOpen。而在CDocManager里面。通过模板链表选择不同的模板来调用文档模板的OpenDocumentFile();
如果传入参数NULL表示新建文件。

下面我们来看看CDocTemplate::OpenDocumentFile()它是一个最关键的函数。因为他是虚拟函数,我们考虑CSingleDocTemplate::OpenDocumentFile的情况。
这个函数里面有一段代码:
其中:AFX_IDP_FAILED_TO_CREATE_DOC 就是字符“建立空文档失败”的资源id
// create a new document
pDocument = CreateNewDocument();
ASSERT(pFrame == NULL); // will be created below
bCreated = TRUE;
if (pDocument == NULL)
{
AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC);
return NULL;
}
ASSERT(pDocument == m_pOnlyDoc);
if (pFrame == NULL)
{
ASSERT(bCreated);

// create frame - set as main document frame
BOOL bAutoDelete = pDocument->m_bAutoDelete;
pDocument->m_bAutoDelete = FALSE;
// don't destroy if something goes wrong
pFrame = CreateNewFrame(pDocument, NULL);
pDocument->m_bAutoDelete = bAutoDelete;
if (pFrame == NULL)
{
AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC);
delete pDocument; // explicit delete on error
return NULL;
}

通过观察上面的代码我们很容易的看出 有两个可能出错的原因:1 CreateNewDocument返回为NULL; 2 CreateNewFrame 返回为空。

先看 CreateNewDocument() 一般来说这个函数很少失败。不过在调试时也不能掉以轻心。
再看看CreateNewFrame() 里面有一个函数LoadFrame是造成这种“建立新文档失败”错误的源泉所在。只要它返回False就会弹出这样的提示。
我们再来看看LoadFrame() 里面调用CFrameWnd::Create()来创建窗口,创建窗口失败返回Fasle。这样问题就变的比较简单了。

看看Create和CreateEx函数的动作就知道怎么回事了。
****************************************************************
1 如果找不到菜单资源 返回False 同时也弹出“建立空文档失败”
HINSTANCE hInst = AfxFindResourceHandle(lpszMenuName, RT_MENU);
if ((hMenu = ::LoadMenu(hInst, lpszMenuName)) == NULL)
{
TRACE0("Warning: failed to load menu for CFrameWnd.\n");
PostNcDestroy(); // perhaps delete the C++ object
return FALSE;
}
2 重载了PreCreateWindow而且返回False也会导致弹出“建立空文档失败”
3 在OnCreate 里面返回-1 也会导致弹出“建立空文档失败”。
******************************************************************

以上就是我分析的 出现这样“建立空文档失败”问题的大致原因。也许还有其他的原因。我这里就不一一列举了。

3.
UpdateAllViews的用法

作者:danny ye
出处:http://www.vchelp.net/cndevforum/subject_view.asp?subject_id=1025&forum_id=22

首先考虑几点:
首先检查GetDocument是否指向你的Doc(看看是不是CMyDoc* Getdocument()),而不是CDocument* Getxxxxx().
其次在调试状态检查你的Doc是否与你所有的View关联。
你可以用pDoc->GetFirstViewPosition和pDoc->GetNextView遍历所有的视,检查一下视列表中是否包含你要处理的视。一般是通过文档模板和
切分窗口中用CreateView加入到Doc的视列表中。SDI的多视一般是用
切分窗口或CTabCtrl方法实现的,你看看是否正确,没有就用pDoc->AddView做或者干脆用AddDocTemplate做成单文档多模板的方式。

其次this和NULL的差别在于NULL是更新所有视而this是除自己(发送UpdateAllView的视)外其余的都更新,这样系统认为是你自己(View)已经更新过了,你的问题跟这没关系,问题在于其他视没有和你的Doc挂钩.因为UpdateAllView中就是通过遍历视列表调用所有视的OnUpdate,如果视列表中所有的View都在,那一定是OnUpdate处直接返回了,而没有缺省调用父类的OnUpdate.

最后总结:
if (thisView->OnUpdate根本没执行)
{
//看原代码知道pDoc->UpdateAllView中是根据的视列表中包含的视来执行相应的OnUpdate
推断:m_pViewClass中没有包含你的View.
解决:1.用new构造View后,用pDoc->AddView加入;
本方法在CTabCtrl管理的多视时常用;
2。在框架的OnCreateClient的地方(用切分窗口时一样), 会用到CreateView,
CCreateContext是OnCreateClient的参数头来传递,
你可以不必关心细节(如CCreateContext中的m_pCurrentDoc),因为框架已经与文档关联了(在AddDocTemplate时,)MFC知道如何传递正确的CCreateContext,View就是在这种情况下
加入Doc的View列表中;
3。用AddDocTemplate(即多模板的方法),太难看,我不喜欢,不推荐。
4。Camel说的方法,自己处理CCreateContext来创建与文档关联的View(不仅仅是构造一个View). 你的View就可以加入到m_pViewClass中.好处是可在你想创建视的地方,不必是OnCreateClient等有CCreateContext参数头的地方,即可以在需要的时候动态创建,唯一的风险就是m_pCurrentDoc在MFC升级后会不会变(很多书对类似情况都这么说,不推荐直接访问成员变量,而用成员函数访问)。
}
else
{
OnUpdate里没有调用父类的OnUpdate或者没有Invalidate,InvalidateRect之类的触发View重画的函数。
}

4.
直接处理MRU
http://www.codeproject.com/docview/most_recent_used.asp

改写以前的程序不得已用过一次,还行。不过,同时注意Eat相关的消息
By Ramil C. Matira
From http://www.codeproject.com/docview/most_recent_used.asp#xx993102xx

To enable action on MRU you must add the following code. The ID_FILE_MRU_FILE1..ID_FILE_MRU_FILEx are actually the menu ids of the MRU's:

On MainFrm.cpp

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
ON_WM_CREATE()
ON_COMMAND_RANGE(ID_FILE_MRU_FILE1, ID_FILE_MRU_FILE4, OnFileMruFile)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
...

void CMainFrame::OnFileMruFile(UINT nID)
{
int nMRUIndex = -1;
switch (nID) {
case ID_FILE_MRU_FILE1:
nMRUIndex = 0;
break;
case ID_FILE_MRU_FILE2:
nMRUIndex = 1;
break;
case ID_FILE_MRU_FILE3:
nMRUIndex = 2;
break;
case ID_FILE_MRU_FILE4:
nMRUIndex = 3;
break;
}

((CCMRUTestApp*)AfxGetApp())->OpenRecentFileList(nMRUIndex);

}

On CMRUTest

void CCMRUTestApp::OpenRecentFileList(int nIndex)
{
if (m_pRecentFileList != NULL) {
CString strFilename;

if (m_pRecentFileList->GetDisplayName(strFilename,
nIndex,
_T("."),
1))
AfxMessageBox(strFilename);
}
}
You must add code to determine the current directory and replece the _T(".") with its value

By Adam Solesby August 6, 1998
From http://www.codeguru.com/Cpp/W-D/doc_view/mrumostrecentusedfilelist/article.php/c3293/


This is very simple, but I had never seen it anywhere else until I had to do it myself. It should save everyone some digging through the documentation.

To make your program re-open the most recently used file, simply add the following code to your App's InitInstance() between the calls to ParseCommandLine() and ProcessShellCommand().


// Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);

// ====================== Begin Inserted code ==============================
// If a file is not specified on the command line, open the last file
if ( ! cmdInfo.m_strFileName.GetLength() )
{
CString strFileName;
if (m_pRecentFileList->GetDisplayName(strFileName, 0, "", 0, true))
{
cmdInfo.m_strFileName = strFileName;
cmdInfo.m_nShellCommand =
CCommandLineInfo::FileOpen;
}
}
// ====================== End Inserted code ==============================


// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;


(continued)




Nico van Ravenstein adds:

There is a problem with opening the most recent file when the filepath is too long. The directory gets abbreviated by the function AbbreviateName like C:\some path\...\file.txt. Thats nice for a windows title or in the last used list but you can't open a file with it.

So a better way is this:

// Add this to your applications InitInstance function
//
// If a file is not specified on the command line, open the last file
if (!cmdInfo.m_strFileName.GetLength())
{
if (m_pRecentFileList->m_nSize > 0 &&
!m_pRecentFileList->m_arrNames[0].IsEmpty())
{
cmdInfo.m_strFileName = m_pRecentFileList->m_arrNames[0];
cmdInfo.m_nShellCommand = CCommandLineInfo::FileOpen;
}
}

5.
How to switch multi-views

5.1
怎样在一个Pane中显示多种View?
2000-01-30 00:00:00· 祁文文·CPCW

---- 在MS Windows 中,一个窗口可以分割成若干个子窗口,每一个子窗口称作一个窗片(pane),每个窗片可以独立控制,这给界面设计提供了很大的方便。

---- 利用VC 可以很方便地实现分割窗口。分割的方法有两种:动态和静态。动态分割时可以根据用户的需要分割成数目不同的窗片,但所有窗片的属性和父窗口都是一样的;而静态分割的窗片的数目在程序中指定,运行时是固定的,但每个窗片可以有各自不同类型的视(View),因此其使用范围更为广泛。本文所讨论的问题仅限于静态分割。

---- 窗片中视的类型大多是在主窗口的创建过程中指定的。这也就意味着,一个窗片虽然可以显示任意类型的视,但是这种类型一旦确定,在程序运行过程中就难以改变。

---- 一、我要的是这样的!

---- 但是我们有时确实需要改变一个窗片所显示的视的类型,也就是说,需要让一个窗片显示多种类型的视。例如一个窗口被分割成两部分,一边是命令窗口,另一边是工作窗口,根据命令窗口中发出的不同命令,需要变换不同的工作类型,这就需要工作窗口中能够显示多种类型的视窗,那么,如何做到这一点呢?

---- 二、你可以这样做!

---- 从图1 中可以看到,本程序共有三个视类,分别是:

---- ? 命令视类CCmdView:用来控制右边窗片中不同视的显示;

---- ? 选项按钮视类CRdiView:显示在右窗片中的选项视类;

---- ? 检查按钮视类CChkView:显示在右窗片中的检查视类。

---- 这三个视类都是CFormView 的子类。

---- 下面我们来看如何在右窗片内进行两类视间的切换。实际上,由视A 切换到视B 的原理很简单,那就是:

---- 1. 从窗片中删除视A;

---- 2. 往窗片中添加视B。

---- 步骤1 的实现非常简单,仅用一条语句即可:

---- m_wndSplitter.DeleteView(0, 1);

---- 但它是必不可少的,因为你不能让一个窗片同时包含两个视。我本来希望往一个窗片中添加新的视时,VC 会自动将原来的视删掉,可是它不干。

---- 我们来看如何实现步骤2,当一个窗片是空的时候,怎样往里面添加一个视呢?其实这样的功能在程序里我们已经用过了,看下面的语句:

BOOL CMainFrame::OnCreateClient
(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
……
if (!m_wndSplitter.CreateView(0, 0,
pContext->m_pNe ewClass,
size,
pContext))
……
}
---- 是的,用的就是CSplitterWnd::CreateView(),要注意的是它共有五个参数,其中前两个用来指定分割窗口的窗片,第三个用来指定视的类型,第四个指定视的大小。最后的一个我们暂时用不上,用空值NULL 就可以了。

---- 这样我们就可以编写视切换的代码了。因为视切换要操纵m_wndSplitter,而它是主窗口的成员,因此切换过程最好设计为主窗口的成员函数。但是切换命令是CCmdView 接受的,因而可以让CCmdView 接受到视更改消息后,将消息传给主窗口,由主窗口完成视更改。具体的代码是这样的:

---- 命令视类中的消息映射:

BEGIN_MESSAGE_MAP(CCmdView, CFormView)
……
ON_BN_CLICKED(IDC_CHECK, OnSwitchToCheckView)
ON_BN_CLICKED(IDC_RADIO, OnSwitchToRadioView)
……
END_MESSAGE_MAP()

命令视类中的消息响应:
void CCmdView::OnSwitchToCheckView()
{
AfxGetApp()->m_pMainWnd->
SendMessage(WM_COMMAND, ID_CHECK);
}

void CCmdView::OnSwitchToRadioView()
{
AfxGetApp()->m_pMainWnd->
SendMessage(WM_COMMAND, ID_RADIO);
}

主窗口中的消息映射:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
……
ON_COMMAND(ID_CHECK, OnSwitchToCheckView)
ON_COMMAND(ID_RADIO, OnSwitchToRadioView)
……
END_MESSAGE_MAP()

主窗口中的消息响应:
void CMainFrame::OnSwitchToCheckView()
{
m_wndSplitter.DeleteView(0, 1);
m_wndSplitter.CreateView(0, 1,
RUNTIME_CLASS(CChkView),
CSize(0, 0),
NULL);
m_wndSplitter.RecalcLayout();
}

void CMainFrame::OnSwitchToRadioView()
{
m_wndSplitter.DeleteView(0, 1);
m_wndSplitter.CreateView(0, 1,
RUNTIME_CLASS(CRdiView),
CSize(0, 0),
NULL);
m_wndSplitter.RecalcLayout();
}
---- 好啦,运行一下这个程序,感觉是否不错?看来大功告成了,可是……

---- 三、还有一个问题

---- 在运行我们辛辛苦苦编出来的程序时,回头看看VC 的调试窗口,你会发现有很多行这样的话:

---- Create view without document.

---- 这是说我们创建了视,可是没有相应的文档。好在这只是警告信息,不是什么错误,如果你不需要相应的文档,就完全不用去管它。可是,VC 中一种很重要的结构就是文档- 视结构,利用这种结构,对数据操纵起来非常方便。如果需要建立与视相对应的文档,应该怎么办呢?

---- 这就涉及到VC 中文档- 视结构的知识,不过不用怕麻烦,与本文有关的就只有这么两点而已:

---- 1. 利用VC 创建的应用程序一般都会管理一些文档模板(Document Template),文档类和视类的对应关系就是在文档模板里描述的。

---- 2. 一个文档可以有多个视,创建视的时候,需要根据文档和视的对应关系,给出它所依附的文档。

---- 怎样实现上述第一点呢?

---- 首先建立相应的文档类:CRdiDoc 和CChkDoc。

---- 其次是定义相应的文档模板,这是应用类的成员变量。因为在别的类中要使用它们,我们将之定义为公共类型:

class CViewSwitcherApp : public CWinApp
{
……
public:
CSingleDocTemplate* m_pRdiDocTemplate;
CSingleDocTemplate* m_pChkDocTemplate;
……
}
然后创建这两个文档模板,并加入到模板列表中:
BOOL CViewSwitcherApp::InitInstance()
{
……
m_pRdiDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CRdiDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CRdiView));
AddDocTemplate(m_pRdiDocTemplate);

m_pChkDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CChkDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CChkView));
AddDocTemplate(m_pChkDocTemplate);
……
}
---- 至于第二点,是在创建视时完成的。还记得创建视的情况么?当时有一个叫做pCreateContext 的参数,我们将之置为空,这里就要用到它了。

---- pCreateContext 是一个指向被称作" 创建上下文"(CreateContext) 结构的指针,这个结构中保存一些与创建视相关的内容。在创建主窗口时,系统会构造这样一个结构,并将它作为参数传递到与创建视有关的函数中。但现在我们不创建主窗口,因此不得不自己构造这样一个结构。实际上,该结构中我们所要使用的字段只有三个:

---- 1. 新视所属的文档模板m_pNewDocTemplate;

---- 2. 新视的类型m_pNewViewClass;

---- 3. 新视所属的文档m_pCurrentDoc;

---- 其中仅有第三项需要新建,前两项都是已知的,只要指定即可。以切换到选项视为例,修改后的代码是:

void CMainFrame::OnSwitchToRadioView()
{
m_wndSplitter.DeleteView(0, 1);

CCreateContext createContext;
// 定义并初始化CreateContext
// 获取新视所属的文档模板
CSingleDocTemplate* pDocTemplate =
((CViewSwitcherApp*)AfxGetApp())-> m_pRdiDocTemplate;
// 创建新文档并初始化
CDocument* pDoc = pDocTemplate->CreateNewDocument();
pDoc->OnNewDocument();

// 设置CreateContext 相关字段
createContext.m_pNewViewClass = RUNTIME_CLASS(CChkView);
createContext.m_pCurrentDoc = pDoc;
createContext.m_pNewDocTemplate = pDocTemplate;

m_wndSplitter.CreateView(0, 1,
RUNTIME_CLASS(CRdiView),
CSize(0, 0),
&createContext);

m_wndSplitter.RecalcLayout();
}
---- 四、最后的修改

---- 为了使这个程序更符合要求,我们还要做一些与更换视无关的修改。在这个程序中我们一共定义了三种类型的文档,程序启动时一般要新建一个文档开始工作,可是它不知道要选择哪一种,就弹出一个对话框来询问。而这是我们不希望看到的。修改的方法是不让VC 选择新文档类型,而我们指定创建哪一种类型的文档,即把CViewSwitcherApp::CViewSwitcherApp() 中的语句

---- if (!ProcessShellCommand(cmdInfo)) return FALSE;

---- 更改为

---- m_pDocTemplate->OpenDocumentFile(NULL)。

5.2
Replacing a view in a doc-view application
By Jorge Lodos Vigil

http://www.codeproject.com/docview/ReplacingView.asp



<< Home

This page is powered by Blogger. Isn't yours?