Single Document With Two Different Views

 

目的:

    在這個例子希望結合範例 69 和 71建構一個 MDI 程式, 它有兩種不同的顯示資料方式, 一種是運用 FormView 來做固定欄位的顯示, 另外一種則希望是用 Hex 的方式來顯示檔案的完整內容

    由於程式操作者可以在 FormView 中修改文件的內容, 當內容修改後我們還是希望能夠在 Hex 的視窗內看到修改過的內容, 所以我們希望同一個檔案對應到程式裡單一的 CDocument 物件

    我們要求的功能如下

    1. 程式新開啟的時候會顯示 FormView / HexView 兩種不同型態的選單讓使用者挑選
    2. 如果使用者選擇 HexView, 他可以使用 視窗/開啟 FormView 視窗 選單來開啟另一個 FormView 顯示同一份文件
    3. 如果使用者選擇 FormView, 他可以使用 視窗/開啟 HexView 視窗 選單來開啟另一個 HexView 顯示同一份文件
    4. 只有在 FormView 視窗中可以修改文件的內容, HexView 視窗中只能看到檔案的十六進位內容
    5. 如果在 FormView 中修改資料, 在 HexView 中必須立刻看到修改的結果
    6. 請替你的 document 物件加上存檔以及讀檔的功能
    7. 開啟舊檔案時以檔案的副檔名 .hex 或是 .frm 來決定使用 HexView 或是 FormView, 其他的副檔名都使用 HexView 來顯示
    8. 請替你的 HexView 視窗撰寫列印的程式碼, 列印十六進位的資料 (範例 71 中已經有部份的列印程式, 只是沒有適當地結合起來, 同時它使用 MM_TEXT 模式而不是 MM_LOENGLISH)
    9. 請替 FormView 中的控制項加入 tooltip 功能

    範例執行程式

    額外功能

    1. FormView 並不支援列印, 但是如果你先完成 HexView (繼承 CScrollView), 如此你的界面還是可以列印,你可以在 FormView 中處理和 ID_FILE_PRINT, ID_FILE_PRINT_DIRECT, 和 ID_FILE_PRINT_PRIEVIEW 的訊息, 同時定義 OnPreparePrinting, OnBeginPrinting 和 OnEndPrinting, 如此新增的 FormView 還是有列印的界面, 你也許可以列印文字格式的檔案資料
    2. 其他...

基本設計:

這個應用程式基本上需要使用多個 document template, 根據上述的功能要求,我們需要有兩個不同的 view 對應到一個 document, 這個 document 裡資料由於要提供兩個不同種類的 view 來讀取, 由於其中 HexView 需要的資料是原始的檔案資料, 所以當 document 物件由檔案系統裡將資料讀入時不能像範例 69 中直接讀進 document 裡的成員變數, 必須維持檔案裡位元串流的格式, 我們可以把資料讀入一個 CMemFile 中暫存, 當 FormView 需要由 document 物件中取得資料時, 直接由已開啟的 CMemFile 中透過界面函式讀出資料, 並且透過 DDX 機制顯示在表格中, 當 FormView 中有資料被修改, 立刻透過 界面函式修改 CMemFile 裡的資料, 將 document 設為 modified, document 物件寫入檔案時就將 CMemFile 裡的資料儲存到檔案中。

在範例 71 中 HexView 使用 MM_TEXT 模式, 這種模式在必須加上特別的程式碼來製作列印功能, 我們希望使用 MM_LOENGLISH 模式來撰寫, 不過要特別注意 y 座標軸變成是向上的。

步驟一:

我們先模仿範例 71 HexView 應用程式製作一個 HexView 的程式, 但是需要加上設定顯示字型、列印、以及設定列印字型的功能

由於我們使用 MM_LOENGLISH 模式來撰寫, 顯示在螢幕上和列印時使用的字型是相同的

以 IDE 製作具有 CScrollView 的 MDI 應用程式

(此項暫時跳過, 在步驟三中再行設計) 在 view 的 OnInitialUpdate() 函式 以及 OnUpdate() 函式中由 document 物件讀取檔案的位元組數, 計算整個文件在邏輯座標系統中的大小, 讀取視窗的大小, 轉換為邏輯座標系統中的大小, 計算目前字型的高度及寬度, 轉換為邏輯座標系統中的大小, 最後呼叫 SetScrollSize() 設定 ScrollBar 的參數

