Skip to content

Instantly share code, notes, and snippets.

@delfigamer
Created December 3, 2023 23:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save delfigamer/6532ce6bc717e1fddb0994519aa8b580 to your computer and use it in GitHub Desktop.
Save delfigamer/6532ce6bc717e1fddb0994519aa8b580 to your computer and use it in GitHub Desktop.
Examples of using WM_CHAR for text input in a Win32 application, a simple and a partially IME-aware version
#define WIN32_LEAN_AND_MEAN
#define _CRT_SECURE_NO_WARNINGS
#define _SCL_SECURE_NO_WARNINGS
#include <Windows.h>
#include <cstdio>
#include <utility>
#include <string>
#include <vector>
struct TextEdit {
std::wstring buffer;
int caret = 0;
wchar_t const* Buffer() {
return buffer.data();
}
int Length() {
return buffer.size();
}
void NormalizeCaret() {
if (caret < 0) {
caret = 0;
} else if (caret > Length()) {
caret = buffer.size();
}
}
void Insert(wchar_t ch) {
NormalizeCaret();
buffer.insert(caret, 1, ch);
caret += 1;
}
void Insert(std::wstring const& str) {
NormalizeCaret();
if (!str.empty()) {
buffer.insert(caret, str);
caret += str.size();
}
}
void EraseLeft() {
NormalizeCaret();
if (caret > 0) {
buffer.erase(caret - 1, 1);
caret -= 1;
}
}
void EraseRight() {
NormalizeCaret();
if (caret < Length()) {
buffer.erase(caret, 1);
}
}
void MoveLeft() {
if (caret > 0) {
caret -= 1;
}
}
void MoveRight() {
if (caret < Length()) {
caret += 1;
}
}
void MoveHome() {
caret = 0;
}
void MoveEnd() {
caret = buffer.size();
}
};
TextEdit Current;
HPEN CaretPen;
void WindowPaint(HWND hwnd) {
RECT clientrect;
GetClientRect(hwnd, &clientrect);
SIZE wndsize = {clientrect.right - clientrect.left, clientrect.bottom - clientrect.top};
PAINTSTRUCT ps;
HDC wnddc = BeginPaint(hwnd, &ps);
HBITMAP bitmap = CreateCompatibleBitmap(wnddc, wndsize.cx, wndsize.cy);
HDC dc = CreateCompatibleDC(wnddc);
SelectObject(dc, bitmap);
RECT wndrect = {0, 0, wndsize.cx, wndsize.cy};
HBRUSH brush = CreateSolidBrush(RGB(255, 255, 255));
FillRect(dc, &wndrect, brush);
TEXTMETRICW textmetric;
GetTextMetricsW(dc, &textmetric);
int currentx = textmetric.tmHeight;
int currenty = textmetric.tmHeight;
SIZE textsize;
TextOutW(dc, currentx, currenty, Current.Buffer(), Current.Length());
GetTextExtentPointW(dc, Current.Buffer(), Current.caret, &textsize);
SelectObject(dc, CaretPen);
MoveToEx(dc, currentx + textsize.cx, currenty, nullptr);
LineTo(dc, currentx + textsize.cx, currenty + textmetric.tmHeight);
BitBlt(wnddc, 0, 0, wndsize.cx, wndsize.cy, dc, 0, 0, SRCCOPY);
DeleteObject(brush);
DeleteDC(dc);
DeleteObject(bitmap);
EndPaint(hwnd, &ps);
}
LRESULT CALLBACK WindowProc(
HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam
) {
switch (uMsg) {
case WM_CREATE:
CaretPen = CreatePen(PS_SOLID, 0, RGB(0, 0, 0));
return 0;
case WM_DESTROY:
DeleteObject(CaretPen);
PostQuitMessage(0);
return 0;
case WM_CHAR:
wprintf(L"WM_CHAR %#4llx'%lc' %3lli\n", wParam, (wchar_t)wParam, (lParam >> 16) & 0xff);
if (wParam == 8) {
Current.EraseLeft();
} else if (wParam >= 32) {
Current.Insert(wParam);
}
InvalidateRect(hwnd, nullptr, false);
return 0;
case WM_KEYDOWN:
wprintf(L"WM_KEYDOWN %4lli %3lli\n", wParam, (lParam >> 16) & 0xff);
if (wParam == VK_LEFT) {
Current.MoveLeft();
InvalidateRect(hwnd, nullptr, false);
} else if (wParam == VK_RIGHT) {
Current.MoveRight();
InvalidateRect(hwnd, nullptr, false);
} else if (wParam == VK_HOME || wParam == VK_PRIOR || wParam == VK_UP) {
Current.MoveHome();
InvalidateRect(hwnd, nullptr, false);
} else if (wParam == VK_END || wParam == VK_NEXT || wParam == VK_DOWN) {
Current.MoveEnd();
InvalidateRect(hwnd, nullptr, false);
} else if (wParam == VK_DELETE) {
Current.EraseRight();
InvalidateRect(hwnd, nullptr, false);
}
return 0;
case WM_KEYUP:
return 0;
case WM_PAINT:
WindowPaint(hwnd);
return 0;
default:
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
}
int CALLBACK WinMain(
HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
AllocConsole();
freopen("CONOUT$", "w", stdout);
const DWORD WindowStyle = WS_OVERLAPPEDWINDOW;
WNDCLASSW wndclass;
wndclass.style = CS_VREDRAW | CS_HREDRAW;
wndclass.lpfnWndProc = &WindowProc;
wndclass.cbClsExtra = 8;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(0, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(0, IDC_ARROW);
wndclass.hbrBackground = 0;
wndclass.lpszMenuName = 0;
wndclass.lpszClassName = L"MyWindowClass";
ATOM wndclassatom = RegisterClassW(&wndclass);
RECT WindowRect = {0, 0, 800, 600};
AdjustWindowRect(
&WindowRect, WindowStyle, false);
HWND hwnd = CreateWindowW(
(LPCWSTR)wndclassatom,
L"window",
WindowStyle,
20,
20,
WindowRect.right - WindowRect.left,
WindowRect.bottom - WindowRect.top,
0,
0,
hInstance,
0);
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
MSG message = {0, 0, 0, 0};
while (GetMessageW(&message, nullptr, 0, 0)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
return 0;
};
#define WIN32_LEAN_AND_MEAN
#define _CRT_SECURE_NO_WARNINGS
#define _SCL_SECURE_NO_WARNINGS
#include <Windows.h>
#include <cstdio>
#include <utility>
#include <string>
#include <vector>
struct TextEdit
{
std::wstring buffer;
int caret = 0;
wchar_t const* Buffer()
{
return buffer.data();
}
int Length()
{
return buffer.size();
}
void NormalizeCaret()
{
if (caret < 0) {
caret = 0;
} else if (caret > Length()) {
caret = buffer.size();
}
}
void Insert(wchar_t ch)
{
NormalizeCaret();
buffer.insert(caret, 1, ch);
caret += 1;
}
void Insert(std::wstring const& str)
{
NormalizeCaret();
if (!str.empty()) {
buffer.insert(caret, str);
caret += str.size();
}
}
void EraseLeft()
{
NormalizeCaret();
if (caret > 0) {
buffer.erase(caret - 1, 1);
caret -= 1;
}
}
void EraseRight()
{
NormalizeCaret();
if (caret < Length()) {
buffer.erase(caret);
}
}
void MoveLeft()
{
if (caret > 0) {
caret -= 1;
}
}
void MoveRight()
{
if (caret < Length()) {
caret += 1;
}
}
void MoveHome()
{
caret = 0;
}
void MoveEnd()
{
caret = buffer.size();
}
};
enum class CompositionAttribute
{
Input,
Selected,
Converted,
};
struct CompositionClause
{
std::wstring text;
CompositionAttribute attr;
};
struct CompositionState
{
std::vector<CompositionClause> clauses;
int cursorclause;
int cursorpos;
std::wstring UpdateAndGetResult(HWND hwnd)
{
cursorclause = 0;
cursorpos = 0;
HIMC imc = ImmGetContext(hwnd);
std::wstring result;
result.resize(ImmGetCompositionStringW(imc, GCS_RESULTSTR, nullptr, 0) / 2);
if (!result.empty()) {
ImmGetCompositionStringW(imc, GCS_RESULTSTR, (wchar_t*)result.data(), result.size() * 2);
}
std::vector<wchar_t> str;
std::vector<BYTE> attr;
std::vector<DWORD> pos;
str.resize(ImmGetCompositionStringW(imc, GCS_COMPSTR, nullptr, 0) / 2);
if (str.empty()) {
clauses.clear();
}
ImmGetCompositionStringW(imc, GCS_COMPSTR, str.data(), str.size() * 2);
attr.resize(ImmGetCompositionStringW(imc, GCS_COMPATTR, nullptr, 0));
ImmGetCompositionStringW(imc, GCS_COMPATTR, attr.data(), attr.size());
pos.resize(ImmGetCompositionStringW(imc, GCS_COMPCLAUSE, nullptr, 0) / 4);
ImmGetCompositionStringW(imc, GCS_COMPCLAUSE, pos.data(), pos.size() * 4);
if (pos.size() < 1) {
return result;
}
clauses.resize(pos.size() - 1);
for (int i = 0; i < (int)clauses.size(); ++i) {
int begin = pos[i];
int end = pos[i + 1];
if (begin >= 0 && end <= (int)str.size() && begin < end) {
clauses[i].text.resize(end - begin);
std::copy(str.data() + begin, str.data() + end, (wchar_t*)clauses[i].text.data());
} else {
clauses[i].text.clear();
}
clauses[i].attr = CompositionAttribute::Input;
if (begin >= 0 && begin < (int)attr.size()) {
switch (attr[begin]) {
case ATTR_CONVERTED:
clauses[i].attr = CompositionAttribute::Converted;
break;
case ATTR_TARGET_CONVERTED:
clauses[i].attr = CompositionAttribute::Selected;
break;
}
}
}
int cursor = ImmGetCompositionStringW(imc, GCS_CURSORPOS, nullptr, 0) & 0xffff;
for (int i = 0; i < (int)clauses.size(); ++i) {
int length = clauses[i].text.size();
if (cursor >= 0 && cursor <= length) {
cursorclause = i;
cursorpos = cursor;
break;
} else {
cursor -= length;
}
}
ImmReleaseContext(hwnd, imc);
return result;
}
};
TextEdit Current;
CompositionState CurrentComposition;
HPEN CaretPen;
HPEN InputAttrPen;
HPEN SelectedAttrPen;
HPEN ConvertedAttrPen;
void WindowPaint(HWND hwnd)
{
RECT clientrect;
GetClientRect(hwnd, &clientrect);
SIZE wndsize = { clientrect.right - clientrect.left, clientrect.bottom - clientrect.top };
PAINTSTRUCT ps;
HDC wnddc = BeginPaint(hwnd, &ps);
HBITMAP bitmap = CreateCompatibleBitmap(wnddc, wndsize.cx, wndsize.cy);
HDC dc = CreateCompatibleDC(wnddc);
SelectObject(dc, bitmap);
RECT wndrect = { 0, 0, wndsize.cx, wndsize.cy };
HBRUSH brush = CreateSolidBrush(RGB(255, 255, 255));
FillRect(dc, &wndrect, brush);
TEXTMETRICW textmetric;
GetTextMetricsW(dc, &textmetric);
int currentx = textmetric.tmHeight;
int currenty = textmetric.tmHeight;
SIZE textsize;
if (CurrentComposition.clauses.empty()) {
TextOutW(dc, currentx, currenty, Current.Buffer(), Current.Length());
GetTextExtentPointW(dc, Current.Buffer(), Current.caret, &textsize);
SelectObject(dc, CaretPen);
MoveToEx(dc, currentx + textsize.cx, currenty, nullptr);
LineTo(dc, currentx + textsize.cx, currenty + textmetric.tmHeight);
} else {
TextOutW(dc, currentx, currenty, Current.Buffer(), Current.caret);
GetTextExtentPointW(dc, Current.Buffer(), Current.caret, &textsize);
currentx += textsize.cx;
for (int i = 0; i < (int)CurrentComposition.clauses.size(); ++i) {
CompositionClause const& clause = CurrentComposition.clauses[i];
TextOutW(dc, currentx, currenty, clause.text.data(), clause.text.size());
if (CurrentComposition.cursorclause == i) {
GetTextExtentPointW(dc, clause.text.data(), CurrentComposition.cursorpos, &textsize);
SelectObject(dc, CaretPen);
MoveToEx(dc, currentx + textsize.cx, currenty, nullptr);
LineTo(dc, currentx + textsize.cx, currenty + textmetric.tmHeight);
}
GetTextExtentPointW(dc, clause.text.data(), clause.text.size(), &textsize);
switch (clause.attr) {
case CompositionAttribute::Input:
SelectObject(dc, InputAttrPen);
break;
case CompositionAttribute::Selected:
SelectObject(dc, SelectedAttrPen);
break;
case CompositionAttribute::Converted:
SelectObject(dc, ConvertedAttrPen);
break;
}
MoveToEx(dc, currentx + 1, currenty + textmetric.tmHeight + 1, nullptr);
LineTo(dc, currentx + textsize.cx - 1, currenty + textmetric.tmHeight + 1);
currentx += textsize.cx;
}
TextOutW(dc, currentx, currenty, Current.Buffer() + Current.caret, Current.Length() - Current.caret);
}
BitBlt(wnddc, 0, 0, wndsize.cx, wndsize.cy, dc, 0, 0, SRCCOPY);
DeleteObject(brush);
DeleteDC(dc);
DeleteObject(bitmap);
EndPaint(hwnd, &ps);
}
void ImrQueryCharPosition(HWND hwnd, IMECHARPOSITION* chpos)
{
int charpos = chpos->dwCharPos;
wprintf(L"WM_IME_REQUEST IMR_QUERYCHARPOSITION %i\n", charpos);
CurrentComposition.UpdateAndGetResult(hwnd);
RECT clientrect;
GetClientRect(hwnd, &clientrect);
HDC dc = GetDC(hwnd);
TEXTMETRICW textmetric;
GetTextMetricsW(dc, &textmetric);
int currentx = textmetric.tmHeight;
int currenty = textmetric.tmHeight;
SIZE textsize;
GetTextExtentPointW(dc, Current.Buffer(), Current.caret, &textsize);
currentx += textsize.cx;
for (int i = 0; i < (int)CurrentComposition.clauses.size(); ++i) {
CompositionClause const& clause = CurrentComposition.clauses[i];
if (charpos < (int)clause.text.size()) {
GetTextExtentPointW(dc, clause.text.data(), charpos, &textsize);
currentx += textsize.cx;
break;
} else {
GetTextExtentPointW(dc, clause.text.data(), clause.text.size(), &textsize);
currentx += textsize.cx;
charpos -= (int)clause.text.size();
}
}
POINT pt = { currentx, currenty + 5 };
ClientToScreen(hwnd, &pt);
chpos->pt = pt;
chpos->cLineHeight = textmetric.tmHeight;
POINT client1 = { 0, 0 };
POINT client2 = { clientrect.right - clientrect.left, currenty + textmetric.tmHeight };
ClientToScreen(hwnd, &client1);
ClientToScreen(hwnd, &client2);
chpos->rcDocument = { client1.x, client1.y, client2.x, client2.y };
ReleaseDC(hwnd, dc);
}
LRESULT CALLBACK WindowProc(
HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg) {
case WM_CREATE:
CaretPen = CreatePen(PS_SOLID, 0, RGB(0, 0, 0));
InputAttrPen = CreatePen(PS_DOT, 0, RGB(0, 0, 0));
SelectedAttrPen = CreatePen(PS_SOLID, 2, RGB(0, 0, 0));
ConvertedAttrPen = CreatePen(PS_SOLID, 0, RGB(0, 0, 0));
return 0;
case WM_DESTROY:
DeleteObject(CaretPen);
DeleteObject(InputAttrPen);
DeleteObject(SelectedAttrPen);
DeleteObject(ConvertedAttrPen);
PostQuitMessage(0);
return 0;
case WM_CHAR:
wprintf(L"WM_CHAR %#4x'%lc' %3i\n", wParam, (wchar_t)wParam, (lParam >> 16) & 0xff);
if (wParam == 8) {
Current.EraseLeft();
} else if (wParam >= 32) {
Current.Insert(wParam);
}
InvalidateRect(hwnd, nullptr, false);
return 0;
case WM_KEYDOWN:
wprintf(L"WM_KEYDOWN %4i %3i\n", wParam, (lParam >> 16) & 0xff);
if (wParam == VK_LEFT) {
Current.MoveLeft();
InvalidateRect(hwnd, nullptr, false);
} else if (wParam == VK_RIGHT) {
Current.MoveRight();
InvalidateRect(hwnd, nullptr, false);
} else if (wParam == VK_HOME || wParam == VK_PRIOR || wParam == VK_UP) {
Current.MoveHome();
InvalidateRect(hwnd, nullptr, false);
} else if (wParam == VK_END || wParam == VK_NEXT || wParam == VK_DOWN) {
Current.MoveEnd();
InvalidateRect(hwnd, nullptr, false);
} else if (wParam == VK_DELETE) {
Current.EraseRight();
InvalidateRect(hwnd, nullptr, false);
}
return 0;
case WM_KEYUP:
return 0;
case WM_IME_SETCONTEXT:
wprintf(L"WM_IME_SETCONTEXT %i\n", wParam);
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
case WM_IME_STARTCOMPOSITION:
wprintf(L"WM_IME_STARTCOMPOSITION\n");
Current.Insert(CurrentComposition.UpdateAndGetResult(hwnd));
InvalidateRect(hwnd, nullptr, false);
return 0;
case WM_IME_COMPOSITION:
wprintf(L"WM_IME_COMPOSITION\n");
Current.Insert(CurrentComposition.UpdateAndGetResult(hwnd));
InvalidateRect(hwnd, nullptr, false);
return 0;
case WM_IME_ENDCOMPOSITION:
wprintf(L"WM_IME_ENDCOMPOSITION\n");
CurrentComposition.clauses.clear();
InvalidateRect(hwnd, nullptr, false);
return 0;
case WM_IME_SELECT:
wprintf(L"WM_IME_SELECT %i\n", wParam);
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
case WM_IME_REQUEST:
switch (wParam) {
case IMR_CANDIDATEWINDOW:
wprintf(L"WM_IME_REQUEST IMR_CANDIDATEWINDOW\n");
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
case IMR_COMPOSITIONFONT:
wprintf(L"WM_IME_REQUEST IMR_COMPOSITIONFONT\n");
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
case IMR_COMPOSITIONWINDOW:
wprintf(L"WM_IME_REQUEST IMR_COMPOSITIONWINDOW\n");
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
case IMR_CONFIRMRECONVERTSTRING:
wprintf(L"WM_IME_REQUEST IMR_CONFIRMRECONVERTSTRING\n");
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
case IMR_DOCUMENTFEED:
wprintf(L"WM_IME_REQUEST IMR_DOCUMENTFEED\n");
return 0;
case IMR_QUERYCHARPOSITION:
ImrQueryCharPosition(hwnd, (IMECHARPOSITION*)lParam);
return true;
case IMR_RECONVERTSTRING:
wprintf(L"WM_IME_REQUEST IMR_RECONVERTSTRING\n");
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
default:
wprintf(L"WM_IME_REQUEST %i\n", wParam);
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
case WM_IME_NOTIFY:
switch (wParam) {
case IMN_CHANGECANDIDATE:
wprintf(L"WM_IME_NOTIFY IMN_CHANGECANDIDATE\n");
return 0;
case IMN_CLOSECANDIDATE:
wprintf(L"WM_IME_NOTIFY IMN_CLOSECANDIDATE\n");
return 0;
case IMN_CLOSESTATUSWINDOW:
wprintf(L"WM_IME_NOTIFY IMN_CLOSESTATUSWINDOW\n");
return 0;
case IMN_GUIDELINE:
wprintf(L"WM_IME_NOTIFY IMN_GUIDELINE\n");
return 0;
case IMN_OPENCANDIDATE:
wprintf(L"WM_IME_NOTIFY IMN_OPENCANDIDATE\n");
return 0;
case IMN_OPENSTATUSWINDOW:
wprintf(L"WM_IME_NOTIFY IMN_OPENSTATUSWINDOW\n");
return 0;
case IMN_SETCANDIDATEPOS:
wprintf(L"WM_IME_NOTIFY IMN_SETCANDIDATEPOS\n");
return 0;
case IMN_SETCOMPOSITIONFONT:
wprintf(L"WM_IME_NOTIFY IMN_SETCOMPOSITIONFONT\n");
return 0;
case IMN_SETCOMPOSITIONWINDOW:
wprintf(L"WM_IME_NOTIFY IMN_SETCOMPOSITIONWINDOW\n");
return 0;
case IMN_SETCONVERSIONMODE:
wprintf(L"WM_IME_NOTIFY IMN_SETCONVERSIONMODE\n");
return 0;
case IMN_SETOPENSTATUS:
wprintf(L"WM_IME_NOTIFY IMN_SETOPENSTATUS\n");
return 0;
case IMN_SETSENTENCEMODE:
wprintf(L"WM_IME_NOTIFY IMN_SETSENTENCEMODE\n");
return 0;
case IMN_SETSTATUSWINDOWPOS:
wprintf(L"WM_IME_NOTIFY IMN_SETSTATUSWINDOWPOS\n");
return 0;
default:
wprintf(L"WM_IME_NOTIFY %i\n", wParam);
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
case WM_PAINT:
WindowPaint(hwnd);
return 0;
default:
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
}
int CALLBACK WinMain(
HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
AllocConsole();
freopen("CONOUT$", "w", stdout);
const DWORD WindowStyle = WS_OVERLAPPEDWINDOW;
WNDCLASSW wndclass;
wndclass.style = CS_VREDRAW | CS_HREDRAW;
wndclass.lpfnWndProc = &WindowProc;
wndclass.cbClsExtra = 8;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(0, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(0, IDC_ARROW);
wndclass.hbrBackground = 0;
wndclass.lpszMenuName = 0;
wndclass.lpszClassName = L"MyWindowClass";
ATOM wndclassatom = RegisterClassW(&wndclass);
RECT WindowRect = { 0, 0, 800, 600 };
AdjustWindowRect(
&WindowRect, WindowStyle, false);
HWND hwnd = CreateWindowW(
(LPCWSTR)wndclassatom,
L"window",
WindowStyle,
20,
20,
WindowRect.right - WindowRect.left,
WindowRect.bottom - WindowRect.top,
0,
0,
hInstance,
0);
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
MSG message = { 0, 0, 0, 0 };
while (GetMessageW(&message, nullptr, 0, 0)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
return 0;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment