文档、视图和单文档界面
一、文档/视图结构的程序
文档/视图结构的程序不同于传统的MFC应用程序,传统的MFC应用程序体系主要包括两个对象:应用程序对象和主窗口对象,应用程序对象的主要任务是创建 程序的主窗口而主窗口对象的任务主要是用来和程序用户进行交互操作。文档/视图体系的MFC应用程序包括了应用程序对象、框架窗口对象、视图对象和文档对 象四个方面。应用程序对象和传统MFC程序当中的应用程序对象完成类似的任务:创建其余的三个对象;而框架窗口对象创建了程序窗口的框架;视图对象是一个 覆盖在框架窗口对象的客户区上方的一个子窗口,在这个子窗口当中程序实现与用户的交互;文档对象主要是用来实现保存程序数据的功能。在这个体系当中视图对 象和文档对象通过文档的公有成员函数实现数据的交互。
使用文档/视图体系的好处在于通过VC所提供的应用程序向导我们可以方便的创建一个应用程序,并且向导将为我们完成程序当中内存数据和磁盘数据交互的过程,我们只需要为整个应用程序添加核心的代码就可以获得一个带有磁盘IO的强大应用程序。
二、文档
文档提供了应用程序数据操作的集合,在这个类当中我们封装所有程序所需要的数据,并且为这些数据的修改和读取提供公有成员函数供视图对象和文档对象交互使用。
在文档类当中我们经常需要覆盖的基类成员函数有:OnNewDocument()这个成员函数提供了创建新文档时文档对象的数据初始化过程;之所以不使用 文档对象的构造函数来完成这个功能是因为文档对象仅仅在应用程序创建之初才被创建并调用构造函数。Serialize(CArchive& ar)这个函数提供了文档数据与磁盘IO的交互功能。
文档类当中我们经常使用的基类成员函数有SetModifiedFlag(BOOL)这个函数设置一个修改标志,如果在文档被关闭的时候这个标志为 TRUE那么程序会提示用户保存数据;另一个函数是UpdateAllViews(CView *)这个函数通知与文档对象相关联的视图对象刷新他的显示。
三、视图
视图对象提供了程序与用户的交互,他通过文档对象的公用成员函数实现程序内部数据的交互。
在视图对象当中我们经常要覆盖的成员函数是OnDraw(CDC *)这个函数用来实现重画视图对象表面。另外InitialDraw成员函数跟文档类当中的OnNewDocument成员函数类似,尽当新文档被加载时 才被调用,他完成加载新文档时视图上的绘图工作。
在视图对象当中我们经常使用的基类成员函数是GetDocument()这个函数返回一个与改视图对象相关联的文档对象指针。
三、消息传递
根据Windows操作系统的规定,操作系统发送的消息只能由程序的窗口接收 。但是在MFC体系特别是基于窗口/视图结构体系的应用程序中程序的每个模块 包括应用程序类、框架窗口类、视图类、文档类都能够接受由操作系统发送而来的消息并进行处理。 这并不是MFC体系获得了操作系统的例外而是MFC体系底层 通过一系列的代码实现了一种所谓的消息传递机制。
所谓消息传递机制指的就是当属于应用程序的消息由操作系统传递而来的时候,活动视图是操作系统消息传递机制所传递的窗口类。视图类首先查找其自身对于该消 息有没有对应的消息处理程序,如果没有则传递给文档类,文档类如果不能处理该消息则传递给文档模板,按照上面所描述的过程,消息将按照活动视图-> 文档->文档模板->框架窗口->应用程序对象->::DefWindowProc的流程进行传递。当消息在这个流程的某个节点 上得到处理那么消息传递的过程将被终止。
需要注意的是消息传递机制传递的是命令消息,也就是说仅有COMMAND消息和UPDATE_COMMAND_UI 消息将通过这个机制被传递;而其他的消 息不会被传递,处理其他消息遵循消息在哪里发生就在哪里处理的规则。
SDI
一、文档/视图结构概述
文档/视图结构的MFC应用程序通过将程序的界面和程序的数据封装在两个类当中实现了程序逻辑的简化。而视图类和文档类之间通过成员函数实现了信息的双向 通信。文档/视图应用程序有两种大的分类:SDI和MDI程序。SDI是一种单文档的应用程序,这种程序的构造在上一章中已经有所描述,这章主要讲述 SDI结构当中两种特殊的视图类;MDI应用程序是一种多文档应用程序。
二、SDI应用程序
1.CScrollView视图类
这个视图类不同于CView视图类他提供了横向和纵向的两个滚动条。这个视图类提出了逻辑空间和物理空间两个概念,在程序窗口表面能够被看到的部分我们称 为物理空间,而视图类可以定义一个逻辑空间,当物理空间小于逻辑空间时窗口上会自动出现滚动条。需要注意的是无论滚动条当前所处的位置在哪里逻辑空间的坐 标都以当前窗口左上角位置为(0,0)点。
实现这个视图类我们要做的是在视图类的OnInitialUpdate成员函数当中通过调用SetScrollSizes成员函数来设置逻辑空间的大小, 而在OnDraw成员函数当中我们根据逻辑空间的大小来进行绘图。在CScrollView视图类当中响应鼠标事件时,我们必须通过调用DpToLp函数 将鼠标坐标转换为逻辑空间坐标
2.CHTMLView视图类
这个视图类提供了一个实现Internet浏览器的简便方法,在这个类当中调用Navigate成员函数能够简单的使视图窗口当中显示成员函数参数当中所提供URL的网页。
3.CTreeView视图类
这个视图类根据CTreeList通用控件封装而来。这个视图类提供了树列表的视图,通过这个视图类能够清晰的表示数据间的层次关系。由于这个视图类是从 CTreeList通用控件封装而来,因此这个视图类的核心是封装在类当中的CTreeList对象,我们可以通过调用GetTreeCtrl()函数来 获取这个对象,并且调用这个对象的方法。
CTreeView视图类提供了多种样式参数,我们可以通过在视图类的PreCreateWindow成员函数当中使用CREATESTRUCT对象的 style字段使用按位与操作将样式参数加入其中。这里常用的样式有TVS_HASLINES:在各个项之间加入线;TVS_LINESATROOT:将 子树与根之间加入线;TVS_SHOWSELALWAYS:总是加亮显示选中项;TVS_HASBUTTONS:在项前显示"+"或"-"按钮。
CTreeView视图类当中的每一项都分为位图和文字两部分组成,向CTreeView当中加入位图的方法是将一个CImageList对象加入到 CTreeView视图对象当中,需要注意的是必须在CTreeView视图对象失效后CImageList对象才能够失效,否则的话视图类当中的位图将 会消失。具体加入的方法如下:
m_ilDrives.Create (IDB_DRIVEIMAGES, 16, 1, RGB (255, 0, 255));//m_ilDrives为CImageList对象,IDB_DRIVEIMAGES为资源ID
GetTreeCtrl ().SetImageList (&m_ilDrives, TVSIL_NORMAL);
向CTreeView视图对象当中添加项需要用到InsertItem函数,这个函数返回一个HTREEITEM句柄。我们必须通过这个句柄完成对CTreeView项的大部分操作。向CTreeView视图对象当中添加项的方法如下:
GetTreeCtrl().InsertItem(szPath,IL_DIR,IL_DIR,hItem); //向句柄hItem指向的子树加入项
GetTreeCtrl().InsertItem(szPath,IL_FDD,IL_FDD); //向树根加入项
这里第二和第三个参数指示了未选中和选中时在CTreeView当中显示的位图在CImageList对象当中的索引值。
在获得某个项的HTREEITEM句柄以后可以利用这个句柄对项进行操作。这些操作主要有GetNextItem获取下一项;GetNextSiblingItem获取邻近项;GetChildItem获取该项的子项句柄;DeleteItem删除该项等等。
通过处理TVN_EXPANDING消息反射能够处理CTreeView类的展开消息,这个消息的参数NMHDR指针可以被转换为 LPNMTREEVIEW指针,并通过pNMTreeView->itemNew.hItem获取展开项的句柄;通过 pNMTreeView->action == TVE_EXPAND获取当前操作是展开还是关闭。
4.列表视图
列表视图类似于Windows的资源管理器的右窗格,他提供了以小图标、大图标、列表方式查看视图当中的项。列表视图相比于树视图更适合表示扁平关系的数 据项。列表视图同树视图一样都是根据通用控件封装而得到的视图类,我们可以通过视图类成员函数GetListCtrl来访问封装在视图当中的列表通用控件 对象,并且列表视图的大部分操作都需要通过这个控件来完成。
列表视图提供了大量的样式参数,其基本样式参数为LVS_ICON、LVS_REPORT、LVS_SMALLICON、LVS_LIST这四个样式对应 于大图标、详细信息、小图标三种查看方式。我们可以在程序创建覆盖视图之前调用视图的类的PreCreateWindow成员函数来中的 CREATESTRUCT参数的style字段设置这些样式标志,具体的方法是首先清除当前样式标志,然后加入新的样式标志。下面是一个加入 TVS_REPORT标志的实例:
cs.style &= ~TVS_TYPEMASK;
cs.style |= TVS_REPORT;
在程序运行过程当中我们同样可以获取当前视图样式并设置视图的样式,获取视图样式的方法如下:
DWORD dwStyle = GetStyle() & LVS_TYPEMASK;
dwStyle == LVS_**; //判断当前视图样式是否跟指定的TVS_**视图样式相同
修改视图样式的方法如下:
GetListCtrl().ModifyStyle(LVS_TYPEMASK,LVS_**); //将当前视图样式修改为LVS_**
如果列表视图样式为LVS_REPORT,那么我们在向列表当中加入数据之前必须先向视图当中加入列,加入列的方法是调用 GetListCtrl().InsertColumn(nIndex,strColumnName,sizeInPix)成员函数。加入视图数据的方法 是调用GetListCtrl().InsertItem()成员函数,为了支持单击列头实现列数据排序,我们通常在这里使用一个LV_ITEM数据结 构,并且将其pszText字段初始化为LPSTR_CALLBACKTEXT,并且使用一个在堆上自定义的数据结构对象来初始化lParam字段。这样 做实现了在需要显示列表项时由视图类发送LVN_DISPINFO消息并回调函数来向视图中加入数据,并且将数据保存在独立的数据结构当中以支持排序的需 要。具体做法如下:
bool CWinDirView::AddItem(int nIndex, const WIN32_FIND_DATA& fd)
{
ITEMINFO *pItem = new ITEMINFO;
if (pItem)
{
pItem->FileName = fd.cFileName;
pItem->size = fd.nFileSizeLow;
LV_ITEM lvi;
lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
lvi.iImage = 0;
lvi.iSubItem = 0;
lvi.iItem = nIndex;
lvi.pszText = LPSTR_TEXTCALLBACK;
lvi.lParam = (LPARAM) pItem;
GetListCtrl().InsertItem(&lvi);
return true;
}
return false;
}
void CWinDirView::OnLvnGetdispinfo(NMHDR *pNMHDR, LRESULT *pResult)
{
NMLVDISPINFO *pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
if (pDispInfo->item.mask & LVIF_TEXT)
{
ITEMINFO *pItem = (ITEMINFO *)(pDispInfo->item.lParam);
CString string;
switch (pDispInfo->item.iSubItem)
{
case 0:
lstrcpy(pDispInfo->item.pszText,pItem->FileName);
break;
case 1:
string.Format(_T("%d"),pItem->size);
lstrcpy(pDispInfo->item.pszText,string);
break;
}
}
*pResult = 0;
}
这里需要注意的是,由于所有的项中的数据都是在堆中建立的,因此我们需要在程序结束时和列表项被清除程序发送LVN_DELETEALLITEMS消息时释放堆上内存。
使列表视图支持排序功能我们需要响应列表视图的LVN_COLUMNCLICK消息,并且在消息处理成员函数中调用 GetListCtrl().SortItems(CompFunc,ColumIndex)成员函数,其中CompFunc是一个指向排序回调函数的函 数指针,而ColumIndex是一个标示哪个列被排序的参数,这个参数通过消息处理程序的参数的iSubItem字段获得;而回调函数的原型如下所示:
int CALLBACK CompFunc(LPARAM,LPARAM,LPARAM);
其中这里第一和第二个参数被赋予了列表视图被比较两个项的LV_ITEM对象的lParam字段数据,而最后一个参数指出了需要排序的列。正是由于这个原因,我们在加入列表数据时一般使用上面所说的方法。下面是一个列表视图对排序处理的实例:
void CWinDirView::OnLvnColumnclick(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
GetListCtrl().SortItems(CompareFunc,pNMLV->iSubItem);
*pResult = 0;
}
int CALLBACK CWinDirView::CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM SortCoumn)
{
ITEMINFO *pItem1 = (ITEMINFO *)lParam1;
ITEMINFO *pItem2 = (ITEMINFO *)lParam2;
switch (SortCoumn)
{
case 0:
return pItem1->FileName.CompareNoCase(pItem2->FileName);
case 1:
return (pItem1->size) - (pItem2->size);
}
}
列表视图不同于树视图,列表视图需要两个图标列表,一个表示大图标在大图标模式下使用,另一个表示小图标在小图标模式和列表模式下使用。在向列表视图中加 入大图标列表和小图标列表的唯一不同在于对SetImageList函数调用的最后一个参数,前者使用LVSIL_NORMAL后者使用 LVSIL_SMALLICON。具体实例如下:
m_ilSmall.Create(IDB_SMALL,16,1,RGB(255,0,255));
m_ilLarge.Create(IDB_LARGE,32,1,RGB(255,0,255));
GetListCtrl().SetImageList(&m_ilSmall,LVSIL_SMALL);
GetListCtrl().SetImageList(&m_ilLarge,LVSIL_NORMAL);
MDI和拆分窗口
一、MDI应用程序
MDI应用程序提供了一个多文档应用程序的解决方案。创建MDI应用程序只需要在程序向导当中做相应的设置即可,程序的实现与SDI应用程序完全相同。 MDI应用程序需要注意的仅仅是当某个文档当中的数据被改变后需要将与文档关联的所有视图当中的显示都进行更新,这个操作与SDI应用程序相同调用 UpdateAllViews函数,这个函数的第一个参数是一个指向视图的指针,这个指针指向的视图将不会被更新,所以如果要更新所有的关联视图则可以将 这个指针设置为NULL。另外这个函数的默认过程是调用OnUpdate函数并有这个函数使整个视图失效并调用OnDraw函数重绘整个视图。如果要提高 绘制效率也就是说只将视图的被修改部分重绘,那么我们需要用到UpdateAllViews的后两个参数,一个LPARAM类型的pHint参数和一个 CObject *的pHint参数,前一个参数可以设置为一个标志值,后一个参数可以用来传递一个CObject对象比如CRect告诉程序需要重绘的部分,然后在 OnUpdate函数当中根据两个相同的函数完成部分重绘过程。
二、窗口拆分
除了使用MDI体系结构来实现一个文档多个视图的程序外,我们也可以使用SDI结构来实现。使用SDI来实现一个文档多个视图的方法称为窗口拆分。窗口拆 分可以分为动态拆分和静态拆分两种;动态拆分是指窗口在行方向上和列方向上能构被拆分为几个子窗口由用户在程序运行时决定,静态拆分指程序在行方向祸者列 方向上能够拆分为几个窗口由程序员在程序编码阶段确定,用户不能更改拆分方式。无论是静态拆分还是动态拆分都只能在行方向或者列方向的其中一个方向上对窗 口进行拆分。动态拆分限制在行或列方向上只能拆分2个子窗口,而静态拆分可以拆分为16个子窗口。
1.动态拆分
动态拆分窗口的方法很简单,按照标准的SDI程序创建程序以后在MainFrame类当中添加一个CSplitterWnd类数据对象,并且在 MainFrame类的OnCreateClient成员函数当中加入对CSplitterWnd对象的Create过程调用,在这个成员函数当中第一个 参数表示了其父窗口对象的指针,第二第三个参数分别表示行、列方向上的拆分数,第四个参数表示拆分窗口的最小宽与高当拆分窗口小于这个数值时拆分窗口将会 被移除,第五个参数是一个指向主结构的CCreateContext指针.
2.静态拆分
静态拆分的窗口样式在程序编码时就被确定不允许用户修改,但是静态拆分的窗口在每个子窗口当中允许使用不同的视图类型。静态拆分窗口的方法可以用下面的3步描述:
1.在MainFrame类当中声名一个CSpilitterWnd对象;
2.在MainFrame类的OnCreateClient成员函数当中调用CSplitterWnd对象的CreateStatic函数确定拆分样式,其原型为:
virtual BOOL CreateStatic(
CWnd* pParentWnd,
int nRows,
int nCols,
DWORD dwStyle = WS_CHILD | WS_VISIBLE,
UINT nID = AFX_IDW_PANE_FIRST
);
3.接着对每个子窗口调用CSplitterWnd的CreateView函数用来创建视图,该函数的原型为:
virtual BOOL CreateView(
int row,
int col, //行列位置
CRuntimeClass* pViewClass, //运行时视图类指针,需要使用RUNTIME_CLASS宏
SIZE sizeInit, //视图大小,对于可以由系统确定的值,即使给定值也将被忽略
CCreateContext* pContext
);
下面是一个MainFrame::OnCreateClient函数的例子:
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
if (!m_SplitterWnd.CreateStatic(this,1,2) ||
!m_SplitterWnd.CreateView(0,0,RUNTIME_CLASS(CExplorerView),CSize(192,0),pContext) ||
!m_SplitterWnd.CreateView(0,1,RUNTIME_CLASS(CFileView),CSize(0,0),pContext))
return FALSE;
return TRUE;
}
在按照上面的步骤定义完一个拆分窗口以后,需要解决的问题还有不同视图类之间的数据通信问题。在MFC的文档/视图体系中数据是被存放在文档中的,但是某 些时候两个视图之间需要直接进行数据交换。实现这个功能的方法是在视图当中调用GetDocument获取文档类,并调用UpdateAllViews函 数,通过其中的lhInt和phInt参数来传递数据,同时视图类通过OnUpdate成员函数来接受数据。
由于静态拆分窗口以后窗口中可能包括不止一个视图类,而且MFC文档/视图体系结构的消息传递机制是活动视图->文档->文档模板-> 框架窗口->应用程序对象->DefWndProc,这样就导致了某些需要传递给某个非活动视图的消息不能被传递到达,因此我们需要修改默认 的消息传递机制。首先我们知道文档对象可以枚举所有的视图对象,因此我们可以利用这个机制来传递消息。具体过程是文档对象枚举除要求枚举的视图对象外的所 有视图对象。而某个可能的活动视图要求文档枚举所有视图对象并传递默认消息处理机制所不能处理的消息。
BOOL CExplorerDoc::RouteToAllViews(CView *pView,UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo) //文档对象枚举视图
{
POSITION pos = GetFirstViewPosition();
while (pos != NULL)
{
CView *pNextView = GetNextView(pos);
if (pNextView != pView)
if (pNextView->OnCmdMsg(nID,nCode,pExtra,pHandlerInfo))
return TRUE;
}
return FALSE;
}
BOOL CExplorerView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo)
{
if (CTreeView::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) //使用默认消息传递机制,成功(返回TRUE)就结束,否则开始枚举
return TRUE;
else
{
CExplorerDoc *pDoc = GetDocument();
if (pDoc)
return pDoc->RouteToAllViews(this,nID,nCode,pExtra,pHandlerInfo);
return FALSE;
}
}