본문으로 건너뛰기
Previous
Next
Windows API로 텍스트 에디터 만들기 | 실전 프로젝트

Windows API로 텍스트 에디터 만들기 | 실전 프로젝트

Windows API로 텍스트 에디터 만들기 | 실전 프로젝트

이 글의 핵심

Windows API로 메모장 클론을 만듭니다. Edit 컨트롤로 텍스트 편집, CreateFile로 파일 저장/열기, FindText로 찾기, PRINTDLG로 인쇄를 구현합니다. 레지스트리로 최근 파일과 설정을 저장합니다.

들어가며

이 글에서는 Windows API만으로 완전한 텍스트 에디터를 만듭니다. Windows 메모장과 유사한 기능을 구현하며, 파일 I/O, 다이얼로그, 레지스트리까지 실전 기술을 종합합니다.

구현 기능

  • 파일 열기/저장/다른 이름으로 저장
  • 찾기/바꾸기
  • 폰트 변경
  • 인쇄
  • 최근 파일 목록 (레지스트리)
  • 상태 표시줄 (줄 번호, 인코딩)

1. 프로젝트 구조

TextEditor/
├─ resource.h        // 리소스 ID
├─ resource.rc       // 메뉴, 다이얼로그 리소스
├─ main.cpp          // WinMain, 메시지 루프
├─ editor.h          // Editor 클래스 헤더
├─ editor.cpp        // Editor 클래스 구현
└─ utils.h           // 유틸리티 함수

2. 리소스 정의 (resource.h)

#pragma once

// 메뉴 ID
#define IDM_FILE_NEW        101
#define IDM_FILE_OPEN       102
#define IDM_FILE_SAVE       103
#define IDM_FILE_SAVEAS     104
#define IDM_FILE_RECENT1    111
#define IDM_FILE_RECENT2    112
#define IDM_FILE_RECENT3    113
#define IDM_FILE_RECENT4    114
#define IDM_FILE_EXIT       120

#define IDM_EDIT_UNDO       201
#define IDM_EDIT_CUT        202
#define IDM_EDIT_COPY       203
#define IDM_EDIT_PASTE      204
#define IDM_EDIT_FIND       210
#define IDM_EDIT_REPLACE    211

#define IDM_FORMAT_FONT     301

#define IDM_HELP_ABOUT      401

// 컨트롤 ID
#define IDC_EDIT            1001
#define IDC_STATUSBAR       1002

3. 리소스 스크립트 (resource.rc)

#include "resource.h"
#include <windows.h>

IDR_MENU MENU
{
    POPUP "&File"
    {
        MENUITEM "&New\tCtrl+N", IDM_FILE_NEW
        MENUITEM "&Open...\tCtrl+O", IDM_FILE_OPEN
        MENUITEM "&Save\tCtrl+S", IDM_FILE_SAVE
        MENUITEM "Save &As...", IDM_FILE_SAVEAS
        MENUITEM SEPARATOR
        MENUITEM "Recent File 1", IDM_FILE_RECENT1
        MENUITEM "Recent File 2", IDM_FILE_RECENT2
        MENUITEM "Recent File 3", IDM_FILE_RECENT3
        MENUITEM "Recent File 4", IDM_FILE_RECENT4
        MENUITEM SEPARATOR
        MENUITEM "E&xit", IDM_FILE_EXIT
    }
    POPUP "&Edit"
    {
        MENUITEM "&Undo\tCtrl+Z", IDM_EDIT_UNDO
        MENUITEM SEPARATOR
        MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT
        MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY
        MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE
        MENUITEM SEPARATOR
        MENUITEM "&Find...\tCtrl+F", IDM_EDIT_FIND
        MENUITEM "&Replace...\tCtrl+H", IDM_EDIT_REPLACE
    }
    POPUP "F&ormat"
    {
        MENUITEM "&Font...", IDM_FORMAT_FONT
    }
    POPUP "&Help"
    {
        MENUITEM "&About", IDM_HELP_ABOUT
    }
}

