Oxygen Basic
Programming => Example Code => Topic started by: Mike Lobanovsky on September 10, 2014, 07:04:00 PM
-
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
.
-
Mike,
Charles did a console interface in DLLC which might be helpful.
-
This isn't a console interface implementation, John. Oxygen's classic console interface is implemented in Console.inc which this script uses unmodified as an include file. This script is simply its functional extension for console-mostly applications where CUI is the main instrument of man-computer interaction and a GUI window or windows is or are but supplementary visual aids.
For example, imagine an SBLisp or FBLisp console interpreter where this GUI window is equipped with a set of progress bars that are following the interpreter's inner states in real time -- its heap memory growth and garbage collection, symbol hash table population, current position of "code pointer" within the program's body, and what not -- while the interpreter communicates with you and runs its programs in its natural console mode. And it's only one of its possible uses; yet another window may be used concurrently for direct graphical output of interpreted programs.
This policy wouldn't interfere with, or break, the existing structure of the imaginary interpreter as a console program. It will simply extend its meager visualization capabilities in what regards its real time operation and program output. It's almost exactly what Rob is doing by interbreeding his LISP's with Oxygen and/or Java. But Rob's setups are different processes where there's no blocking action of console input in one process on the graphical output in another one, which isn't all that simple to achieve in a single-threaded all-in-one console-mostly interpreter.
-
Thanks Mike,
May I include your example in examples/console ?
Using a thread to capture console input without blocking:
A flag is used to monitor and control the input thread.
0 do nothing but sleep
1 allow input
2 input string is ready to read
3 exit the input thread
include "$\inc\minwin.inc"
include "$\inc\console.inc"
SetConsoleTitle "Threaded Console Input: 'q' to quit"
string sa 'shared input string
sys ra 'shared input flag
function InputThread(sys p) as sys callback
===========================================
do
if ra=1 then
sa=rtrim input
ra=2 'signal input available
elseif ra=3 'quit
exit do
else
sleep 20
end if
end do
end function
thread=CreateThread 0,0,@InputThread,0,0,0
sys c
ra=1 'ready for input
do
c++ 'activity
if ra=2 then
if sa="q" then
ra=3 'quit
exit do
end if
print c " activity cycles" cr
ra=1
end if
sleep 20
end do
sleep 100
CloseHandle thread
'waitkey
-
Yes Charles,
Of course you may and it will be my honor if you do.
Your multithreaded solution is academically elegant when seen barebones-style. But I think it will get at least as complicated as mine as soon as you actually try to redirect something from the window message pipe (non-existent in your code) into your input() so it could read that something from its console input buffer. :)
-
thanks mike for your little nice console/gui example. I've changed only for a test yesterday the %WS_OVERLAPPEDWINDOW style with WM_SYSMENU, WM_VISIBLE, WM_MAXIMIZEBOX, WM_MINIMIZEBOD etc to see what happened with "X" closing. only after start I saw this x here at my test, after that, the "X" dissapear (behind or before console window). And yes, without having read charles input function post, I missed this input handling too :) my example I haven't saved on my usb stick, I am at work...
regards, frank
-
Hi Frank,
Yes, my code contains an example of how to disallow the user to close a console and/or GUI window by clicking their [X] buttons or pressing Alt+F4. Usually you do it by changing window styles but the shortest and cleanest programmatic way would be just to kill the corresponding item in the existing system menu with a DeleteMenu() call. Also, you can't change the styles of a console window because the system wouldn't let you do it. So this is the only possible way to disable the console menu buttons:
DeleteMenu GetSystemMenu(hwnd, 0), SC_CLOSE, MF_BYCOMMAND ' GUI window
DeleteMenu GetSystemMenu(hcons, 0), SC_CLOSE, MF_BYCOMMAND ' console window
Remove these calls if you want your windows to close normally. FBSL shows the corresponding change in the [X] button states immediately but I noticed that O2 windows require some arbitrary redraw cycles to change the visual style of these buttons.
Another one of my console scripts, Text Mode Squash, contains an example of how to also make the window non-resizable by deleting other items in the window's system menu.