[WinAPI] Edit Control에서 Enter키 입력 받기
WinAPI의 Edit control은 사용자의 입력을 받는 컨트롤 중 하나입니다.
때때로는 설계 방식에 따라, 이 입력 도중 Enter키를 통해 특정 상호작용을 해야 할 때가 있습니다.
이번 글에서는 그에 대해 다루어 보도록 하겠습니다.
1. UI 설정
글에서 목표하는 바는 Edit control에서 Enter키가 개행 이외의 방법으로 작동 되게 하는 것 입니다.
이를 위해 간단하게 Edit Control이 있는 UI를 만들어 보도록 하겠습니다.
코드 및 형태는 아래와 같습니다.
#include <stdlib.h>
#include <string.h>
#include <tchar.h>
#include <string>
#include <Windows.h>
#define ID_EDIT 100
static TCHAR szWindowClass[] = _T("DesktopApp");
static TCHAR szTitle[] = _T("Simple APP");
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HWND hEdit;
int WINAPI WinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
) {
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(wcex.hInstance, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION);
if (!RegisterClassEx(&wcex)) {
MessageBox(NULL,
_T("Call to RegisterClassEx failed!"),
_T("Simple APP"),
NULL);
return 1;
}
HWND hWnd = CreateWindowEx(
WS_EX_OVERLAPPEDWINDOW,
szWindowClass,
szTitle,
WS_OVERLAPPEDWINDOW,
10, 10,
310, 510,
NULL,
NULL,
hInstance,
NULL
);
if (!hWnd) {
MessageBox(NULL,
_T("Call to CreateWindow failed!"),
_T("Windows Desktop Guided Tour"),
NULL);
return 1;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_CREATE:
hEdit = CreateWindowEx(
0, L"EDIT",
NULL,
WS_CHILD | WS_VISIBLE | WS_BORDER |
ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN,
10, 410,
270, 50,
hWnd,
(HMENU)ID_EDIT,
(HINSTANCE)GetWindowLongPtr(hWnd, GWLP_HINSTANCE),
NULL);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

위 실행 화면 하단부의 Edit control에서 사용자의 입력을 받고, Enter키가 입력되었을 경우 특정 행동을 하도록 (이 글에서는 화면 상단에 기록이 남도록)해 보도록 하겠습니다.
2. Enter 키 반응
Edit control에 포커스가 있는 동안에는 특정 키 입력이 되지 않습니다.
이는 Edit control에 추가적인 Procedure를 설정하는 것으로 해결할 수 있습니다.
기본 UI 코드가 긴 관계로, 추가되는 코드만 작성하도록 하겠습니다.
WNDPROC DefEditProc;
LRESULT CALLBACK SubEditProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_CREATE:
DefEditProc = (WNDPROC)SetWindowLongPtr(hEdit, GWLP_WNDPROC, (LONG_PTR)SubEditProc);
...
}
...
}
LRESULT CALLBACK SubEditProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
HDC hdc;
PAINTSTRUCT ps;
RECT rt = { 10, 10, 100, 100 };
switch (uMsg) {
case WM_KEYDOWN:
switch (wParam) {
case VK_RETURN:
//
// Code
//
break;
}
break;
default:
return CallWindowProc(DefEditProc, hWnd, uMsg, wParam, lParam);
}
return FALSE;
}
Edit control을 생성하고, Edit control에 대하여 적용되는 추가적인 Procedure를 정의한 모습입니다.
Enter키에 해당하는 파라미터인 VK_RETURN에 대하여 행동을 정의하는 것으로, Enter키 입력을 받을 수 있습니다.
3. Not Ctrl + Enter
Enter키를 입력으로 받는 몇몇 프로그램의 경우, Enter키를 조합하여 사용하는 키(Alt, Ctrl 등)에 대하여는 Enter키와 다른 기능을 하는 경우가 대부분입니다.
이 경우는, Enter키 입력을 처리하기 전에 다른 키가 중복되어 입력되어 있는지 확인하는 것으로 구현할 수 있습니다.
switch (wParam) {
case VK_RETURN:
if (!GetAsyncKeyState(VK_CONTROL)) {
//
// Code
//
}
break;
}
GetAsyncKeyState는 인자로 받은 키의 입력 여부를 반환합니다.
위 코드는 부정 연산자를 사용함으로써, Ctrl이 눌리지 않았을 경우의 Enter 입력에 대하여 작동하는 코드입니다.
위 문단에서 정의한 Edit control에 ES_MULTILINE옵션은 Edit control에 대한 여러 줄 입력을 허용하는 옵션입니다.
위와 같은 코드를 작성함으로써, Enter키에 대하여는 특정 동작을 수행하고, Ctrl + Enter에 대하여는 개행을 할 수 있도록 구현할 수 있습니다.
4. Write text
위 문단의 내용을 통해 Enter키에 대해 특정 행동을 할 수 있는 조건문을 만들었습니다.
이제 이 조건문 내부에 특정 행동을 정의하면 됩니다.
이 글에서는 첫 문단의 내용대로 입력한 텍스트를 지우고, 상단에 출력하도록 하는 코드를 작성하도록 하겠습니다.
HDC hdc;
RECT rt;
if (!GetAsyncKeyState(VK_CONTROL)) {
hdc = GetDC(GetParent(hEdit));
GetClientRect(GetParent(hEdit), &rt);
TCHAR buff[1024];
GetWindowText(hEdit, buff, 1024);
SetWindowText(hEdit, 0);
DrawText(hdc, buff, -1, &rt, DT_CENTER);
ReleaseDC(GetParent(hEdit), hdc);
}
구현하는 방법은 여러가지가 있겠지만, 위와 같은 방식으로 Edit control의 텍스트를 지우고 이용할 수 있습니다.
5. Issue : CRLF
위 문단의 코드에는 아쉬운 문제점이 하나 있습니다.
Enter키를 입력해서 텍스트를 지우게 될 때, 개행문자는 지워지지 않는 문제가 있습니다.
이 문제의 원인은 개행 문자의 입력과 조건문 사이의 서순이 원인입니다.
깔끔한 해답은 찾지 못 했지만, 제가 찾은 두 가지 해결책을 공유하도록 하겠습니다.
첫 번째 방법은 서순을 변경하는 것 입니다.
이것은 Procedure의 Message를 변경하는 것으로 간단하게 해결 가능합니다.
// Before
switch (uMsg) {
case WM_KEYDOWN:
...
break;
...
}
// After
switch (uMsg) {
case WM_KEYUP:
...
break;
...
}
위 문단의 WM_KEYDOWN을 WM_KEYUP으로 변경하면 입력 이후 개행 문자가 남아있는 문제를 해결할 수 있습니다.
하지만, KEYUP 이전에 개행 문자가 삽입되고, 입력 기능이 동작하기 전에 추가적인 키보드 입력이 발생할 수 있다는 점에서 시각적, 기능적으로 깔끔하지 못 한 경향이 있습니다.
두 번째 방법은 Edit control을 새로 생성하는 것 입니다.
#define MY_CLEAR (WM_USER + 1)
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
...
case MY_CLEAR:
//RemovePropW(hEdit, 0);
SendMessage(hEdit, WM_CLOSE, 0, 0);
hEdit = CreateWindowEx(
0, L"EDIT", // predefined class
NULL, // no window title
WS_CHILD | WS_VISIBLE | WS_BORDER |
ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL,
10, 410,
270, 50, // set size in WM_SIZE message
hWnd, // parent window
(HMENU)ID_EDIT, // edit control ID
(HINSTANCE)GetWindowLongPtr(hWnd, GWLP_HINSTANCE),
NULL); // pointer not needed
DefEditProc = (WNDPROC)SetWindowLongPtr(hEdit, GWLP_WNDPROC, (LONG_PTR)SubEditProc);
SetFocus(hEdit);
break;
...
}
}
LRESULT CALLBACK SubEditProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
...
case WM_KEYDOWN:
switch (wParam) {
case VK_RETURN:
if (!GetAsyncKeyState(VK_CONTROL)) {
...
...
SendMessage(GetParent(hEdit), MY_CLEAR, 0, 0);
}
}
...
}
}
Edit control을 새로 생성하는 메시지를 위한 커스텀 메시지를 정의합니다.
WndProc에 커스텀 메시지를 수신할 경우 Edit control을 제거하고, 새 Edit control을 생성하는 코드를 작성합니다.
이 때, 입력 커서가 사라질 경우 자연스럽지 못 하므로 SetFocus를 통해 커서를 유지시킵니다.
위 기능을 Enter키 입력 동작의 마지막에 호출하는 것으로 개행 문자를 제거할 수 있습니다.
이 구현은 시각적으로는 깔끔할 수 있으나, 사용자가 입력할 때 마다 Control을 새로 생성해야 하므로 성능적인 이슈가 발생할 수 있습니다.
두 방법 모두 명확한 단점이 존재하기 때문에 사용하는 것에 부담감이 있습니다.
이후 더 나은 구현법을 찾을 경우 추가로 작성하도록 하겠습니다.
Win32를 다루는 것이 이번이 처음이라, 구조적인 부분에서 이해도가 많이 부족합니다.
그렇지만 제가 겪은 이슈에 대하여 이후에 참고하거나, 공유하고자 이 글을 작성합니다.
이 글이 지금 겪고계신 이슈와 관련이 있고, 도움이 되길 바랍니다.
감사합니다.