2024-01-20 02:57:30 +00:00
|
|
|
/*
|
2024-01-20 03:07:31 +00:00
|
|
|
** Win32 Typing Test Program
|
|
|
|
** v1.0.0 - Jan 19th 2024
|
|
|
|
** by Allen Webster allenw@mr4th.com
|
|
|
|
**
|
|
|
|
** public domain example program
|
|
|
|
** NO WARRANTY IMPLIED; USE AT YOUR OWN RISK
|
|
|
|
**
|
|
|
|
**
|
|
|
|
** Tested on:
|
|
|
|
** Windows 10 English
|
|
|
|
**
|
|
|
|
** This is an example and a tool for gathering information about
|
|
|
|
** Win32 text input APIs.
|
|
|
|
**
|
|
|
|
** You may use it as a starting point for your own Win32 text input
|
|
|
|
** implementation.
|
|
|
|
**
|
|
|
|
** Or you may contribute to the project by:
|
|
|
|
** Trying typing in the test program and report any misbehavior you see.
|
|
|
|
** Supplying references to relevant documentation/examples that perform
|
|
|
|
** this task in a better way.
|
|
|
|
**
|
2024-01-20 02:57:30 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include <Windows.h>
|
|
|
|
|
|
|
|
// only for vsync in GDI
|
|
|
|
#include <dwmapi.h>
|
|
|
|
|
|
|
|
typedef LONG S32;
|
|
|
|
typedef USHORT U16;
|
|
|
|
typedef ULONG U32;
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////
|
|
|
|
// App Functions
|
|
|
|
|
|
|
|
static void AppInit(HWND wnd);
|
|
|
|
static void AppRender(HWND wnd);
|
|
|
|
static void AppTypeBackspace(void);
|
|
|
|
static void AppTypeCodepoint(U32 cp);
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////
|
|
|
|
// Window Proc
|
|
|
|
|
|
|
|
static BOOL keep_running = 1;
|
|
|
|
|
|
|
|
static LRESULT
|
|
|
|
WindowProc(HWND wnd,
|
|
|
|
UINT msg,
|
|
|
|
WPARAM wparam,
|
|
|
|
LPARAM lparam){
|
|
|
|
LRESULT result = 0;
|
|
|
|
|
|
|
|
switch (msg){
|
|
|
|
case WM_PAINT:
|
|
|
|
{
|
|
|
|
PAINTSTRUCT ps;
|
|
|
|
BeginPaint(wnd, &ps);
|
|
|
|
AppRender(wnd);
|
|
|
|
EndPaint(wnd, &ps);
|
|
|
|
}break;
|
|
|
|
|
|
|
|
case WM_CHAR:
|
|
|
|
{
|
|
|
|
// When a user uses IME, their input is handled by
|
|
|
|
// WM_IME_CHAR messages, but additional WM_CHAR messages
|
|
|
|
// get generated in the process.
|
|
|
|
//
|
|
|
|
// I'm not entirely sure what the best thing is to do with
|
|
|
|
// them - but they appear to always be messages I want to ignore.
|
|
|
|
//
|
|
|
|
// They also appear to be the only case where a WM_CHAR
|
|
|
|
// message has a scan code of zero.
|
|
|
|
//
|
|
|
|
// Therefore my best solution so far is to filter out WM_CHAR
|
|
|
|
// messages that have a zero scan code.
|
|
|
|
|
|
|
|
U32 scan_code = ((lparam >> 16) & 0xFF);
|
|
|
|
if (scan_code != 0){
|
|
|
|
U32 cp16 = (wparam & 0xFFFF);
|
|
|
|
if (cp16 >= ' ' && cp16 != 0x7F){
|
|
|
|
AppTypeCodepoint(cp16);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}break;
|
|
|
|
|
|
|
|
case WM_IME_CHAR:
|
|
|
|
{
|
|
|
|
U32 cp16 = (wparam & 0xFFFF);
|
|
|
|
if (cp16 >= ' ' && cp16 != 0x7F){
|
|
|
|
AppTypeCodepoint(cp16);
|
|
|
|
}
|
|
|
|
}break;
|
|
|
|
|
|
|
|
case WM_UNICHAR:
|
|
|
|
{
|
|
|
|
U32 cp32 = wparam;
|
|
|
|
if (cp32 >= ' ' && cp32 != 0x7F){
|
|
|
|
AppTypeCodepoint(cp32);
|
|
|
|
}
|
|
|
|
}break;
|
|
|
|
|
|
|
|
case WM_KEYDOWN:
|
|
|
|
{
|
|
|
|
// While it is possible to see newlines/carriage returns
|
|
|
|
// and tabs through the WM_CHAR message, it is also possible
|
|
|
|
// to get erroneous newlines and tabs in WM_CHAR.
|
|
|
|
//
|
|
|
|
// "Erroneous" WM_CHAR messages are generated when a user
|
|
|
|
// holds down "Ctrl" and presses any alphabetic character.
|
|
|
|
// The erroneous values generated this way are always less
|
|
|
|
// than 0x20 (0x20 is the ascii for the space character ' ').
|
|
|
|
//
|
|
|
|
// Thus it's possible to get simple behavior by ignoring
|
|
|
|
// all the WM_CHAR messages that have character values less
|
|
|
|
// than 0x20 and then directly handle these two relevant
|
|
|
|
// cases here.
|
|
|
|
|
|
|
|
if (wparam == VK_RETURN){
|
|
|
|
AppTypeCodepoint('\n');
|
|
|
|
}
|
|
|
|
if (wparam == VK_TAB){
|
|
|
|
AppTypeCodepoint('\t');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (wparam == VK_BACK){
|
|
|
|
AppTypeBackspace();
|
|
|
|
}
|
|
|
|
}break;
|
|
|
|
|
|
|
|
case WM_SYSCOMMAND:
|
|
|
|
{
|
|
|
|
// Not directly relevant to the main goal, but if you don't
|
|
|
|
// want your Alt+Key combos to play a bell sound you have to
|
|
|
|
// disable default handling of this 'menu' path
|
|
|
|
|
|
|
|
BOOL is_menu = (wparam == SC_KEYMENU && (lparam >> 16) <= 0);
|
|
|
|
if (!is_menu){
|
|
|
|
result = DefWindowProcW(wnd, msg, wparam, lparam);
|
|
|
|
}
|
|
|
|
}break;
|
|
|
|
|
|
|
|
case WM_CLOSE:
|
|
|
|
{
|
|
|
|
keep_running = 0;
|
|
|
|
}break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
{
|
|
|
|
result = DefWindowProcW(wnd, msg, wparam, lparam);
|
|
|
|
}break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////
|
|
|
|
// App Implementation
|
|
|
|
|
|
|
|
static HFONT font = 0;
|
|
|
|
|
|
|
|
#define TEXT_CAP (1 << 14)
|
|
|
|
static U16 text_memory[TEXT_CAP];
|
|
|
|
static U32 text_len = 0;
|
|
|
|
|
|
|
|
static void
|
|
|
|
AppInsertCodepoint(U32 cp){
|
|
|
|
// convert codepoint to UTF16
|
|
|
|
U16 encode_buf[2];
|
|
|
|
U32 encode_len = 0;
|
|
|
|
if (cp < 0x10000){
|
|
|
|
encode_buf[0] = cp;
|
|
|
|
encode_len = 1;
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
U32 cpj = cp - 0x10000;
|
|
|
|
encode_buf[0] = (cpj >> 10) + 0xD800;
|
|
|
|
encode_buf[1] = (cpj & 0x3FF) + 0xDC00;
|
|
|
|
encode_len = 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
// insert into string
|
|
|
|
if (text_len + encode_len < TEXT_CAP){
|
|
|
|
for (U32 i = 0; i < encode_len; i += 1){
|
|
|
|
text_memory[text_len] = encode_buf[i];
|
|
|
|
text_len += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
AppTypeBackspace(void){
|
|
|
|
text_len = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
AppTypeCodepoint(U32 cp){
|
|
|
|
if (cp == '\n'){
|
|
|
|
AppInsertCodepoint('\\');
|
|
|
|
AppInsertCodepoint('n');
|
|
|
|
}
|
|
|
|
else if (cp == '\t'){
|
|
|
|
AppInsertCodepoint('\\');
|
|
|
|
AppInsertCodepoint('t');
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
AppInsertCodepoint(cp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
AppInit(HWND wnd){
|
|
|
|
LOGFONT logfont = {0};
|
|
|
|
logfont.lfHeight = 24;
|
|
|
|
memcpy(logfont.lfFaceName, "Arial", sizeof("Arial"));
|
|
|
|
font = CreateFontIndirectA(&logfont);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
AppRender(HWND wnd){
|
|
|
|
|
|
|
|
// window dimensions
|
|
|
|
RECT wnd_rect = {0};
|
|
|
|
GetClientRect(wnd, &wnd_rect);
|
|
|
|
|
|
|
|
// render begin
|
|
|
|
HDC wnd_dc = GetDC(wnd);
|
|
|
|
HDC dc = CreateCompatibleDC(wnd_dc);
|
|
|
|
HBITMAP bitmap =
|
|
|
|
CreateCompatibleBitmap(wnd_dc, wnd_rect.right, wnd_rect.bottom);
|
|
|
|
SelectObject(dc, bitmap);
|
|
|
|
|
|
|
|
// render body
|
|
|
|
{
|
|
|
|
SelectObject(dc, GetStockObject(DC_PEN));
|
|
|
|
SelectObject(dc, GetStockObject(DC_BRUSH));
|
|
|
|
SetBkMode(dc, TRANSPARENT);
|
|
|
|
SelectObject(dc, font);
|
|
|
|
|
|
|
|
// background
|
|
|
|
{
|
|
|
|
SetDCPenColor(dc, RGB(255, 255, 255));
|
|
|
|
SetDCBrushColor(dc, RGB(255, 255, 255));
|
|
|
|
Rectangle(dc, wnd_rect.left, wnd_rect.top, wnd_rect.right, wnd_rect.bottom);
|
|
|
|
}
|
|
|
|
|
|
|
|
// string
|
|
|
|
{
|
|
|
|
SIZE dim = {0};
|
|
|
|
GetTextExtentPoint32W(dc, (WCHAR*)text_memory, text_len, &dim);
|
|
|
|
|
|
|
|
S32 cx = (wnd_rect.right - wnd_rect.left)/2;
|
|
|
|
S32 cy = (wnd_rect.bottom - wnd_rect.top)/2;
|
|
|
|
|
|
|
|
S32 x = cx - dim.cx/2;
|
|
|
|
S32 y = cy - dim.cy/2;
|
|
|
|
|
|
|
|
TextOutW(dc, x, y, (WCHAR*)text_memory, text_len);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// render end
|
|
|
|
BitBlt(wnd_dc, 0, 0, wnd_rect.right, wnd_rect.bottom, dc, 0, 0, SRCCOPY);
|
|
|
|
DeleteObject(bitmap);
|
|
|
|
DeleteDC(dc);
|
|
|
|
ReleaseDC(wnd, wnd_dc);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////
|
|
|
|
// Main
|
|
|
|
|
|
|
|
static BOOL composition_enabled = 0;
|
|
|
|
|
|
|
|
int
|
|
|
|
WinMain(HINSTANCE hInstance,
|
|
|
|
HINSTANCE hPrevInstance,
|
|
|
|
LPSTR lpCmdLine,
|
|
|
|
int nShowCmd){
|
|
|
|
// window setup
|
|
|
|
#define WINDOW_CLASS L"InputWindowClass"
|
|
|
|
|
|
|
|
WNDCLASSW window_class = {0};
|
|
|
|
window_class.style = CS_HREDRAW|CS_VREDRAW;
|
|
|
|
window_class.lpfnWndProc = WindowProc;
|
|
|
|
window_class.hInstance = hInstance;
|
|
|
|
window_class.hCursor = LoadCursorA(0, IDC_ARROW);
|
|
|
|
window_class.lpszClassName = WINDOW_CLASS;
|
|
|
|
|
|
|
|
ATOM atom = RegisterClassW(&window_class);
|
|
|
|
if (atom == 0){
|
|
|
|
MessageBoxW(0, (WCHAR*)L"RegisterClassW failed\n", (WCHAR*)L"Fatal", MB_OK);
|
|
|
|
ExitProcess(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
DWORD style = WS_OVERLAPPEDWINDOW;
|
|
|
|
HWND wnd = CreateWindowW(WINDOW_CLASS, L"Type Test", style,
|
|
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
|
|
0, 0, hInstance, 0);
|
|
|
|
|
|
|
|
// setup vsync
|
|
|
|
if (DwmIsCompositionEnabled(&composition_enabled) != S_OK){
|
|
|
|
composition_enabled = 0;
|
|
|
|
}
|
|
|
|
if (!composition_enabled){
|
|
|
|
MessageBoxW(0, (WCHAR*)L"No Vsync\n", (WCHAR*)L"Issue", MB_OK);
|
|
|
|
}
|
|
|
|
|
|
|
|
// init
|
|
|
|
AppInit(wnd);
|
|
|
|
|
|
|
|
// main loop
|
|
|
|
ShowWindow(wnd, SW_SHOW);
|
|
|
|
|
|
|
|
keep_running = 1;
|
|
|
|
for (;keep_running;){
|
|
|
|
|
|
|
|
// flush message queue
|
|
|
|
{
|
|
|
|
BOOL wait_for_message = 1;
|
|
|
|
MSG msg;
|
|
|
|
for (;(wait_for_message?
|
|
|
|
GetMessageW(&msg, 0, 0, 0):
|
|
|
|
PeekMessageW(&msg, 0, 0, 0, PM_REMOVE));){
|
|
|
|
wait_for_message = 0;
|
|
|
|
TranslateMessage(&msg);
|
|
|
|
DispatchMessage(&msg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// render
|
|
|
|
AppRender(wnd);
|
|
|
|
|
|
|
|
// vsync or sleep
|
|
|
|
if (composition_enabled){
|
|
|
|
DwmFlush();
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
Sleep(33);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//$ graphical //
|