You've probably noticed how easy it is to turn your Windows console into a PRINT-only message board in your GUI applications, but how difficult it really is to redirect your interactive keyboard input there while keeping your GUI windows still alive and functioning.
The console's keyboard input loop blocks single threaded applications and their window message pumps become unresponsive to system message flow. This is immediately reported by the system with a "program not responding" message and the application terminates abnormally. Similarly, a GUI window message pump (particularly its TranslateMessage() calls) leaves no chance for the console to get through to the user keystrokes interactively.
There are several ways to resolve such a stalemate. One of them is to set up your own loop that would monitor the state of console input buffer in real time and prevent its attempts to seize the keyboard input exclusively. It will also allow the app windows to receive their messages regularly enough not to arouse any suspition on behalf of the system as to whether the app is more dead than alive.
That's exactly what the following script does. It allows you to interact with your graphics windows without restriction but whenever the user presses a key, the focus is switched immediately to the console and the keypress is echoed there as usual. The script is only a skeleton and it doesn't offer full functionality out of the box but it identifies the key points for further development.
It is interesting that neither OxygenBasic nor FBSL can adequately reproduce the WinAPI structures INPUT_RECORD and KEY_EVENT_RECORD that are needed for interaction with the console input buffer. Their proper C language implementation must be having some weird alignment that O2's PACKED and FBSL's ALIGN 1/2/4/8/etc. directives can't cope with. Yet I managed to set up my own KEY_RECORD equivalent to these structures by trial and error so that the script is equally functional in O2 and FBSL's BASIC. FBSL's DynC would utilize the original C header files so it could use the original INPUT_RECORD/KEY_EVENT_RECORD structures where appropriate.
Note that you can't close the console or GUI window by clicking their [X] buttons. That's the simplest way to prevent uncontrolled app termination by a careless user action.
INCLUDEPATH "$/inc/"
'$FILENAME "NBC.exe"
'INCLUDE "rtl32.inc"
INCLUDE "MinWin.inc"
INCLUDE "Console.inc"
INDEXBASE 0
#LOOKAHEAD
! SUB DeleteMenu LIB "user32.dll" (DWORD hMenu, DWORD uPosition, DWORD uFlags)
! FUNCTION IsWindowVisible LIB "user32.dll" (DWORD hwnd) AS LONG
! FUNCTION PeekConsoleInputA LIB "kernel32.dll" (DWORD hConsoleInput, DWORD lpBuffer, DWORD nLength, DWORD lpNumberOfEventsRead) AS LONG
! SUB FlushConsoleInputBuffer LIB "kernel32.dll" (DWORD hConsoleInput)
! SUB RtlZeroMemory LIB "kernel32.dll" (DWORD ptr, DWORD size)
! SUB WriteConsoleInputA LIB "kernel32.dll" (DWORD hConsoleInput, DWORD lpBuffer, DWORD nLength, DWORD lpNumberOfEventsWritten)
! FUNCTION GetConsoleWindow LIB "kernel32.dll" () AS DWORD
PACKED TYPE KEY_RECORD
EventType AS WORD
wReserved AS WORD
bKeyDown AS DWORD
wRepeatCount AS WORD
wVirtualKeyCode AS WORD
wScanCode AS WORD
wAsciiCode AS WORD
dwReserved AS DWORD
END TYPE
DIM cmdline AS ASCIIZ PTR, inst AS SYS, hwnd AS DWORD
&cmdline = GetCommandLine()
inst = GetModuleHandle(0)
' ===================================================================
WinMain inst, 0, cmdline, SW_SHOW
SetConsoleTitle ":: Non-Blocking Oxygen Console Demo :: (F1 shows GUI, Esc quits)"
DIM AS DWORD hcons = GetConsoleWindow() ' console hwnd
DIM AS DWORD hstdin = GetStdHandle(STD_INPUT_HANDLE) ' input buffer handle
DeleteMenu GetSystemMenu(hwnd, 0), SC_CLOSE, MF_BYCOMMAND ' disable close buttons
DeleteMenu GetSystemMenu(hcons, 0), SC_CLOSE, MF_BYCOMMAND
MainLoop()
END
' ===================================================================
SUB MainLoop()
CONST MAXRECORD = 8
DIM IR[MAXRECORD] AS KEY_RECORD ' read 8 records max
DIM AS LONG i, read ' number of records actually read
DIM w AS WORD ' virtual keycode
DIM c AS BYTE ' key ASCII
DO ' main program loop
PeekConsoleInputA hstdin, @IR[0], 8, @read
FOR i = 0 TO < MAXRECORD
IF IR[i].EventType = 1 THEN ' KEY_EVENT
IF IR[i].bKeyDown THEN
w = IR[i].wVirtualKeyCode
c = IR[i].wAsciiCode
SELECT w ' add navigation subs here
CASE VK_UP
CASE VK_DOWN
CASE VK_LEFT
CASE VK_RIGHT
CASE VK_END
CASE VK_HOME
CASE VK_INSERT
CASE VK_DELETE
CASE VK_F1
IF NOT IsWindowVisible(hwnd) THEN
ShowWindow hwnd, SW_SHOWNOACTIVATE
END IF
EXIT FOR ' don't fall thru to print
CASE ELSE
END SELECT
SELECT c ' add backspace/enter subs here
CASE 0 TO 7 ' unprintables
CASE 8 ' BACKSPACE currently suppressed
CASE 13 ' ENTER currently suppressed
CASE 27 ' ESCAPE
IF IsWindowVisible(hwnd) THEN
ShowWindow hwnd, SW_HIDE
ELSE
EXIT SUB ' quit
END IF
CASE ELSE ' printables
PRINT CHR(c)
END SELECT
EXIT FOR
END IF
END IF
NEXT
FlushConsoleInputBuffer hstdin
RtlZeroMemory &IR[0], MAXRECORD * SIZEOF(KEY_RECORD)
DoEvents
Sleep 10
END DO
END SUB
SUB DoEvents()
SYS bRet
MSG wm
STATIC bSwitchToConsole AS LONG
IF PeekMessage(@wm, hwnd, 256, 264, PM_NOREMOVE) THEN ' WM_KEYFIRST, WM_KEYLAST
bSwitchToConsole = 1 ' ... and don't miss this keypress!
IF wm.message = WM_KEYDOWN AND wm.wParam <> VK_ESCAPE THEN
DIM ir AS KEY_RECORD, written AS LONG
WITH ir.
EventType = 1 ' KEY_EVENT
bKeyDown = 1 ' TRUE
wRepeatCount = 1
wVirtualKeyCode = wm.wParam
IF GetAsyncKeyState(VK_SHIFT) AND &HF0000000 THEN
wAsciiCode = wm.wParam
ELSE
wAsciiCode = wm.wParam + 32
END IF
END WITH
WriteConsoleInputA hstdin, @ir, 1, @written
END IF
END IF
IF bSwitchToConsole THEN
bSwitchToConsole = 0 ' FALSE
SetForegroundWindow hcons
END IF
WHILE bRet := PeekMessage (@wm, 0, 0, 0, PM_REMOVE)
IF bRet = -1 THEN
'
ELSE
TranslateMessage @wm
DispatchMessage @wm
END IF
WEND
END SUB
FUNCTION WinMain(SYS inst, prevInst, ASCIIZ* cmdline, SYS show) AS SYS
WndClass wc
SYS wwd, wht, wtx, wty, tax
WITH wc.
style = CS_HREDRAW or CS_VREDRAW
lpfnWndProc = @WndProc
cbClsExtra = 0
cbWndExtra = 0
hInstance = inst
hIcon = LoadIcon 0, IDI_APPLICATION
hCursor = LoadCursor 0,IDC_ARROW
hbrBackground = GetStockObject WHITE_BRUSH
lpszMenuName = NULL
lpszClassName = strptr "Demo"
END WITH
RegisterClass (@wc)
Wwd = 320 : Wht = 200
Tax = GetSystemMetrics SM_CXSCREEN
Wtx = (Tax - Wwd) / 2
Tax = GetSystemMetrics SM_CYSCREEN
Wty = (Tax - Wht) / 2
hwnd = CreateWindowEx 0, wc.lpszClassName, "OXYGEN BASIC", WS_OVERLAPPEDWINDOW, Wtx, Wty, Wwd, Wht, 0, 0, inst, 0
UpdateWindow hwnd
END FUNCTION
FUNCTION WndProc ( hWnd, wMsg, wParam, lparam ) as sys callback
STATIC AS SYS hdc
STATIC AS STRING txt
STATIC AS PAINTSTRUCT Paintst
STATIC AS RECT crect
SELECT wMsg
CASE WM_CREATE
GetClientRect hWnd, &cRect
CASE WM_DESTROY
PostQuitMessage 0
CASE WM_PAINT
GetClientRect hWnd, &cRect
hDC = BeginPaint(hWnd, &Paintst)
SetBkColor hdc, yellow
SetTextColor hdc, red
DrawText hDC, "Hello World!", -1, &cRect, 0x25 ' DT_SINGLELINE|DT_VCENTER|DT_CENTER
EndPaint hWnd, &Paintst
CASE ELSE
FUNCTION = DefWindowProc(hWnd,wMsg,wParam,lParam)
END SELECT
END FUNCTION ' WndProc
.