在圖形應用開發中,常需將複雜的 C++ 對象(含動態數組、GDI 物件等)持久化到數據庫。本文展示一套成熟方案:**利用 MFC 序列化機制將對象轉為二進制流,存入 Access/SQL Server 的 BLOB (OLE Object) 字段,讀取時反序列化還原對象**。完整覆蓋類定義、數據庫連接、寫入/讀取核心代碼、關鍵數據結構轉換 (CMemFile→SAFEARRAY→VARIANT→AppendChunk/GetChunk),可直接應用於圖形、文檔、配置對象的數據庫存儲。
對象類必須:
CObject (支持運行時類信息、動態創建、序列化)。DECLARE_SERIAL(ClassName) 宏 (頭文件)。IMPLEMENT_SERIAL(ClassName, BaseClass, Schema) 宏 (CPP 文件)。virtual void Serialize(CArchive& ar),區分 ar.IsStoring() (寫入) 與 ar.IsLoading() (讀取)。<</>>,結構體用 Write/Read,容器 (如 CArray) 調用其 Serialize。
// Line.h
class CLine : public CObject {
DECLARE_SERIAL(CLine)
private:
LOGPEN m_logPen; // 畫筆屬性 (結構體)
COLORREF m_crBackColor; // 背景色
CArray m_PointArray; // 點座標動態數組
public:
CLine(); virtual ~CLine();
virtual void Serialize(CArchive& ar); // 核心序列化
// 業務方法:AddPoint, DrawPoint, DrawLine, Get/Set Color/Width...
CLine& operator=(CLine& src); // 賦值運算符
};
// Line.cpp
IMPLEMENT_SERIAL(CLine, CObject, 1) // Schema 1
void CLine::Serialize(CArchive& ar) {
if (ar.IsStoring()) {
ar << DWORD(m_crBackColor); // COLORREF 轉 DWORD 存儲
ar.Write(&m_logPen, sizeof(LOGPEN)); // 結構體原始寫入
} else {
DWORD dw; ar >> dw; m_crBackColor = COLORREF(dw);
ar.Read(&m_logPen, sizeof(LOGPEN));
}
m_PointArray.Serialize(ar); // CArray 內置序列化支持
}
// 賦值運算符 (淺拷貝非指針成員)
CLine& CLine::operator=(CLine& src) {
if (this != &src) {
m_logPen = src.m_logPen;
m_crBackColor = src.m_crBackColor;
// m_PointArray 需深拷貝或賴於 CArray operator=
}
return *this;
}
// StdAfx.h 引入 ADO 類型庫 (路徑隨 Office 版本調整)
#import "C:\\Program Files\\Common Files\\System\\ADO\\msado15.dll" \
no_namespace rename("EOF", "adoEOF")
// 初始化 COM (App::InitInstance)
AfxOleInit(); // 或 CoInitialize(NULL)
// 連接函數
BOOL CMyDlg::OpenDB(CString strPath) {
try {
HRESULT hr = m_pCon.CreateInstance(__uuidof(Connection));
_bstr_t connStr = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + _bstr_t(strPath) + ";";
hr = m_pCon->Open(connStr, "", "", adModeUnknown);
hr = m_pSet.CreateInstance(__uuidof(Recordset));
m_pSet->CursorLocation = adUseClient; // 客戶端游標支援 GetChunk/AppendChunk
hr = m_pSet->Open("SELECT * FROM object_table", _variant_t((IDispatch*)m_pCon, true),
adOpenStatic, adLockOptimistic, adCmdText);
return SUCCEEDED(hr);
} catch (_com_error& e) {
AfxMessageBox(_T("DB連接失敗: ") + e.ErrorMessage());
return FALSE;
}
}
表結構建議:object_table (id AUTOINCREMENT, name TEXT, object OLEOBJECT)。
void CMyDlg::OnButtonSave() {
if (!m_bState) return; // 連接狀態檢查
UpdateData(); // 獲取 m_sName
try {
m_pSet->AddNew();
m_pSet->PutCollect("name", _variant_t(m_sName));
// 1. 序列化到內存文件
CMemFile memFile;
CArchive ar(&memFile, CArchive::store);
m_Line.Serialize(ar); // 調用對象序列化
ar.Close(); // 必須 Close 刷新緩衝區
// 2. 獲取內存緩衝區指針與長度
DWORD dwSize = memFile.GetLength();
LPBYTE lpInfo = memFile.Detach(); // 奪取所有權,調用者負責 delete[]
// 3. 構造一維 SAFEARRAY (VT_UI1)
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].lLbound = 0;
rgsabound[0].cElements = dwSize;
SAFEARRAY* psa = SafeArrayCreate(VT_UI1, 1, rgsabound);
// 4. 填充數據
for (long i = 0; i < (long)dwSize; i++) {
SafeArrayPutElement(psa, &i, lpInfo++);
}
delete[] (lpInfo - dwSize); // 釋放 Detach 返回的緩衝區
// 5. 封裝 VARIANT 並分塊寫入 BLOB 字段
VARIANT varBLOB;
varBLOB.vt = VT_ARRAY | VT_UI1;
varBLOB.parray = psa;
// 關鍵:使用 AppendChunk 分塊寫入大對象
m_pSet->GetFields()->GetItem("object")->AppendChunk(varBLOB);
m_pSet->Update(); // 提交記錄
SafeArrayDestroy(psa); // 釋放 SAFEARRAY
m_List.AddString(m_sName); // UI 同步
} catch (_com_error& e) {
AfxMessageBox(_T("保存失敗: ") + e.Description());
}
}
void CMyDlg::OnSelchangeListData() {
int iPos = m_List.GetCurSel();
if (iPos < 0) return;
m_pSet->MoveFirst();
m_pSet->Move(iPos); // 定位到選中記錄
long lDataSize = m_pSet->GetFields()->GetItem("object")->ActualSize;
if (lDataSize <= 0) return;
_variant_t varBLOB;
varBLOB = m_pSet->GetFields()->GetItem("object")->GetChunk(lDataSize); // 一次性讀取
if (varBLOB.vt == (VT_ARRAY | VT_UI1)) {
BYTE* pBuf = nullptr;
// 1. 訪問 SAFEARRAY 數據指針
SafeArrayAccessData(varBLOB.parray, (void**)&pBuf);
// 2. 包裝為 CMemFile 並反序列化
CMemFile memFile;
memFile.Attach(pBuf, lDataSize); // 不複製,直接掛接
memFile.SeekToBegin();
CArchive ar(&memFile, CArchive::load);
m_Line.Serialize(ar); // 還原對象
ar.Close();
memFile.Detach(); // 分離,不釋放 pBuf (由 SafeArrayUnaccessData 管理)
// 3. 釋放 SAFEARRAY 訪問鎖
SafeArrayUnaccessData(varBLOB.parray);
// 4. 觸發重繪
CRect rc = GetRect(IDC_STATIC_RECT);
InvalidateRect(rc);
}
VariantClear(&varBLOB); // 清理 VARIANT
}
| 環節 | 核心 API/類 | 關鍵細節 |
|---|---|---|
| 序列化容器 | CMemFile | 內存文件,無磁盤 I/O,Detach() 移交緩衝區所有權,Attach() 掛接外部緩衝區 |
| 二進制封裝 | SAFEARRAY + VARIANT | ADO BLOB 字段要求 VT_ARRAY | VT_UI1;SafeArrayCreate/PutElement/AccessData/UnaccessData/Destroy 全生命周期管理 |
| 大對象寫入 | AppendChunk | 分塊寫入避免單次巨大內存分配,配合 adUseClient 游標 |
| 大對象讀取 | GetChunk / ActualSize | ActualSize 獲取真實大小分配緩衝;GetChunk(size) 一次性讀完 |
| 游標類型 | adOpenStatic + adLockOptimistic | 靜態游標支持書籤/絕對定位;樂觀鎖減少並發衝突 |
| COM 錯誤處理 | _com_error | 所有 ADO 操作拋出 _com_error,統一 try-catch 捕獲,ErrorMessage()/Description() 獲取詳情 |
CImage/CDocument 同樣支持 Serialize,無需修改寫入/讀取邏輯。type 字段區分對象類型,讀取時根據類型動態創建對象 (CRuntimeClass::CreateObject) 再反序列化。VARBINARY(MAX):原理相同,連接字符串改用 SQLOLEDB,字段類型改為 VARBINARY(MAX),ADO 操作不變。IMPLEMENT_SERIAL 的 wSchema 參數配合 CArchive::GetObjectSchema() 實現多版本共存。「MFC 序列化 + ADO BLOB」是 C++ 對象持久化到關係數據庫的經典高效方案。**核心鏈路**:Serialize → CMemFile → SAFEARRAY(VT_UI1) → VARIANT → AppendChunk/GetChunk。掌握 CMemFile::Detach/Attach 零拷貝技巧、SAFEARRAY 生命周期管理、adUseClient 游標配置,即可構建穩健的對象存儲層,支撐圖形編輯器、報表模板、用戶配置等複雜場景。