IDR_ACCEL ACCELERATORS
{
    "N", IDM_FILE_NEW, CONTROL, VIRTKEY
    "O", IDM_FILE_OPEN, CONTROL, VIRTKEY
    "S", IDM_FILE_SAVE, CONTROL, VIRTKEY
    "Z", IDM_EDIT_UNDO, CONTROL, VIRTKEY
    "X", IDM_EDIT_CUT, CONTROL, VIRTKEY
    "C", IDM_EDIT_COPY, CONTROL, VIRTKEY
    "V", IDM_EDIT_PASTE, CONTROL, VIRTKEY
    "F", IDM_EDIT_FIND, CONTROL, VIRTKEY
    "H", IDM_EDIT_REPLACE, CONTROL, VIRTKEY
}

4. Editor 클래스 (editor.h)

#pragma once
#include <windows.h>
#include <string>
#include <vector>

class Editor
{
private:
    HWND m_hwnd;
    HWND m_hwndEdit;
    HWND m_hwndStatus;
    std::wstring m_currentFile;
    bool m_modified;
    HFONT m_hFont;
    
    std::vector<std::wstring> m_recentFiles;
    
    // 찾기/바꾸기
    FINDREPLACE m_fr;
    wchar_t m_findBuffer[256];
    wchar_t m_replaceBuffer[256];
    HWND m_hwndFindDlg;
    
public:
    Editor();
    ~Editor();
    
    bool Create(HINSTANCE hInstance, int nCmdShow);
    
    void OnCommand(WPARAM wParam);
    void OnSize(int width, int height);
    void OnClose();
    
    // 파일 작업
    void FileNew();
    void FileOpen();
    void FileSave();
    void FileSaveAs();
    void OpenRecentFile(int index);
    
    // 편집 작업
    void EditUndo();
    void EditCut();
    void EditCopy();
    void EditPaste();
    void EditFind();
    void EditReplace();
    
    // 서식
    void FormatFont();
    
    // 유틸리티
    void UpdateTitle();
    void UpdateStatusBar();
    void AddRecentFile(const std::wstring& path);
    void LoadRecentFiles();
    void SaveRecentFiles();
    
    bool PromptSaveChanges();
    
    HWND GetHWND() const { return m_hwnd; }
    
    static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
};

5. Editor 구현 (editor.cpp) - 일부

#include "editor.h"
#include "resource.h"
#include <commdlg.h>
#include <fstream>
#include <sstream>

Editor::Editor()
    : m_hwnd(NULL)
    , m_hwndEdit(NULL)
    , m_hwndStatus(NULL)
    , m_modified(false)
    , m_hFont(NULL)
    , m_hwndFindDlg(NULL)
{
    ZeroMemory(&m_fr, sizeof(m_fr));
    ZeroMemory(m_findBuffer, sizeof(m_findBuffer));
    ZeroMemory(m_replaceBuffer, sizeof(m_replaceBuffer));
}

Editor::~Editor()
{
    if (m_hFont) DeleteObject(m_hFont);
}

