本講介紹 MFC 文件/視圖架構中各類別的關聯機制、文件讀取流程與序列化操作,並詳細比較三種文件資料顯示方法:CEditView 機制、編輯控件 (CEdit)、以及直接使用 CDC 繪圖類別輸出文字,分析各自優缺點與適用情境。
使用 MFC 應用程式精靈建立單一檔案專案 TextViewer 後,精靈自動產生下列核心類別:
CTextViewerApp — 應用程式類別,程式進入點CMainFrame — 主框架視窗類別,管理標題列、選單、工具列、狀態列CTextViewerDoc — 文件類別,負責資料讀取/儲存CTextViewerView — 視圖類別,負責資料顯示與使用者輸入回應CAboutDlg — 關於對話框類別類別宣告置於 .h 標頭檔,實作碼置於 .cpp 檔。三大核心類別 (框架、文件、視圖) 之間的關聯是在 CTextViewerApp::InitInstance 中由 CSingleDocTemplate 建立:
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CTextViewerDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CTextViewerView));
AddDocTemplate(pDocTemplate);

圖 1 InitInstance 中的文件模板建立代碼
關鍵機制:
CSingleDocTemplate 協調文件、框架視窗、視圖三者關係。RUNTIME_CLASS 宏取得 CRuntimeClass 結構指標,支援執行期類型資訊 (RTTI)。AddDocTemplate 將模板指標加入內部清單。執行程式,選擇 [檔案] → [開啟舊檔],會自動彈出通用「開啟舊檔」對話框。此功能源於精靈自動將 ID_FILE_OPEN 對應至 CWinApp::OnFileOpen,透過訊息對應實現:
BEGIN_MESSAGE_MAP(CTextViewerApp, CWinApp)
ON_COMMAND(ID_FILE_OPEN, &CWinApp::OnFileOpen)
END_MESSAGE_MAP()

圖 2 應用程式類別的訊息對應
訊息對應機制:Windows 所有動作 (系統或使用者) 產生事件/訊息。ON_COMMAND 專門對應選單等命令訊息,參數為命令 ID 與處理函數。
使用者在對話框指定檔案後,流程如下:
CDocument::OnOpenDocument 虛函數開啟檔案。DeleteContents 清除文件物件現有內容。CArchive (歸檔類別) 物件進行資料讀取。Serialize 函數執行序列化讀取。CView::OnInitialUpdate 更新顯示。關鍵類別說明:
CArchive:提供 << (寫入) 與 >> (讀取) 運算子,支援 CObject、CString 等類型,另提供 ReadString/WriteString 逐行讀寫。Serialize:雙向函數,依 CArchive 內部旗標決定讀取或儲存。
圖 3 Serialize 函數代碼範例
我們在文件類別的 Serialize 中加入讀取程式碼,將檔案文字內容逐行讀入 CStringArray 集合類別物件。
CTextViewerDoc 加入成員變數:CStringArray m_strContent。CStringArray 封裝 CString 陣列操作 (Add、RemoveAll、GetAt 等),類似集合類別尚有 CByteArray、CUIntArray、CWordArray、CDWordArray。
圖 4 加入 CStringArray 成員變數
CTextViewerDoc::Serialize 加入讀取代碼:
if (ar.IsStoring())
{
// 儲存邏輯 (略)
}
else
{
m_strContent.RemoveAll();
CString strLine;
while (ar.ReadString(strLine))
{
m_strContent.Add(strLine);
}
}

圖 5 Serialize 中加入讀取檔案文字內容代碼
ReadString 成功讀出一行回傳 TRUE,檔案結尾回傳 FALSE,配合 while 迴圈讀完整個檔案。
DeleteContents 確保開啟新檔案時清除舊資料:
圖 6 加入 DeleteContents 函數覆寫
void CTextViewerDoc::DeleteContents()
{
m_strContent.RemoveAll();
CDocument::DeleteContents();
}

圖 7 DeleteContents 中加入清除代碼
備註:也可將 m_strContent.RemoveAll() 直接放在 Serialize 的 while 迴圈前,省略覆寫 DeleteContents 步驟。
CEditView 內建簡易文字編輯功能 (列印、尋找取代、剪貼簿操作) 且支援序列化。
InitInstance 中文件模板,將視圖類別從 CTextViewerView 改為 CEditView:
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CTextViewerDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CEditView)); // 變更此處