在選單中加入一個 "檢視/設定顯示字型" 的功能選項 ID_VIEWFONT

在 view 類別中處理此選單命令, 加入下列程式, 以開啟字型選取對話盒

	LOGFONT lfCopy;
	memcpy(&lfCopy, &m_logfont, sizeof(lfCopy));

	// pop up the common font dialog for user to choose
	CFontDialog dlg(&lfCopy);
	dlg.m_cf.Flags |= CF_FORCEFONTEXIST | CF_FIXEDPITCHONLY;

	// create the font and save it in a member variable for later use
	if (dlg.DoModal() == IDOK)
	{
		CFont*  pFontCopy = new CFont;
		if (pFontCopy->CreateFontIndirect(&lfCopy))
		{
			m_nPointSize = dlg.GetSize();
			if (m_pFont != NULL)
				delete m_pFont;

			m_pFont = pFontCopy;
			memcpy(&m_logfont, &lfCopy, sizeof(lfCopy));
			Invalidate();
		}
		else
		{
			delete pFontCopy;
			MessageBox(_T("Could not create new font!"));
		}
	}

這一段程式需要三個配合的成員變數, 在 view 類別中加入

1. LOGFONT m_logfont;
2. CFont *m_pFont;
3. int m_nPointSize;

並且在建構元函式中以下列程式中適當地初始化

	memset(&m_logfont, 0, sizeof(m_logfont));
	m_nPointSize = 120;
	_tcscpy(m_logfont.lfFaceName, "Fixedsys"));

	// start out with a system font

	CWindowDC dc(NULL);
	m_logfont.lfHeight = ::MulDiv(m_nPointSize, dc.GetDeviceCaps(LOGPIXELSY), 720);
	m_logfont.lfPitchAndFamily = FIXED_PITCH;

	m_pFont = new CFont;
	m_pFont->CreateFontIndirect(&m_logfont);

並在解構元函式中刪除 m_pFont 物件

編譯並執行

步驟二:

在 View 類別中加入下列函式計算字型實際顯示時的高度

int CMyView::MeasureFontHeight(CFont* pFont, CDC* pDC)
{
	CFont* pOldFont;
	pOldFont = pDC->SelectObject(pFont);

	CRect rectDummy;
	CString strRender = _T("1234567890ABCDEF- ");
	int nHeight = pDC->DrawText(strRender, -1, rectDummy,
								DT_TOP | DT_SINGLELINE | DT_CALCRECT);
	pDC->SelectObject(pOldFont);

	return nHeight;
}

使用這種方式量測到的是真正顯示在螢幕上的高度, 在預覽列印功能裡當縮小顯示時,
有時因為找不到小的字型, 會用較大的字型來取代, 計算時需要很小心, 有些地方我會直接
由 font 物件中讀取其高度 lfHeight

步驟三:

在 OnDraw() 中加入顯示十六進位的程式碼

首先 document 類別中存放原始檔案資料,所以應該設計一個成員函式 GetDocLength() 可以取得 document 的大小 (單位為 byte)

OnPrepareDC(): 先不考慮列印的功能, 只設定 mapping mode

pDC->SetMapMode(MM_LOENGLISH);

只要 document 物件內容有所修改, scrollview 的參數就需要有變化, 所以我們在 view 的 OnInitialUpdate() 函式以及 OnUpdate() 函式中由 document 物件讀取檔案的位元組數, 計算整個文件在邏輯座標系統中的大小, 讀取視窗的大小, 轉換為邏輯座標系統中的大小, 計算目前字型的高度及寬度, 轉換為邏輯座標系統中的大小, 最後呼叫 SetScrollSize() 設定 ScrollBar 中控制項

    CClientDC dc(this);
    dc.SetMapMode(MM_LOENGLISH);
    CFont *pOldFont = dc.SelectObject(m_pFont);

    char str[] = "01035620 - 61 62 63 64 65 66 67 68-69 6a 6b 6c 6d 6e 6f 70  abcdefghijklmnop ";
    CSize lineSize;
    ::GetTextExtentPoint32(dc.GetSafeHdc(), str, (int)strlen(str), &lineSize);
    dc.SelectObject(pOldFont);

    int docLength = GetDocument()->GetDocLength();
    if (docLength == 0) docLength = 16;

    CSize sizeTotal(lineSize.cx, ((docLength-1)/16+1)*lineSize.cy);
    CSize sizeLine(lineSize.cx/((int)strlen(str)), lineSize.cy);

    CRect   rectClient;
    GetClientRect(&rectClient);
    dc.DPtoLP(&rectClient);
    CSize sizePage(rectClient.Width(), -rectClient.Height());

    SetScrollSizes(MM_LOENGLISH, sizeTotal, sizePage, sizeLine);

