I found the solution — with low level keyboard hook it can be done, so the problem solved.
It is not known whether the game was launched from the disk or from the console, so first check whether you can connect the parent process to the console. There are many ways to detect whether a program has been launched from the console, but the simplest is to use the
AttachConsole function and check whether the operation was successful:
uses
Windows;
{...}
if AttachConsole(ATTACH_PARENT_PROCESS) then
// The program was launched from the console and managed to attach to it.
If it failed to attach to the console, it means that the program was either not launched from the console or the system was not able to attach it to it. If there is no console available, you need to create one — this is where the
AllocConsole function comes in:
if AllocConsole() then
// We managed to create a console for the program.
If the console cannot be created, the logs should be sent to a file. Now you need to inform RTL that the program uses the console and it must update the handles, including: to standard output. Two lines are enough:
IsConsole := True;
SysInitStdIO();
From now on, the process has a console at its disposal. To prevent the user from closing it and killing the process of our program, this option must be taken away from him. First, lock the button on the console window bar:
var
ConsoleHandle: HANDLE
ConsoleMenu: HMENU;
{...}
// Get the handle to the console window.
ConsoleHandle := GetConsoleWindow();
if ConsoleHandle <> 0 then
begin
// Get the console window system menu handle.
ConsoleMenu := GetSystemMenu(ConsoleHandle, False);
if ConsoleMenu <> 0 then
// Disable the close button.
EnableMenuItem(GetSystemMenu(ConsoleHandle, False), SC_CLOSE, MF_BYCOMMAND or MF_DISABLED or MF_GRAYED);
end;
You can still close the console with two keyboard shortcuts —
Ctrl+C and
Alt+F4. So, you should install a low-level keyboard hook — you can listen in on and block all keyboard input, as well as ”eat” anything you want (including single keys and keyboard shortcuts). First, you need to define a function that processes the input (our hook):
function ConsoleKeyboardHook(nCode: LongInt; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
This callback should be registered after successful allocation or connection to the console:
const
WH_KEYBOARD_LL = 13;
var
ConsoleHook: HHOOK;
{...}
ConsoleHook := SetWindowsHookEx(WH_KEYBOARD_LL, @ConsoleKeyboardHook, GetModuleHandle(nil), 0);
Then you need to program the listening function:
function ConsoleKeyboardHook(nCode: LongInt; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
type
KBDLLHOOKSTRUCT = record
vkCode: DWORD;
scanCode: DWORD;
flags: DWORD;
time: DWORD;
dwExtraInfo: ULONG_PTR;
end;
var
HookStruct: ^KBDLLHOOKSTRUCT absolute lParam;
begin
if nCode = HC_ACTION then
// If the console window is active, check the input, otherwise pass the data to the next hook.
if GetForegroundWindow() = ConsoleHandle then
begin
// If "Alt+F4" or "Ctrl+C" was pressed, return "1" — the input will not reach the console window (it will be eaten).
if (HookStruct^.vkCode = VK_F4) and (GetAsyncKeyState(VK_MENU) and $8000 <> 0) then exit(1);
if (HookStruct^.vkCode = VK_C) and (GetAsyncKeyState(VK_CONTROL) and $8000 <> 0) then exit(1);
end;
// Pass data to the next hook (extremely important).
Result := CallNextHookEx(ConsoleHook, nCode, wParam, lParam);
end;
From this moment on, the console will no longer be able to be closed, at least not in the normal way known to the user. Of course, the initialization of all this must go hand in hand with finalization. At the end of the program session, you need to clean up after yourself — unregister the keyboard hook, enable the system button for closing the console (if our program was attached to an existing one) or destroy the console (if it allocated the console itself). Manually destroying the console is not required, because the system will destroy it itself if our program was the last one attached to it.
// Unregister keyboard hook.
UnhookWindowsHookEx(ConsoleHook);
// Enable close button on the console title bar.
ConsoleMenu := GetSystemMenu(ConsoleHandle, False);
if ConsoleMenu <> 0 then
EnableMenuItem(GetSystemMenu(ConsoleHandle, False), SC_CLOSE, MF_BYCOMMAND or MF_ENABLED);
// Destroy the console (or detach it).
FreeConsole();
Well, that's pretty much it. In the case of my engine, after detecting an attempt to close the console, the hook function adds the event
SDL_QUITEVENT to the SDL queue. In the next frame of the game, the event processing function will encounter it and, if necessary, terminate the main game loop, thanks to which the game will be able to finalize its operation (save data and clean up resources).
If we use the above in a standard window application (created in LCL), the hook's callback can in any way inform the application about an attempt to close the console window — fire it
Application.Terminate, send it a
WM_CLOSE message or anything else. Of course, you can also eat these shortcuts and not inform the application at all — this way, the console will be able to close only after closing the main window of the application (the system will close the console or disconnect the application from it).
PS: I declared the constant
WH_KEYBOARD_LL and type
KBDLLHOOKSTRUCT manually because they are not declared in the
Windows module.