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 채팅 프로그램을 다루겠습니다. 💬