步驟四:

接下來就是顯示十六進位的繪圖程式碼了, 先不管列印部份的程式, 最主要就是計算出視窗對應到文件裡的哪一個區塊 viewRect 以及起始的列數 nStartLine, 然後由 document 物件中一列一列地把資料讀出來繪出

   CTwoViewDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);
   CRect   viewRect;
   int     nStartLine;
   int     nHeight;
   CFont *pOldFont = (CFont*) pDC->SelectObject(m_pFont);


   if (m_bPrinting)
   {
       viewRect = CRect(1, -1, // .01 inch additional margin
                        m_nPageWidth - 1,
                        -m_nPageHeight + 1);
       nStartLine = m_nPrintLine;

       LOGFONT lf;
       m_pFont->GetLogFont(&lf);
       nHeight = lf.lfHeight;
   }
   else
   {
       CRect   rectClient;
       GetClientRect(&rectClient);
       pDC->DPtoLP(&rectClient);

    // 計算一行的高度與顯示字串的寬度 (單位是 logical unit, i.e. 0.01 in)

       char str[] = "01035620 - 61 62 63 64 65 66 67 68-69 6a 6b 6c 6d 6e 6f 70  abcdefghijklmnop ";
       CSize lineSize;
       ::GetTextExtentPoint32(pDC->GetSafeHdc(), str, (int)strlen(str), &lineSize);

       nHeight = -lineSize.cy;

    // 取得目前視窗左上角的邏輯座標 (整個文件的左上角設為 0,0)

       CPoint   scrollPos = GetScrollPosition();
       nStartLine = scrollPos.y / nHeight;

       viewRect = CRect(0, 
                        nStartLine*nHeight,
                        lineSize.cx,
                        scrollPos.y + rectClient.Height());
    }

    CString strRender;
    strRender = "01035620 - 61 62 63 64 65 66 67 68-69 6a 6b 6c 6d 6e 6f 70  abcdefghijklmnop ";

//    TRACE("nHeight=%d m_nPageHeight=%d\n", nHeight, m_nPageHeight);

   if (pDoc->m_pFile != NULL)
   {
       int nLine;

       int docLength = pDoc->GetDocLength();

       for (nLine = nStartLine; (viewRect.top >= viewRect.bottom);
            nLine++)
       {
           if (!pDoc->ReadLine(strRender, 16, nLine*16))
               break;

           pDC->TextOut(viewRect.left, viewRect.top, strRender);
           viewRect.top += nHeight;
       }
   }

   pDC->SelectObject(pOldFont);

步驟五:

列印的部份我們在三個地方處理

  1. CHexView::OnPrepareDC
  2. CHexView::OnBeginPrinting
  3. CHexView::OnDraw (上一步驟中已經看到程式碼)

OnPrepareDC 中一是要判別此次的 DC 是螢幕繪圖還是印表機列印, 並且記錄在一個成員變數 m_bPrinting 中, 以便 OnDraw 中可以因應, 另外則是計算由哪一列開始列印 m_nPrintLine, 其程式碼如下

   if (pInfo == NULL)
       m_bPrinting = FALSE;
   else
   {
       int nHeight;
//     nHeight = -MeasureFontHeight(m_pFont, pDC);
       LOGFONT lf;
       m_pFont->GetLogFont(&lf);
       nHeight = -lf.lfHeight;
       m_nPrintLine = ::MulDiv(pInfo->m_nCurPage-1, m_nPageHeight, nHeight);
       m_bPrinting = TRUE;
   }

