Zablokowanie możliwości zamknięcia dynamicznie zaalokowanej konsoli systemowej

Zablokowanie możliwości zamknięcia dynamicznie zaalokowanej konsoli systemowej
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 4 godziny
  • Lokalizacja:Tuchów
  • Postów:12167
0

Przepisuję właśnie system logów w swoim silniku. Jeśli gra zostanie uruchomiona z określonym parametrem, silnik alokuje systemowe okno konsoli, za pomocą funkcji AllocConsole. Potem blokuję systemowy przycisk do zamykania okna — funkcja DeleteMenu robi robotę. Aby skrót Ctrl+C nie ubijał konsoli i przy okazji gry, dodaję pusty handler funkcją SetConsoleCtrlHandler. Wszystko gra, te opcje stają się niedostępne.

Pozostała jeszcze jedna rzecz — blokowanie skrótu Alt+F4. Ma ktoś pojęcie jak ten skrót zablokować, tak aby nie dało się w żaden sposób zamknąć dynamicznie utworzonego okna konsoli? Chcę, aby konsola była zamykana wyłącznie po zamknięciu okna gry.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 2x, ostatnio: flowCRANE
flowCRANE
Jutro sprawdzę czy da się podmienić procedurę okna i obsłużyć WM_CLOSE, dziś już nie mam siły.
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 4 godziny
  • Lokalizacja:Tuchów
  • Postów:12167
0

Dobra, sprawdziłem z procedurą okna i dupa — nie da się subclassować konsoli, każda próba rejestracji własnej procedury okna kończy się niepowodzeniem. Nie da się tego zrobić, bo konsola jest kontrolowana przez zewnętrzny proces — csrss.exe.


To czego potrzebuję to po prostu sposobu na wykrycie próby zamknięcia okna konsoli (przyciskiem, skrótami klawiszowymi) i jej zablokowanie, tak aby nie została ona zamknięta. To co trzeba zrobić w przypadku takiej próby to ją odrzucić, a następnie poinformować kod silnika, aby odpowiednio na taką próbę zareagował i albo pozwolił zamknąć grę, albo nie.

Potrzebuję tego z dwóch powodów. Po pierwsze, silnik musi obsłużyć zamknięcie na swój sposób, czyli przerwać główną pętlę gry i sfinalizować swoje działanie, czyli przede wszystkim zapisać ważne dane. Może też taką próbę zignorować, aby gra nie została zamknięta. Po drugie, zamknięcie konsoli musi być blokowane, bo jeśli konsola zostanie zamknięta, to proces gry wali błędem, co mnie wkurza:

screenshot-20240303112910.png

Tak samo to wygląda w przypadku, gdy stworzy się normalną aplikację okienkową w LCL, ale ustawi się {$APPTYPE CONSOLE} (albo przełącznik -WG). Zamknięcie okna konsoli np. krzyżykiem powoduje dokładnie taki sam błąd.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
Manna5
  • Rejestracja:prawie 6 lat
  • Ostatnio:dzień
  • Lokalizacja:Kraków
  • Postów:639
0

Zgaduję że to nie jest nic interaktywnego w tej konsoli, tylko wyświetlasz tam informacje do debugowania. Okna konsoli zostały stworzone dla aplikacji konsolowych, dlatego też na przykład aplikacja może mieć tylko jedną konsolę. Może zrób po prostu drugie okno graficzne i wyświetlaj w nim tekst na zasadzie konsoli, proste przewijanie też nie powinno być trudne. Może nawet jest do tego jakaś biblioteka.


edytowany 1x, ostatnio: Manna5
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 4 godziny
  • Lokalizacja:Tuchów
  • Postów:12167
4

@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
  // Program został odpalony z poziomu konsoli i udało się pod nią podczepić.

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
  // Udało się stworzyć konsolę dla programu.

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;

{...}

// Pobierz uchwyt okna konsoli.
ConsoleHandle := GetConsoleWindow();

if ConsoleHandle <> 0 then
begin
  // Pobierz uchwyt systemowego menu okna konsoli.
  ConsoleMenu := GetSystemMenu(ConsoleHandle, False);

  if ConsoleMenu <> 0 then
    // Zablokuj przycisk do zamykania okna (z krzyżykiem).
    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
    // Jeśli aktywne jest okno konsoli, sprawdź input, w przeciwnym razie przekaż dane do kolejnego hooka.
    if GetForegroundWindow() = ConsoleHandle then
    begin
      // Jeśli wciśnięto skrót "Alt+F4" lub "Ctrl+C", zwróć "1" — input nie dotrze do okna konsoli.
      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;

  // Przekaż dane do kolejnego hooka (niezwykle istotne).
  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
// Wyrejestruj hook na klawiaturę.
UnhookWindowsHookEx(ConsoleHook);

// Odblokuj z powrotem przycisk zamykania konsoli, w razie gdyby system jej nie zniszczył.
ConsoleMenu := GetSystemMenu(ConsoleHandle, False);

if ConsoleMenu <> 0 then
  EnableMenuItem(GetSystemMenu(ConsoleHandle, False), SC_CLOSE, MF_BYCOMMAND or MF_ENABLED);

// Zniszcz konsolę. Jeśli inny proces się pod nią podłączył, konsola nie zostanie zamknięta, dlatego
// ważne jest to, aby odblokować systemowy przycisk zamykania konsoli.
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.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 9x, ostatnio: flowCRANE
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)