@Manna5: musi być wsparcie systemowej konsoli. Po pierwsze dlatego, że ona już istnieje, ma wiele ficzerów itd., więc nie muszę jej implementować ręcznie. Po drugie dlatego, że system logów silnika zapisuje dane na standardowe wyjście za pomocą WriteLn
, a tym może być konsola lub plik dyskowy. Po trzecie dlatego, że użytkownik może odpalić grę z poziomu konsoli oraz określić, że logi mają być kierowane właśnie do konsoli, a nie do pliku.
Takie rozwiązanie ładnie współpracuje z systemem i daje dużo więcej możliwości. 
Znalazłem rozwiązanie — blokadę na skróty klawiszowe Ctrl+C oraz Alt+F4 (i dowolne inne) należy założyć za pomocą niskopoziomowego hooka na klawiaturę. Microsoft przygotował na ten temat artykuł — Disabling Shortcut Keys in Games. Na nim się wzorowałem, choć aby hook faktycznie działał, nie należy testować wParam
. Niżej opiszę szczegółowo o tym jak to wykonałem, w razie gdyby ktoś kiedyś tego potrzebował.
Nie wiadomo czy gra została odpalona z dysku czy z poziomu konsoli, więc najpierw należy sprawdzić, czy na się podpiąć pod konsolę procesu-rodzica. Jest wiele sposobów na wykrycie tego czy program został odpalony z poziomu konsoli, ale najprostszy to użycie funkcji AttachConsole i sprawdzenie czy operacja się powiodła:
Kopiuj
uses
Windows;
if AttachConsole(ATTACH_PARENT_PROCESS) then
Jeśli nie udało się podczepić pod konsolę, to znaczy, że program albo nie został uruchomiony z poziomu konsoli, albo system nie umiał go do niej podczepić. Skoro nie ma dostępnej konsoli, to należy ją stworzyć — tutaj wjeżdża funkcja AllocConsole:
Kopiuj
if AllocConsole() then
Jeśli nie uda się stworzyć konsoli, to logi kierujemy do pliku. Albo do d**y — uchwyt standardowego wyjścia jest zainicjalizowany, więc wszystko co się wypisze za pomocą WriteLn
do niego trafi, a że na nic sensownego nie jest ustawiony, to po prostu te dane przepadną.
Natomiast jeśli konsolę uda się stworzyć, należy poinformować RTL Free Pascala, że program używa konsoli i trzeba zaktualizować uchwyty, m.in. do standardowego wyjścia. Wystarczą dwie linijki:
Kopiuj
IsConsole := True;
SysInitStdIO();
Od teraz proces ma konsolę do dyspozycji. Aby użytkownik nie mógł jej zamknąć i zabić procesu naszego programu, trzeba mu taką możliwość odebrać. Najpierw należy zablokować przycisk na belce okna konsoli:
Kopiuj
var
ConsoleHandle: HANDLE
ConsoleMenu: HMENU;
ConsoleHandle := GetConsoleWindow();
if ConsoleHandle <> 0 then
begin
ConsoleMenu := GetSystemMenu(ConsoleHandle, False);
if ConsoleMenu <> 0 then
EnableMenuItem(GetSystemMenu(ConsoleHandle, False), SC_CLOSE, MF_BYCOMMAND or MF_DISABLED or MF_GRAYED);
end;
Przyciskiem nie da się zamknąć konsoli, bo stał się wyszarzony — nie ma też opcji Close
w menu kontekstowym belki okna. Ale konsolę można jeszcze zamknąć dwoma skrótami klawiszowymi — Ctrl+C oraz Alt+F4. Ten pierwszy można zablokować poprzez ustawienie pustego handlera, za pomocą funkcji SetConsoleCtrlHandler:
Kopiuj
SetConsoleCtrlHandler(nil, True);
Ale to tylko jeden skrót, a trzeba zablokować dwa. Zamiast powyższego, należy założyć low level keyboard hook — w nim można podsłuchiwać i blokować cały input z klawiatury, a także ”zjadać” wszystko co chcemy (w tym pojedyncze klawisze oraz skróty klawiszowe).
Najpierw należy zdefiniować funkcję przetwarzającą input (nasz hook):
Kopiuj
function ConsoleKeyboardHook(nCode: LongInt; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
Ten callback należy zarejestrować, po udanej alokacji lub podłączeniu się pod konsolę:
Kopiuj
const
WH_KEYBOARD_LL = 13;
var
ConsoleHook: HHOOK;
ConsoleHook := SetWindowsHookEx(WH_KEYBOARD_LL, @ConsoleKeyboardHook, GetModuleHandle(nil), 0);
Jeśli system zarejestruje nasz hook to spoko, a jeśli nie to trudno — ta funkcja nie będzie wywoływana. Sam niczego nie potrzebuję robić w przypadku, gdy rejestracja hooka się nie powiedzie, więc to tyle. Następnie należy oprogramować funkcję podsłuchującą:
Kopiuj
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 GetForegroundWindow() = ConsoleHandle then
begin
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;
Result := CallNextHookEx(ConsoleHook, nCode, wParam, lParam);
end;
W tej funkcji nie należy testować wParam
przeciwko WM_KEYDOWN
i WM_KEYUP
(tak jak w artykule od Microsoftu), bo w przypadku tych skrótów klawiszowych, wParam
tych wartości nie zawiera i blokada skrótów będzie nieskuteczna. Bardzo istotne jest to, aby ten callback wywoływał CallNextHookEx
dla każdego innego inputu, którego nie blokujemy — w przeciwnym razie nasz hook zablokuje input w całym systemie. 
Od tego momentu konsoli nie będzie się dało nijak zamknąć, w przynajmniej nie w normalny, znany użytkownikowi sposób. Oczywiście inicjalizacja tego wszystkiego musi iść w parze z finalizacją. Na koniec sesji programu trzeba po sobie posprzątać — wyrejestrować hook na klawiaturę, odblokować systemowy przycisk do zamykania konsoli (jeśli nasz program podłączył się pod istniejącą) lub zniszczyć konsolę (jeśli sam ją alokował). Ręczne niszczenie konsoli nie jest wymagane, dlatego że system sam ją zniszczy, jeśli nasz program był ostatnim do niej podłączonym.
Kopiuj
UnhookWindowsHookEx(ConsoleHook);
ConsoleMenu := GetSystemMenu(ConsoleHandle, False);
if ConsoleMenu <> 0 then
EnableMenuItem(GetSystemMenu(ConsoleHandle, False), SC_CLOSE, MF_BYCOMMAND or MF_ENABLED);
FreeConsole();
No i to w sumie tyle. W przypadku mojego silnika, po wykryciu próby ubicia konsoli, funkcja hooka dodaje zdarzenie SDL_QUITEVENT
do kolejki SDL-a. W kolejnej klatce gry, funkcja przetwarzająca zdarzenia SDL-a napotka na SDL_QUITEVENT
i w razie czego zakończy działanie głównej pętli gry, dzięki czemu gra będzie mogła sfinalizować swoje działanie (zapisać dane oraz sprzątnąć zasoby).
Jeśli powyższego używamy w standardowej aplikacji okienkowej (stworzonej w LCL), w callbacku hooka można w dowolny sposób poinformować aplikację o próbie jego zamknięcia — odpalić Application.Terminate
, wysłać jej komunikat WM_CLOSE
lub cokolwiek innego. Oczywiście można też zeżreć te skróty i nie informować aplikacji — w ten sposób konsolę będzie można zamknąć tylko po zamknięciu głównego okna programu (system zamknie konsolę lub odłączy od niej naszą aplikację, jeśli ta podłączyła się pod konsolę, zamiast ją alokować).
PS: stałą WH_KEYBOARD_LL
oraz typ KBDLLHOOKSTRUCT
deklarowałem ręcznie, bo nie ma ich w module Windows
.
WM_CLOSE
, dziś już nie mam siły.