OnBeginPrinting 最主要是計算總共要列印多少頁, 利用 pInfo->SetMinPage() 及 pInfo->SetMaxPage() 設定, 其主要程式碼如下

   pDC->SetMapMode(MM_LOENGLISH);

   // in the unit of printer dot (e.g. 600dpi, 1/600 inch)
   CSize rawSize=CSize(pDC->GetDeviceCaps(PHYSICALWIDTH) - 2*(pDC->GetDeviceCaps(PHYSICALOFFSETX)),
                       pDC->GetDeviceCaps(PHYSICALHEIGHT) - 2*(pDC->GetDeviceCaps(PHYSICALOFFSETY)));
   pDC->DPtoLP(&rawSize);
   m_nPageWidth = rawSize.cx; // in logical unit (1/100 inch)
   m_nPageHeight = rawSize.cy;

   int nHeight = -MeasureFontHeight(m_pFont, pDC); // logical unit
   int nDocLength = pDoc->GetDocLength(); 
   int nPages = (nDocLength/16*nHeight + (m_nPageHeight-1)) / m_nPageHeight;

   pInfo->SetMinPage(1);
   pInfo->SetMaxPage(nPages);

CHexView 中所有用到的成員變數如下

   LOGFONT m_logfont;
   CFont *m_pFont;
   int m_nPointSize;
   BOOL m_bPrinting;
   int m_nPrintLine;
   int m_nPageWidth;
   int m_nPageHeight;

步驟六:

由於 document 物件內 ReadLine 功能還沒有寫,你沒有辦法測試上述程式,可以先在 document 物件裡加上一個 ReadLine 的函式, 傳回固定的一個 CString 來測試, 例如

    CString strRender = "01035620 - 61 62 63 64 65 66 67 68-69 "
                        "6a 6b 6c 6d 6e 6f 70 abcdefghijklmnop "; 
    return strRender;

編譯並且測試, 你應該有一個正常顯示的 HexView,

由於離完成整個程式還有一段時間, 同時你已經作了不少程式碼了, 這個時候你應該要離開 visual studio, 把程式碼用 WINRAR 包裝起來

步驟七:

撰寫 document 物件中的 ReadLine

   BOOL CTwoViewDoc::ReadLine(CString& strLine, int nLength, LONG lOffset)
   {
       LONG lPosition;

       if (lOffset != -1L)
           lPosition = (long)m_pMemFile->Seek(lOffset, CFile::begin);
       else
           lPosition = (long)m_pMemFile->GetPosition();//取得檔案位置

       if (lPosition == -1L)
       {
           TRACE2("CTwoViewDoc::ReadLine returns FALSE Seek(%8.81X, %8.81X)\n",
                   lOffset, lPosition);
           return FALSE;
       }

       BYTE* pszBuffer = new BYTE[nLength];
       int nReturned = m_pMemFile->Read(pszBuffer, nLength);//讀入檔案

       if (nReturned <= 0)
       {
           TRACE2("CTwoViewDoc::ReadLine returns FALSE Read(%d, %d)\n",
                  nLength, nReturned);
           delete pszBuffer;
           return FALSE;
       }

       CString strTemp;
       CString strCharsIn;

       strTemp.Format(_T("%8.8lX - "), lPosition);
       strLine = strTemp;
       //進制的轉換
       for (int nIndex = 0; nIndex < nReturned; nIndex++)
       {
           if (nIndex == 0)
               strTemp.Format(_T("%2.2X"), pszBuffer[nIndex]);
           else if (nIndex % 16 == 0)
               strTemp.Format(_T("=%2.2X"), pszBuffer[nIndex]);
           else if (nIndex % 8 == 0)
               strTemp.Format(_T("-%2.2X"), pszBuffer[nIndex]);
           else
               strTemp.Format(_T(" %2.2X"), pszBuffer[nIndex]);

           if (_istprint, (pszBuffer[nIndex]))
               strCharsIn += pszBuffer[nIndex];
           else
               strCharsIn += _T('.');
           strLine += strTemp;
       }
       if (nReturned < nLength)
       {
           CString strPadding(_T(' '), 3*(nLength-nReturned));
           strLine += strPadding;
       }
       strLine += _T("  ");
       strLine += strCharsIn;

       delete pszBuffer;
       return TRUE;
   }

這個函式基本上是和範例 71 中的讀檔程式相同, 唯一更改的是由一個 CMemFile 物件 m_pMemFile 中讀取資料

步驟八:

在 OpenDocument 時開啟檔案並且開啟 CMemFile, 將檔案資料讀入 CMemFile 中暫存