bool Editor::Create(HINSTANCE hInstance, int nCmdShow)
{
    // 윈도우 클래스 등록
    WNDCLASS wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = L"TextEditorClass";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU);
    
    if (!RegisterClass(&wc)) return false;
    
    // 메인 윈도우 생성
    m_hwnd = CreateWindowEx(
        0,
        L"TextEditorClass",
        L"Text Editor",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
        NULL, NULL, hInstance, this
    );
    
    if (!m_hwnd) return false;
    
    // Edit 컨트롤 생성
    m_hwndEdit = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        L"EDIT",
        L"",
        WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL |
        ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_NOHIDESEL,
        0, 0, 0, 0,
        m_hwnd,
        (HMENU)IDC_EDIT,
        hInstance,
        NULL
    );
    
    // 텍스트 크기 제한 해제
    SendMessage(m_hwndEdit, EM_LIMITTEXT, 0, 0);
    
    // 상태 표시줄
    m_hwndStatus = CreateWindowEx(
        0,
        STATUSCLASSNAME,
        NULL,
        WS_CHILD | WS_VISIBLE | SBARS_SIZEGRIP,
        0, 0, 0, 0,
        m_hwnd,
        (HMENU)IDC_STATUSBAR,
        hInstance,
        NULL
    );
    
    // 폰트 설정
    m_hFont = CreateFont(
        18, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
        DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
        CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
        L"Consolas"
    );
    SendMessage(m_hwndEdit, WM_SETFONT, (WPARAM)m_hFont, TRUE);
    
    // 최근 파일 로드
    LoadRecentFiles();
    
    ShowWindow(m_hwnd, nCmdShow);
    UpdateWindow(m_hwnd);
    
    SetFocus(m_hwndEdit);
    
    return true;
}

