MFC MDI 환경에서 CRichEditView 기반의 서식 텍스트를 SQLite 데이터베이스에 지속적으로 보관하는 방법을 다룹니다. 트리 뷰의 각 노드와 편집 뷰를 연동하며, RTF 데이터를 BLOB 필드로 입출력합니다.
데이터베이스 설계
노트 정보를 담는 테이블은 다음과 같이 구성합니다. data 컬럼이 RTF 이진 데이터를 저장하는 BLOB 필드입니다.
CREATE TABLE notes (
id INTEGER PRIMARY KEY,
name TEXT,
data BLOB
);
SQLite BLOB 래퍼 클래스
원시 SQLite API를 RAII 패턴으로 감싸 예외 안전성을 확보한 클래스입니다. BLOB 읽기/쓰기에 특화되어 있습니다.
// DBManager.h
#pragma once
#include "sqlite3.h"
#include <atlstr.h>
class CDatabase
{
public:
CDatabase();
~CDatabase();
bool Connect(LPCTSTR path);
void Disconnect();
bool Execute(LPCTSTR sql);
// BLOB 전용 메서드
int FetchBlob(LPCTSTR query, CString& output);
bool StoreBlob(LPCTSTR query, int paramIndex,
const void* data, int length);
private:
sqlite3* m_hDB;
bool m_bConnected;
static int OnQueryResult(void*, int, char**, char**);
};
// DBManager.cpp
#include "StdAfx.h"
#include "DBManager.h"
CDatabase::CDatabase() : m_hDB(nullptr), m_bConnected(false) {}
CDatabase::~CDatabase()
{
Disconnect();
}
bool CDatabase::Connect(LPCTSTR path)
{
if (m_hDB) Disconnect();
USES_CONVERSION;
int rc = sqlite3_open(T2A(path), &m_hDB);
m_bConnected = (rc == SQLITE_OK);
return m_bConnected;
}
void CDatabase::Disconnect()
{
if (m_hDB) {
sqlite3_close(m_hDB);
m_hDB = nullptr;
}
m_bConnected = false;
}
bool CDatabase::Execute(LPCTSTR sql)
{
char* err = nullptr;
USES_CONVERSION;
int rc = sqlite3_exec(m_hDB, T2A(sql), OnQueryResult, 0, &err);
if (rc != SQLITE_OK && err) {
sqlite3_free(err);
return false;
}
return rc == SQLITE_OK;
}
int CDatabase::FetchBlob(LPCTSTR query, CString& output)
{
sqlite3_stmt* stmt = nullptr;
int totalBytes = 0;
USES_CONVERSION;
if (sqlite3_prepare_v2(m_hDB, T2A(query), -1, &stmt, nullptr) != SQLITE_OK)
return 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const void* blob = sqlite3_column_blob(stmt, 0);
int len = sqlite3_column_bytes(stmt, 0);
if (blob && len > 0) {
output.Append(CStringA((const char*)blob, len));
totalBytes += len;
}
}
sqlite3_finalize(stmt);
return totalBytes;
}
bool CDatabase::StoreBlob(LPCTSTR query, int paramIndex,
const void* data, int length)
{
sqlite3_stmt* stmt = nullptr;
USES_CONVERSION;
if (sqlite3_prepare_v2(m_hDB, T2A(query), -1, &stmt, nullptr) != SQLITE_OK)
return false;
sqlite3_bind_blob(stmt, paramIndex, data, length, SQLITE_STATIC);
bool ok = (sqlite3_step(stmt) == SQLITE_DONE);
sqlite3_finalize(stmt);
return ok;
}
int CDatabase::OnQueryResult(void*, int argc, char** argv, char**)
{
for (int i = 0; i < argc; i++) {
// 디버그용 콜백
}
return 0;
}
뷰-노드 동기화 구조
트리 항목과 뷰 인스턴스를 양방향으로 매핑하여 중복 생성을 방지합니다.
// NodeData.h
#pragma once
class CNoteView;
struct NodeInfo
{
int recordID;
CString displayName;
CNoteView* pActiveView;
NodeInfo() : recordID(0), pActiveView(nullptr) {}
};
서식 텍스트 뷰 구현
CRichEditView를 상속받아 데이터베이스 입출력 기능을 추가합니다. 스트림 콜백을 통해 RTF 형식으로 추출합니다.
// NoteView.h
#pragma once
#include "DBManager.h"
class CNoteView : public CRichEditView
{
DECLARE_DYNCREATE(CNoteView)
public:
CNoteView();
void LoadFromStorage();
void PersistToStorage();
protected:
CDatabase* m_pDataSource;
HTREEITEM m_hTreeNode;
CString ExtractRTF() const;
void InjectRTF(const CString& rtfData);
static DWORD CALLBACK StreamOutProc(DWORD_PTR cookie,
LPBYTE buffer, LONG count, LONG* transferred);
DECLARE_MESSAGE_MAP()
afx_msg int OnCreate(LPCREATESTRUCT lp);
afx_msg void OnInitialUpdate();
afx_msg void OnSaveToDB();
};
inline CString CNoteView::ExtractRTF() const
{
CString result;
EDITSTREAM es = { (DWORD_PTR)&result, 0, StreamOutProc };
GetRichEditCtrl().StreamOut(SF_RTF, es);
return result;
}
inline void CNoteView::InjectRTF(const CString& rtfData)
{
SetWindowText(rtfData);
}
// NoteView.cpp
#include "stdafx.h"
#include "NoteView.h"
#include "MainFrame.h"
#include "FilePanel.h"
IMPLEMENT_DYNCREATE(CNoteView, CRichEditView)
BEGIN_MESSAGE_MAP(CNoteView, CRichEditView)
ON_WM_CREATE()
ON_COMMAND(ID_DB_SAVE, &CNoteView::OnSaveToDB)
END_MESSAGE_MAP()
CNoteView::CNoteView() : m_pDataSource(nullptr), m_hTreeNode(nullptr) {}
int CNoteView::OnCreate(LPCREATESTRUCT lp)
{
if (CRichEditView::OnCreate(lp) == -1)
return -1;
return 0;
}
void CNoteView::OnInitialUpdate()
{
CRichEditView::OnInitialUpdate();
// 메인 프레임에서 파일 패널과 데이터베이스 참조 획득
CMainFrame* pFrame = static_cast<CMainFrame*>(AfxGetApp()->m_pMainWnd);
m_pDataSource = &pFrame->GetFilePanel()->m_dbEngine;
// 선택된 트리 노드와 연결
CTreeCtrl& tree = pFrame->GetFilePanel()->m_tree;
m_hTreeNode = tree.GetSelectedItem();
NodeInfo* pInfo = reinterpret_cast<NodeInfo*>(tree.GetItemData(m_hTreeNode));
if (pInfo) {
pInfo->pActiveView = this;
pFrame->SetWindowText(pInfo->displayName);
}
LoadFromStorage();
}
DWORD CALLBACK CNoteView::StreamOutProc(DWORD_PTR cookie,
LPBYTE buffer, LONG count, LONG* transferred)
{
CString* pCollector = reinterpret_cast<CString*>(cookie);
pCollector->Append(CStringA((LPCSTR)buffer, count));
*transferred = count;
return 0;
}
void CNoteView::LoadFromStorage()
{
if (!m_pDataSource) return;
NodeInfo* pInfo = GetNodeData();
if (!pInfo) return;
CString sql;
sql.Format(_T("SELECT data FROM notes WHERE id=%d"), pInfo->recordID);
CString rtfContent;
m_pDataSource->FetchBlob(sql, rtfContent);
if (!rtfContent.IsEmpty()) {
InjectRTF(rtfContent);
}
}
void CNoteView::PersistToStorage()
{
if (!m_pDataSource) return;
NodeInfo* pInfo = GetNodeData();
if (!pInfo) return;
CString rtfPayload = ExtractRTF();
LPCSTR rawData = CStringA(rtfPayload);
CString sql;
sql.Format(_T("UPDATE notes SET data=:blob WHERE id=%d"), pInfo->recordID);
m_pDataSource->StoreBlob(sql, 1, rawData, rtfPayload.GetLength());
}
void CNoteView::OnSaveToDB()
{
PersistToStorage();
}
NodeInfo* CNoteView::GetNodeData() const
{
// 트리 노드에서 사용자 데이터 획득
CMainFrame* pFrame = static_cast<CMainFrame*>(AfxGetApp()->m_pMainWnd);
CTreeCtrl& tree = pFrame->GetFilePanel()->m_tree;
return reinterpret_cast<NodeInfo*>(tree.GetItemData(m_hTreeNode));
}
RTF 데이터 름 요약
| 단계 | 동작 | API |
|---|---|---|
| 저장 | RichEdit → RTF 문자열 추출 | StreamOut(SF_RTF) |
| 저장 | UTF-8 변환 후 BLOB 바인딩 | sqlite3_bind_blob() |
| 로드 | BLOB 조회 및 CString 복원 | sqlite3_column_blob() |
| 로드 | RTF 문자열을 RichEdit에 주입 | SetWindowText() |
이 구조를 통해 여러 문서를 탭 또는 분할 창으로 편집하면서도 모든 서식 정보를 관계형 데이터베이스에 안정적으로 유지할 수 있습니다.