BOOL CTwoViewDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
    if (!CDocument::OnOpenDocument(lpszPathName))
        return FALSE;

    //檔案不是空時的處理
    if (m_pFile != NULL)
    {
        m_pFile->Close();
        delete m_pFile;
        m_pFile = NULL;
    }
    if (m_pMemFile != NULL)
    {
        delete m_pMemFile;
        m_pMemFile = NULL;
    }

    try
    {   //分配空間
        m_pFile = new CFile(lpszPathName, CFile::modeReadWrite | CFile::typeBinary);
    }
    //檔案異常時的處理
    catch (CFileException* e)
    {
        CString strError;
        strError.Format(_T("Couldn't open file: %d"),
            _sys_errlist[e->m_lOsError]);
        AfxMessageBox(strError);
        return FALSE;
    }
    //取得檔案長度
    m_lFileSize = (long) m_pFile->GetLength();

   m_pMemFile = new CMemFile;
   char buf[100];
   int nbytes;

   nbytes = m_pFile->Read(buf, 100);
   while (nbytes > 0)
   {
       m_pMemFile->Write(buf, nbytes);
       nbytes = m_pFile->Read(buf, 100);
   }

   return TRUE;
}

另外 GetDocLength() 如下

int CTwoViewDoc::GetDocLength(void)
{
    return m_lFileSize;
}

存檔的基本邏輯如下

BOOL CTwoViewDoc::OnSaveDocument(LPCTSTR lpszPathName)
{
    if (m_pMemFile)
    {
        if (m_pFile == 0)
            return FALSE; // illegal situation
        if (m_pFile->GetFilePath() != lpszPathName)
        {
            m_pFile->Close();
            delete m_pFile;
            m_pFile = new CFile(lpszPathName, CFile::modeCreate | 
                                              CFile::modeReadWrite | 
                                              CFile::typeBinary);
            // m_strPathName
        }
        m_pFile->Seek(0L, CFile::begin);

        char buf[100];
        int nbytes;

        m_pMemFile->Seek(0L, CFile::begin);
        nbytes = m_pMemFile->Read(buf, 100);
        while (nbytes > 0)
        {
            m_pFile->Write(buf, nbytes);
            nbytes = m_pMemFile->Read(buf, 100);
        }
        SetModifiedFlag(FALSE);
        return TRUE;
    }
    else
        return FALSE;

//  return CDocument::OnSaveDocument(lpszPathName);
}


編譯, 執行, 偵錯, 測試後包裝為 winrar 的 package, 以免將來做錯什麼設計沒有辦法回復, 可以由此處接續下去

步驟九:

新增一個 MFC 類別 FormView, 繼承 CFormView 類別, 使用對話盒資源 IDD_FORMVIEW

在資源編輯器中編輯 對應的對話盒內容 如下

這個界面主要是讓使用者可以指定某一個檔案中的連續 8 個位元組, 直接檢視其內容, 並且修改其內容, 請替這些欄位 (IDC_ADDR, IDC_BYTE0, ... IDC_BYTE7) 設定 CString 型別的 DDX 變數

步驟十:

由於希望在新增檔案時讓使用者決定需要開啟哪一種 View, 所以我們使用不同的 Document Template:

在 CWinApp::InitInstance() 中加入新的 Document Template 的程式碼

    pDocTemplate = new CMultiDocTemplate(IDR_TwoViewTYPE1,
        RUNTIME_CLASS(CTwoViewDoc),
        RUNTIME_CLASS(CChildFrame), // 自訂 MDI 子框架
        RUNTIME_CLASS(FormView));
    if (!pDocTemplate)
        return FALSE;
    AddDocTemplate(pDocTemplate);

指定新的資源 ID IDR_TwoViewTYPE1, 可以在使用這個新的 document template 開啟 document-frame-view 時使用指定的選單和圖示, 所以同時需要在資源編輯器中加入新的 Icon 以及 Menu, 可以由舊的 IDR_TwoViewTYPE 拷貝後再修改

加入新的選單項目 視窗/開新 HexView 視窗, ID_HEXVIEW

在資源編輯器中新增 DocString IDR_TwoViewTYPE1 (拷貝原來的 DocString IDR_TwoViewTYPE)

       \nFormView\nForm View\n\n\nTwoView.Document\nTwoView.Document

其中包括七個以 \n 分隔的字串, 意義如下:

       windowTitle\n
       docName\n
       fileNewName\n
       Files (*.abc)\n
       .abc\n
       regFileTypeId.Document\n
       regFileTypeName.Document