圖 8 文件模板中變更關聯的視圖類別
CTextViewerDoc::Serialize 中呼叫 CEditView 序列化:
((CEditView*)m_viewList.GetHead())->SerializeRaw(ar);

圖 9 加入 CEditView 序列化代碼
評析:簡單有效,具編輯功能。缺點:原視圖類別 CTextViewerView 失去作用,難以進行深層視圖控制。VC++.NET 中可能出現亂碼 (已知 Bug)。
在視圖客戶區建立與視圖同大小的編輯控件,將文件文字內容轉送至控件。
CTextViewerView 加入成員指標 CEdit* m_ctrlEdit。OnInitialUpdate 建立控件:
void CTextViewerView::OnInitialUpdate()
{
CView::OnInitialUpdate();
if (m_ctrlEdit) delete m_ctrlEdit;
m_ctrlEdit = new CEdit;
m_ctrlEdit->Create(WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL |
ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL,
CRect(0,0,0,0), this, 1);
// 將文件內容填入編輯控件
CTextViewerDoc* pDoc = GetDocument();
CString str;
for (int i = 0; i < pDoc->m_strContent.GetSize(); i++)
{
str += pDoc->m_strContent.GetAt(i) + _T("\r\n");
}
m_ctrlEdit->SetWindowText(str);
}

圖 10 OnInitialUpdate 中建立編輯控件代碼
if (m_ctrlEdit) delete m_ctrlEdit;WM_SIZE 訊息,視窗大小改變時調整控件尺寸:
void CTextViewerView::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
if (m_ctrlEdit)
m_ctrlEdit->MoveWindow(0, 0, cx, cy);
}

圖 11 加入 WM_SIZE 訊息對應

圖 12 OnSize 中調整控件大小代碼

圖 13 使用編輯控件顯示文件資料結果
評析:簡單且具編輯功能。缺點:格式單調 (行距、字距無法調整),視圖繪圖功能無法發揮。
CDC 為裝置環境類別,提供四個文字輸出函數:
TextOut — 簡單輸出ExtTextOut — 支援字符間距、背景色、矩形裁剪TabbedTextOut — 支援 Tab 定位,適合對齊顯示DrawText — 矩形區域多行輸出,效率佳本例使用 TabbedTextOut,原型:
CSize TabbedTextOut(int x, int y, const CString& str,
int nTabPositions, LPINT lpnTabStopPositions, int nTabOrigin);
CViewerView 基底類別改為 CScrollView (支援自動捲動/縮放)。CViewerDoc 加入 CStringArray m_strContent 及讀取代碼 (同方法二)。CViewerView 加入成員函數 void DispContent(CDC* pDC)。
圖 14 加入 DispContent 成員函數
DispContent 實作代碼:
void CViewerView::DispContent(CDC* pDC)
{
CViewerDoc* pDoc = GetDocument();
int y = 0;
int nTabStops[] = { 200, 400, 600 }; // Tab 停靠位置
for (int i = 0; i < pDoc->m_strContent.GetSize(); i++)
{
pDC->TabbedTextOut(0, y, pDoc->m_strContent.GetAt(i),
3, nTabStops, 0);
y += 20; // 行距
}
// 設定捲動範圍
SetScrollSizes(MM_TEXT, CSize(800, y));
}

圖 15 DispContent 函數代碼
SetScrollSizes 設定座標映射模式 (MM_TEXT:像素單位,X 向右、Y 向下遞增) 與邏輯捲動視窗大小。虛擬視窗超過顯示區域時自動出現捲軸。
OnDraw 呼叫 DispContent:
void CViewerView::OnDraw(CDC* pDC)
{
DispContent(pDC);
}

圖 16 OnDraw 中呼叫 DispContent

圖 17 文字繪製結果 (含 Tab 對齊)
評析:可精確控制行距、字距、Tab 對齊,善用 CDC 繪圖功能,列印/預覽自動生效。缺點:顯示速度較慢,程式碼較複雜。
本講透過 MFC 文件讀取流程探討序列化操作,並比較三種顯示方法:CEditView 最簡單但擴充性低;編輯控件平衡簡易與功能;CDC 直接繪圖最靈活強大但效能較低。下一講將透過選單與工具列改變文字字型與顏色,探討三者與狀態列的互動關係。