I'm making a (modal) dialog box with text input in C or C++ with just the Win32 API by itself (without MFC or such):
I have done it from scratch (see code below), and it works, but each time I add a new basic functionality and use it, I discover another missing feature which is nevertheless common in such dialog boxes with text inputs (present in so many software!):
Question: How, with pure Win32 API and C/C++, to create a dialog box with a text input, without reinventing the wheel + the endless list of little UI interactions we are used to?
Is there a built-in way in the Win32 API?
Practically, would someone have a minimal main.cpp
example, compilable with cl main.cpp /link user32.lib
or similar, achieving this without requiring a .rc
file, and without requiring an IDE?
Note: if using a .rc makes the code simpler, then let's use a .rc file. Here without .rc file the example Creating a Template in Memory looks unnecessarily complex.
My current code:
LRESULT CALLBACK EditWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CREATE:
hEdit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL | ES_LEFT, 10, 10, 580, 25, hWnd, (HMENU)ID_EDIT, NULL, NULL);
SetFocus(hEdit);
originalEditProc = (WNDPROC)GetWindowLongPtr(hEdit, GWLP_WNDPROC);
SetWindowLongPtr(hEdit, GWLP_WNDPROC, (LONG_PTR)EditWndProc);
CreateWindow("BUTTON", "OK", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 10, 50, 80, 30, hWnd, (HMENU)ID_OK, NULL, NULL);
CreateWindow("BUTTON", "Cancel", WS_CHILD | WS_VISIBLE, 100, 50, 80, 30, hWnd, (HMENU)ID_CANCEL, NULL, NULL);
break;
case WM_COMMAND: {
int wmId = LOWORD(wParam);
if (wmId == ID_OK) {
GetWindowText(hEdit, g_input, sizeof(g_input));
DestroyWindow(hWnd);
} else if (wmId == ID_CANCEL) {
g_input[0] = '\0';
DestroyWindow(hWnd);
}
break;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
LRESULT CALLBACK EditWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_KEYDOWN:
if (wParam == VK_RETURN) {
GetWindowText(hWnd, g_input, sizeof(g_input));
HWND parentWnd = GetParent(hWnd);
PostMessage(parentWnd, WM_COMMAND, ID_OK, 0);
return 0; // Prevent default processing (no newline in edit control)
}
break;
default:
if (originalEditProc) {
return CallWindowProc(originalEditProc, hWnd, message, wParam, lParam);
}
break;
}
return 0;
}
Thanks to @SimonMourier's example + modifications to add an Edit box, here is a minimal example, compilable with:
call "C:\path\to\vcvarsall.bat" x64
rc test.rc
cl test.cpp /DUNICODE /link user32.lib test.res
It has ARROW keys, CTRL+C / CTRL+X / CTRL+V, BACKSPACE, DEL, HOME, END keys, selection with SHIFT, right click context menu, TAB to cycle between elements, out of the box.
Notes:
The only missing common shortcut is CTRL+A (select all), I don't know if we can add this feature just by a flag in the RC file or if we have to handle this in WM_COMMAND
or WM_KEYDOWN
, feel free to edit this answer if you have an idea.
It seems that not using .rc files would make the solution less readable (see here).
resource.h
#define IDS_APP_TITLE 103
#define IDR_MAINFRAME 128
#define IDD_TEST_DIALOG 102
#define IDD_ABOUTBOX 103
#define IDM_ABOUT 104
#define IDM_EXIT 105
#define IDI_TEST 107
#define IDI_SMALL 108
#define IDC_TEST 109
#define IDC_MYEDIT 110
#define IDC_MYICON 2
#define IDC_STATIC -1
#define MAX_LOADSTRING 100
test.rc
#include <windows.h>
#include "resource.h"
LANGUAGE 9, 1
IDI_TEST ICON "test.ico"
IDC_TEST MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "E&xit", IDM_EXIT
END
POPUP "&Help"
BEGIN
MENUITEM "&About ...", IDM_ABOUT
END
END
IDC_TEST ACCELERATORS
BEGIN
"?", IDM_ABOUT, ASCII, ALT
"/", IDM_ABOUT, ASCII, ALT
END
IDD_ABOUTBOX DIALOGEX 0, 0, 235, 62
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About Test"
FONT 8, "MS Shell Dlg"
BEGIN
CONTROL "Edit Control", IDC_MYEDIT, "Edit", WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL | WS_TABSTOP, 14, 14, 200, 12
DEFPUSHBUTTON "OK",IDOK,113,41,50,14,WS_GROUP
PUSHBUTTON "Cancel",IDCANCEL,173,41,50,14,WS_GROUP
END
STRINGTABLE
BEGIN
IDC_TEST "TEST"
IDS_APP_TITLE "TEST"
END
test.cpp
#include <windows.h>
#include "resource.h"
HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING];
WCHAR szWindowClass[MAX_LOADSTRING];
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) {
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_TEST, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
if (!InitInstance(hInstance, nCmdShow))
return FALSE;
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TEST));
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int)msg.wParam;
}
ATOM MyRegisterClass(HINSTANCE hInstance) {
WNDCLASSEXW wcex{};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_TEST));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_TEST);
wcex.lpszClassName = szWindowClass;
return RegisterClassExW(&wcex);
}
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) {
hInst = hInstance;
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
return FALSE;
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
int wmId = LOWORD(wParam);
switch (message) {
case WM_COMMAND:
switch (wmId) {
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) {
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}