我們修改的是 docName 以及 fileNewName, 其中 fileNewName 的字串就是在新開檔案時選單中所看到的文字, docName 則是我們在程式裡判斷是否 為指定的 document template 時使用到

相對應於我們在 FormView 的界面中以 視窗/開新 HexView 視窗 選單來打開一個 HexView 視窗, 我們在 HexView 的界面中也需要可以使用 視窗/開新 FormView 視窗 選單來打開一個 FormView 視窗

所以在資源編輯器中修改 IDR_TwoViewTYPE 的選單

加入新的選單項目 視窗/新開 FormView 視窗, ID_FORMVIEW

步驟十一:

在 CMainFrame 類別中處理上述兩個選單項目 ID_HEXVIEW 以及 ID_FORMVIEW, 透過適當的 document template 物件開啟對應的 view, 請注意新的 view 物件還是對應到相同的 document 物件

void CMainFrame::OnHexView()
{
    CMDIChildWnd* pActiveChild = MDIGetActive();
    CDocument* pDocument;
    if (pActiveChild == NULL ||
        (pDocument = pActiveChild->GetActiveDocument()) == NULL)
    {
        TRACE(traceAppMsg, 0, "Warning: No active document for WindowNew command.\n");
        AfxMessageBox(AFX_IDP_COMMAND_FAILURE);
        return;     // command failed
    }

    POSITION pos = AfxGetApp()->GetFirstDocTemplatePosition();
    CDocTemplate *pTemplate;
    CString buf;
    while (pos != NULL)
    {
        pTemplate = AfxGetApp()->GetNextDocTemplate(pos);
        pTemplate->GetDocString(buf, CDocTemplate::docName);
        if (buf == "HexView") 
        {
            ASSERT_VALID(pTemplate);
            CFrameWnd* pFrame = pTemplate->CreateNewFrame(pDocument, 0);
            if (pFrame == NULL)
            {
                TRACE(traceAppMsg, 0, "Warning: failed to create new frame.\n");
                return;     // command failed
            }
            pTemplate->InitialUpdateFrame(pFrame, pDocument);
        }
    }
}

步驟十二:

OnInitialUpdate() 由 document 物件內讀取檔案內指定位址 m_addr ... m_addr+7 的資料內容到對應的變數中 m_byte0, ... m_byte7

如果操作者修改位址欄位, 利用 DDX 由欄位中讀入修改過後的位址, 立即由 document 物件中讀取對應資料顯示在界面中 (Hint: 在對話盒編輯棄中以右鍵點選編輯欄位, 加入事件處理常式, 在 FormView 類別中處理 EN_CHANGE notification 訊息)

如果操作者修改任一資料欄位, 利用 DDX 由欄位中讀入修改過後的內容, 立即修改 document 物件中對應的位元組, 設定 document 物件為 modified, 並且呼叫 UpdateAllViews() 更新所有顯示此檔案的 view (包括 formview 與 hexview), 此時各個 view 物件的 OnUpdate() 函式會被呼叫到, 需要在 HexView::OnUpdate() 裡面加上 Invalidate() 來重新繪製視窗內容, 至於 FormView::OnUpdate() 的內容稍為複雜, 需要重新由 document 裡讀取修改過的資料來顯示在各個欄位中

覆寫 document 類別的 OnSaveDocument(), 將 CMemFile 中已經修改的資料存檔 (程式碼在步驟八中)

編譯, 執行, 偵錯, 測試後包裝為 winrar 的 package, 以免將來做錯什麼設計沒有辦法回復, 可以由此處接續下去

步驟十三:

替 FormView 增加 tooltip 的功能

編譯, 執行, 偵錯, 測試後包裝為 winrar 的 package, 以免將來做錯什麼設計沒有辦法回復, 可以由此處接續下去

步驟十四:

標題列檔名的處理, 當檔案內容修改過而沒有存檔時, 我們希望在標題列顯示一個 * 符號, 使得使用者可以了解到更改過的文件還沒有儲存到檔案裡, 當使用者存檔或是另存新檔時, 就應該把標題列的 * 號去掉

這些功能可以藉由 CDocument 的 IsModified(), GetTitle() 以及 SetTitle() 函式來完成

步驟十五:

測試, 測試, 測試