MFC에서 SQLite BLOB을 활용한 서식 텍스트 영구 저장

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()

이 구조를 통해 여러 문서를 탭 또는 분할 창으로 편집하면서도 모든 서식 정보를 관계형 데이터베이스에 안정적으로 유지할 수 있습니다.

태그: MFC SQLite BLOB CRichEditView RTF

6월 28일 20:42에 게시됨