LRESULT CALLBACK Editor::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    Editor* pEditor = nullptr;
    
    if (msg == WM_CREATE) {
        CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam;
        pEditor = (Editor*)pCreate->lpCreateParams;
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pEditor);
    } else {
        pEditor = (Editor*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
    }
    
    if (pEditor) {
        switch (msg) {
            case WM_COMMAND:
                pEditor->OnCommand(wParam);
                return 0;
            case WM_SIZE:
                pEditor->OnSize(LOWORD(lParam), HIWORD(lParam));
                return 0;
            case WM_CLOSE:
                pEditor->OnClose();
                return 0;
            case WM_DESTROY:
                PostQuitMessage(0);
                return 0;
        }
    }
    
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

void Editor::OnCommand(WPARAM wParam)
{
    int wmId = LOWORD(wParam);
    int wmEvent = HIWORD(wParam);
    
    switch (wmId) {
        case IDM_FILE_NEW: FileNew(); break;
        case IDM_FILE_OPEN: FileOpen(); break;
        case IDM_FILE_SAVE: FileSave(); break;
        case IDM_FILE_SAVEAS: FileSaveAs(); break;
        case IDM_FILE_RECENT1: OpenRecentFile(0); break;
        case IDM_FILE_RECENT2: OpenRecentFile(1); break;
        case IDM_FILE_RECENT3: OpenRecentFile(2); break;
        case IDM_FILE_RECENT4: OpenRecentFile(3); break;
        case IDM_FILE_EXIT: PostMessage(m_hwnd, WM_CLOSE, 0, 0); break;
        
        case IDM_EDIT_UNDO: EditUndo(); break;
        case IDM_EDIT_CUT: EditCut(); break;
        case IDM_EDIT_COPY: EditCopy(); break;
        case IDM_EDIT_PASTE: EditPaste(); break;
        case IDM_EDIT_FIND: EditFind(); break;
        case IDM_EDIT_REPLACE: EditReplace(); break;
        
        case IDM_FORMAT_FONT: FormatFont(); break;
        
        case IDM_HELP_ABOUT:
            MessageBox(m_hwnd, L"Text Editor v1.0\nWindows API Project", L"About", MB_OK | MB_ICONINFORMATION);
            break;
    }
    
    // Edit 컨트롤 알림
    if (wmEvent == EN_CHANGE && LOWORD(wParam) == IDC_EDIT) {
        m_modified = true;
        UpdateTitle();
        UpdateStatusBar();
    }
}

void Editor::FileOpen()
{
    OPENFILENAME ofn = {};
    wchar_t szFile[MAX_PATH] = {};
    
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = m_hwnd;
    ofn.lpstrFile = szFile;
    ofn.nMaxFile = MAX_PATH;
    ofn.lpstrFilter = L"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0";
    ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
    
    if (GetOpenFileName(&ofn)) {
        // 파일 읽기
        HANDLE hFile = CreateFile(
            szFile,
            GENERIC_READ,
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL
        );
        
        if (hFile != INVALID_HANDLE_VALUE) {
            DWORD fileSize = GetFileSize(hFile, NULL);
            char* buffer = new char[fileSize + 1];
            
            DWORD bytesRead;
            ReadFile(hFile, buffer, fileSize, &bytesRead, NULL);
            buffer[bytesRead] = '\0';
            
            CloseHandle(hFile);
            
            // UTF-8 → Unicode 변환
            int wideSize = MultiByteToWideChar(CP_UTF8, 0, buffer, bytesRead, NULL, 0);
            wchar_t* wideBuffer = new wchar_t[wideSize + 1];
            MultiByteToWideChar(CP_UTF8, 0, buffer, bytesRead, wideBuffer, wideSize);
            wideBuffer[wideSize] = L'\0';
            
            SetWindowText(m_hwndEdit, wideBuffer);
            
            delete[] buffer;
            delete[] wideBuffer;
            
            m_currentFile = szFile;
            m_modified = false;
            UpdateTitle();
            AddRecentFile(m_currentFile);
        }
    }
}

void Editor::FileSave()
{
    if (m_currentFile.empty()) {
        FileSaveAs();
        return;
    }
    
    int textLength = GetWindowTextLength(m_hwndEdit);
    wchar_t* text = new wchar_t[textLength + 1];
    GetWindowText(m_hwndEdit, text, textLength + 1);
    
    // Unicode → UTF-8 변환
    int utf8Size = WideCharToMultiByte(CP_UTF8, 0, text, textLength, NULL, 0, NULL, NULL);
    char* utf8Buffer = new char[utf8Size + 1];
    WideCharToMultiByte(CP_UTF8, 0, text, textLength, utf8Buffer, utf8Size, NULL, NULL);
    
    HANDLE hFile = CreateFile(
        m_currentFile.c_str(),
        GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );
    
    if (hFile != INVALID_HANDLE_VALUE) {
        DWORD bytesWritten;
        WriteFile(hFile, utf8Buffer, utf8Size, &bytesWritten, NULL);
        CloseHandle(hFile);
        
        m_modified = false;
        UpdateTitle();
    }
    
    delete[] text;
    delete[] utf8Buffer;
}

void Editor::UpdateTitle()
{
    std::wstring title;
    
    if (m_currentFile.empty()) {
        title = L"Untitled";
    } else {
        size_t pos = m_currentFile.find_last_of(L"\\/");
        title = (pos != std::wstring::npos) ? m_currentFile.substr(pos + 1) : m_currentFile;
    }
    
    if (m_modified) {
        title += L" *";
    }
    
    title += L" - Text Editor";
    
    SetWindowText(m_hwnd, title.c_str());
}

void Editor::UpdateStatusBar()
{
    // 줄 번호, 컬럼 가져오기
    int lineIndex = (int)SendMessage(m_hwndEdit, EM_LINEFROMCHAR, -1, 0) + 1;
    int charIndex = (int)SendMessage(m_hwndEdit, EM_LINEINDEX, -1, 0);
    int currentPos = (int)SendMessage(m_hwndEdit, EM_GETSEL, 0, 0) & 0xFFFF;
    int column = currentPos - charIndex + 1;
    
    wchar_t status[256];
    swprintf_s(status, L"Ln %d, Col %d", lineIndex, column);
    
    SendMessage(m_hwndStatus, SB_SETTEXT, 0, (LPARAM)status);
}

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • Windows API 기초 | 메시지 루프와 윈도우 프로시저
  • Windows API 파일 I/O·레지스트리 | 파일 처리 완벽 가이드
  • C++ 프로젝트 구조 완벽 가이드

이 글이 도움이 되셨나요? Windows API로 완전한 애플리케이션을 만드는 데 도움이 되었기를 바랍니다!

다음 글에서는 MFC 채팅 프로그램을 다루겠습니다. 💬