• Print

Author Topic: expirimental win32 console library  (Read 627 times)

mcalkins

  • Hero Member
  • *****
  • Posts: 1269
    • qbasicmichael.com
    • Email
expirimental win32 console library
« on: February 23, 2012, 02:25:02 PM »
Yes, I know Galleon said he will use ncurses, but I very much felt like doing this. It is experimental, and, knowing me, I'll probably never finish it.

I haven't implemented any of the keyboard/mouse input yet. that will have to wait for another day.

I wanted it to try AttachConsole(ATTACH_PARENT_PROCESS) first, and if that fails, try AllocConsole. However, if I try AttachConsole from the qb64 code, it fails silently. If I try it from the c++ code, and it fails with 0x6, then even AllocConsole fails with 0x5. So at this point, I have the user choose which one to use. If you are invoking it from the command prompt, use AttachConsole to use your existing cmd window. Otherwise, AllocConsole creates a new one.

Note that if you run it from command prompt, you might not see the new prompt after the program has terminated, but it's there. you can press enter on a blank line to get another.

The one bug that I can see that it has is that it is not properly shrinking the buffer when going from 40x50 to 80x25. I'm too tired to diagnose it right now.

In the other forum thread, I mentioned that win32 doesn't allow specifying the top and bottom of the cursor, only the percentage. There are at least 2 other limitations: win32 doesn't seem to allow text blinking, although windows can do it for full screen DOS programs. Also, you're stuck with the 16 standard colors, although you do get all 16 for background as well.

As you can see, there is a lot I haven't implemented yet. INKEY$ by itself would be fairly easy, if I didn't have to worry about the mouse. However, keyboard input and mouse input come out of the same buffer, so I think the code will have to maintain its own buffers, so that INKEY doesn't lose mouse events, and the mouse functions don't lose keyboard events.

win32 allows a console to have multiple screen buffers. This could be used to emulate the video memory pages, such as PCOPY would use. However, I think implementing it would add complication. I'll think about it after I do the input stuff.

VIEW PRINT would also be possible, but would be likewise complicated, in my opinion.

You can see there are 2 causes of complexity in PRINT. One is QBASIC's behavior of moving stuff to the next line if it would otherwise cross the end of the line. The other is the necessity of being able to print to the bottom right spot on the window without it scrolling. However, a manual LOCATE or PRINTcrlf is then required to keep the next PRINT from overwriting it.

Note that where the window is smaller than the buffer, LOCATE works relative to the buffer, not the window. Although I'd prefer an origin of 0,0, LOCATE keeps QBASIC's origin of 1,1. However, the window offset is 0,0.

some of the functions allow you to get structures directly from windows, in which case they will use window's 0,0 origin.

Note that I have not put much error checking at all in this code. Most of it completely ignores errors. Also, there is little to no input validation. For example, it is up to you to make sure that the parameter to CONTROLCHR is either 0 or 1, nothing else.

I had meant to include an explanation of the usage of each function, but I've about had it with this today, and most or all of it should be self explanatory.

cl.h
Code: [Select]
/*
expirimental console library for QB64 0.951 win32.
public domain, michael calkins, revision 2012 02 23
*/

CONSOLE_CURSOR_INFO cl_curinfo;

HANDLE cl_stdin = 0;
HANDLE cl_stdout = 0;
HANDLE cl_stderr = 0;
HANDLE cl_conin = 0;
HANDLE cl_conout = 0;

BOOL cl_ctrlchr = 1;

CONSOLE_SCREEN_BUFFER_INFO cl_bufinfo;

WORD cl_crlf[] = {0xd,0xa};

DWORD cl_init(BOOL m){
 SECURITY_ATTRIBUTES SecAttribs = {sizeof(SECURITY_ATTRIBUTES), 0, 1};

 DWORD trash;

 cl_stdin = GetStdHandle(STD_INPUT_HANDLE);
 cl_stdout = GetStdHandle(STD_OUTPUT_HANDLE);
 cl_stderr = GetStdHandle(STD_ERROR_HANDLE);

 if (m) {
  if (! AllocConsole()) {
   return GetLastError();
  }
 } else {
  if (! AttachConsole(ATTACH_PARENT_PROCESS)) {
   return GetLastError();
  }
 }

 cl_conin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, & SecAttribs, OPEN_EXISTING, 0, 0);
 cl_conout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, & SecAttribs, OPEN_EXISTING, 0, 0);
 return 0;
}

void cl_getWIDTH(CONSOLE_SCREEN_BUFFER_INFO * p){
 GetConsoleScreenBufferInfo(cl_conout, p);
}

void cl_WIDTH(short bufx, short bufy, short winox, short winoy, short winsx, short winsy){
 if (! winsx){
  winsx = bufx;
  winsy = bufy;
 }

/* convert from a size to a bottom right corner */

 winsx += winox - 1;
 winsy += winoy - 1;

 GetConsoleScreenBufferInfo(cl_conout, & cl_bufinfo);

/*
 in both the x and y dimension individually:
 if the new buffer size is smaller than the old:
  shrink the window first, then shrink the buffer
 else:
  enlarge the buffer first, then enlarge the window
*/

 cl_bufinfo.srWindow.Left = winox;
 cl_bufinfo.srWindow.Right = winsx;
 cl_bufinfo.dwSize.X = bufx;

 if (cl_bufinfo.dwSize.X < bufx){
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
 } else {
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
 }

 cl_bufinfo.srWindow.Top = winoy;
 cl_bufinfo.srWindow.Bottom = winsy;
 cl_bufinfo.dwSize.Y = bufy;

 if (cl_bufinfo.dwSize.Y < bufy){
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
 } else {
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
 }
}

short cl_CSRLIN(void){
 GetConsoleScreenBufferInfo(cl_conout, & cl_bufinfo);
 return cl_bufinfo.dwCursorPosition.Y + 1;
}

short cl_POS(void){
 GetConsoleScreenBufferInfo(cl_conout, & cl_bufinfo);
 return cl_bufinfo.dwCursorPosition.X + 1;
}

void cl_LOCATE(short l, short c){
 COORD cp = {--c, --l};
 SetConsoleCursorPosition(cl_conout, cp);
}

void cl_getLOCATEvis(CONSOLE_CURSOR_INFO * p){
 GetConsoleCursorInfo(cl_conout, p);
}

void cl_LOCATEvis(BOOL v, DWORD s){
 cl_curinfo.dwSize = s;
 cl_curinfo.bVisible = v;
 SetConsoleCursorInfo(cl_conout, & cl_curinfo);
}

void cl_COLOR(short f, short b){
 SetConsoleTextAttribute(cl_conout, (f & 0xf) |  (b << 4));
}

WORD cl_SCREEN(short l, short c, BOOL f){
 COORD cp = {--c, --l};
 DWORD t;
 WORD a;
 if (f){
  ReadConsoleOutputAttribute(cl_conout, & a, 1, cp, & t) ;
  return a;
 } else {
  ReadConsoleOutputCharacterA(cl_conout, (char *) & a, 1, cp, & t) ;
  return a & 0xff;
 }
}

void cl_skiptonextline(void)
{
/* assumes the caller has already called GetConsoleScreenBufferInfo
   assumes the caller will cal SetConsoleCursorPosition */
 cl_bufinfo.dwCursorPosition.X = 0;
 if (++cl_bufinfo.dwCursorPosition.Y > cl_bufinfo.dwSize.Y){
  /* scroll the buffer */
  SMALL_RECT scroll = {0, 1, cl_bufinfo.dwSize.X-1, cl_bufinfo.dwSize.Y-1};
  CHAR_INFO ch = {(WCHAR) 0x20, cl_bufinfo.wAttributes};
  COORD z = {0, 0};
  ScrollConsoleScreenBufferW(cl_conout, & scroll, 0, z, & ch);
  --cl_bufinfo.dwCursorPosition.Y;
 }
}

void cl_PRINT(char * p, DWORD s){
 DWORD t;
 
 /* Attempt to duplicate QBASIC's behavior if CSRLIN + length > width of screen */

 GetConsoleScreenBufferInfo(cl_conout, & cl_bufinfo);
 if (s + cl_bufinfo.dwCursorPosition.X > cl_bufinfo.dwSize.X){
  cl_skiptonextline();
  SetConsoleCursorPosition(cl_conout, cl_bufinfo.dwCursorPosition);
 }

 /* Attempt to prevent scrolling if output ends in the last cell of the window */

 SetConsoleMode(cl_conout, ENABLE_WRAP_AT_EOL_OUTPUT | cl_ctrlchr);

 if (s > 1) {
  WriteConsoleA(cl_conout, p, s - 1, & t, 0);
 }

 GetConsoleScreenBufferInfo(cl_conout, & cl_bufinfo);
 if ((cl_bufinfo.dwCursorPosition.X == cl_bufinfo.srWindow.Right) && (cl_bufinfo.dwCursorPosition.Y == cl_bufinfo.srWindow.Bottom)) {
  SetConsoleMode(cl_conout, cl_ctrlchr);
 }

 if (s) {
  WriteConsoleA(cl_conout, p + s - 1, 1, & t, 0);
 }

 SetConsoleMode(cl_conout, ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT);
}

void cl_CONTROLCHR(BOOL b){
 cl_ctrlchr = b; /* b must be 0 or 1 */
}

void cl_PRINTcrlf(void){
 DWORD t;
 WriteConsoleW(cl_conout, cl_crlf, 2, & t, 0);
}

void cl_TITLE(char * p){
 SetConsoleTitleA(p);
}

DWORD cl_getTITLE(char * p, DWORD n){
 return GetConsoleTitleA(p, n);
}

void cl_TAB(short n){
/* do later */
}

void cl_SPC(short n){
/* do later */
}

void cl_PRINTcomma(void){
/* do later */
}

void cl_LINEINPUT(char * p, DWORD s){
/* do later */
}

void cl_CLS(void){
/* do later */
}

WORD cl_INKEY(void){
/* do later */
}

unsigned char cl_PEEK0x417(void){
/* do later */
}

DWORD cl_multikey(void){
/* do later */
}

DWORD cl_MOUSE(MOUSE_EVENT_RECORD * p){
/* do later */
}

cl.bi
Code: [Select]
' expirimental console library for QB64 0.951 win32.
' public domain, michael calkins, revision 2012 02 23

DECLARE CUSTOMTYPE LIBRARY "cl"
 FUNCTION cl_init~& (BYVAL m&)
 SUB cl_getWIDTH (BYVAL lpConsoleScreenBufferInfo%&)
 SUB cl_WIDTH (BYVAL bufx%, BYVAL bufy%, BYVAL winox%, BYVAL winoy%, BYVAL winsx%, BYVAL winsy%)
 FUNCTION cl_CSRLIN% ()
 FUNCTION cl_POS% ()
 SUB cl_LOCATE (BYVAL l%, BYVAL c%)
 SUB cl_getLOCATEvis (BYVAL lpConsoleCursorInfo%&)
 SUB cl_LOCATEvis (BYVAL v~&, BYVAL s~&)
 SUB cl_COLOR (BYVAL f%, BYVAL b%)
 FUNCTION cl_SCREEN~& (BYVAL l%, BYVAL c%, BYVAL f&)
 SUB cl_PRINT (BYVAL lpBuffer%&, BYVAL s~&)
 SUB cl_CONTROLCHR (BYVAL b~&)
 'sub cl_TAB(byval n%)
 'sub cl_SPC(byval n%)
 SUB cl_PRINTcrlf ()
 'sub cl_PRINTcomma()
 'sub cl_LINEINPUT(byval lpBuffer%&, byval s~&)
 SUB cl_TITLE (BYVAL lpConsoleTitle%&)
 FUNCTION cl_getTITLE~& (BYVAL lpConsoleTitle%&, BYVAL n~&)
 'sub cl_CLS()
 'function cl_INKEY~%()
 'function cl_PEEK0x417~%%()
 'function cl_multikey~&()
 'function cl_MOUSE(byval pMouseEvent%&)
END DECLARE

TYPE MOUSE_EVENT_RECORD
 dwMousePosition_X AS INTEGER
 dwMousePosition_Y AS INTEGER
 dwButtonState AS _UNSIGNED LONG
 dwControlKeyState AS _UNSIGNED LONG
 dwEventFlags AS _UNSIGNED LONG
END TYPE

TYPE CONSOLE_CURSOR_INFO
 dwSize AS _UNSIGNED LONG
 bVisible AS LONG
END TYPE

TYPE CONSOLE_SCREEN_BUFFER_INFO
 dwSize_X AS INTEGER
 dwSize_Y AS INTEGER
 dwCursorPosition_X AS INTEGER
 dwCursorPosition_Y AS INTEGER
 wAttributes AS _UNSIGNED INTEGER
 srWindow_Left AS INTEGER
 srWindow_Top AS INTEGER
 srWindow_Right AS INTEGER
 srWindow_Bottom AS INTEGER
 dwMaximumWindowSize_X AS INTEGER
 dwMaximumWindowSize_Y AS INTEGER
END TYPE

cl.bas
Code: [Select]
'test/demo code
'public domain, michael calkins, revision 2012 02 23

'$include:'cl.bi'
DIM t AS STRING
DIM i AS LONG
DIM curinfo AS CONSOLE_CURSOR_INFO
DIM bufinfo AS CONSOLE_SCREEN_BUFFER_INFO

PRINT "0 --- AttachConsole(ATTACH_PARENT_PROCESS)"
PRINT "1 --- AllocConsole"
INPUT "Your choice? ", i
i = cl_init(i)
IF i THEN
 PRINT "Failed! Error: 0x" + LCASE$(HEX$(i))
 END
END IF
_DELAY 1

cl_getWIDTH _OFFSET(bufinfo)

t = "Console title" + CHR$(0)
cl_TITLE _OFFSET(t)

cl_LOCATE 1, 1

cl_COLOR &HF, 0

cl_WIDTH 40, 25, 0, 0, 0, 0
cl_LOCATE 1, 1
t = "attempting 40x25 buffer and window"
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_WIDTH 40, 43, 0, 0, 0, 0
cl_LOCATE 1, 1
t = "attempting 40x43 buffer and window"
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_WIDTH 40, 50, 0, 0, 0, 0
cl_LOCATE 1, 1
t = "attempting 40x50 buffer and window"
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_WIDTH 80, 25, 0, 0, 0, 0
cl_LOCATE 1, 1
t = "attempting 80x25 buffer and window"
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_WIDTH 80, 43, 0, 0, 0, 0
cl_LOCATE 1, 1
t = "attempting 80x43 buffer and window"
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_WIDTH 80, 50, 0, 0, 0, 0
cl_LOCATE 1, 1
t = "attempting 80x50 buffer and window"
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_WIDTH 80, 300, 0, 150, 80, 25
cl_LOCATE 151, 1
t = "attempting 80x300 buffer, 80x25 window starting on line 150."
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_WIDTH bufinfo.dwSize_X, bufinfo.dwSize_Y, bufinfo.srWindow_Left, bufinfo.srWindow_Top, bufinfo.srWindow_Right + 1 - bufinfo.srWindow_Left, bufinfo.srWindow_Bottom + 1 - bufinfo.srWindow_Top
cl_LOCATE 1, 1
t = "attempting original buffer and window"
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3

cl_COLOR &HF, 1
cl_CONTROLCHR 0
t = SPACE$(&H40)
FOR i = 0 TO &HFF
 MID$(t, (i AND &H3F) + 1, 1) = CHR$(i)
 IF (i AND &H3F) = &H3F THEN
  cl_PRINT _OFFSET(t), &H40
  cl_PRINTcrlf
 END IF
NEXT
cl_CONTROLCHR 1

cl_COLOR &HE, 4
t = "ab" + MKI$(&HA0D) + "cd"
cl_PRINT _OFFSET(t), LEN(t)
cl_PRINTcrlf

cl_COLOR &HD, 0
cl_getLOCATEvis _OFFSET(curinfo)

cl_LOCATEvis 0, 1
t = "No cursor."
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_PRINTcrlf
cl_LOCATEvis 1, 1
t = "1% cursor."
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_PRINTcrlf
cl_LOCATEvis 1, 50
t = "50% cursor."
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_PRINTcrlf
cl_LOCATEvis 1, 100
t = "100% cursor."
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_PRINTcrlf
cl_LOCATEvis 1, curinfo.dwSize
t = "Back to the original."
cl_PRINT _OFFSET(t), LEN(t)
SLEEP 3
cl_PRINTcrlf

cl_COLOR 9, 0
t = "csrlin:" + STR$(cl_CSRLIN) + "  pos:" + STR$(cl_POS)
cl_PRINT _OFFSET(t), LEN(t)
cl_PRINTcrlf

t = "screen(2,5,0): 0x" + HEX$(cl_SCREEN(2, 5, 0)) + "  screen(2,5,1): 0x" + HEX$(cl_SCREEN(2, 5, 1))
cl_PRINT _OFFSET(t), LEN(t)
cl_PRINTcrlf

cl_getWIDTH _OFFSET(bufinfo) 'it might have changed.

t = "test"
cl_LOCATE cl_CSRLIN, bufinfo.dwSize_X - 4
cl_PRINT _OFFSET(t), LEN(t)
cl_PRINTcrlf
cl_LOCATE cl_CSRLIN, bufinfo.dwSize_X - 3
cl_PRINT _OFFSET(t), LEN(t)
cl_PRINTcrlf
cl_LOCATE cl_CSRLIN, bufinfo.dwSize_X - 2
cl_PRINT _OFFSET(t), LEN(t)
cl_PRINTcrlf

t = "bottom right of window"
cl_LOCATE bufinfo.srWindow_Bottom + 1, bufinfo.srWindow_Right + 2 - LEN(t)
cl_PRINT _OFFSET(t), LEN(t)
'Note that the cursor is still in the bottom right. You'll have to move it
'before the next print, to avoid overwriting the last character.

SLEEP 3
cl_PRINTcrlf
cl_PRINTcrlf
cl_CONTROLCHR 0
FOR i = 0 TO &HFF
 cl_COLOR i AND &HF, (i AND &HF0) \ &H10
 t = CHR$(i)
 cl_PRINT _OFFSET(t), 1
 IF (i AND &HF) = &HF THEN
  cl_PRINTcrlf
 END IF
NEXT
cl_COLOR 7, 0
cl_PRINTcrlf
t = "Done!"
cl_PRINT _OFFSET(t), LEN(t)
cl_PRINTcrlf
END

Regards,
Michael
The QBASIC Forum Community: http://www.network54.com/index/10167 Includes off-topic subforums.
QB64 Off-topic subforum: http://qb64offtopic.freeforums.org/

mcalkins

  • Hero Member
  • *****
  • Posts: 1269
    • qbasicmichael.com
    • Email
Re: expirimental win32 console library
« Reply #1 on: September 02, 2012, 01:53:46 AM »
cl_WIDTH is buggy:

Code: [Select]
cl_bufinfo.srWindow.Left = winox;
 cl_bufinfo.srWindow.Right = winsx;
 cl_bufinfo.dwSize.X = bufx;

 if (cl_bufinfo.dwSize.X < bufx){
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
 } else {
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
 }

 cl_bufinfo.srWindow.Top = winoy;
 cl_bufinfo.srWindow.Bottom = winsy;
 cl_bufinfo.dwSize.Y = bufy;

 if (cl_bufinfo.dwSize.Y < bufy){
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
 } else {
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
 }

I'm still not sure if I have got it right, but I think that this is an improvement:

Code: [Select]
cl_bufinfo.srWindow.Left = winox;
 cl_bufinfo.srWindow.Right = winsx;

 if (bufx < cl_bufinfo.dwSize.X){
  cl_bufinfo.dwSize.X = bufx;
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
 } else {
  cl_bufinfo.dwSize.X = bufx;
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
 }

 cl_bufinfo.srWindow.Top = winoy;
 cl_bufinfo.srWindow.Bottom = winsy;

 if (bufy < cl_bufinfo.dwSize.Y){
  cl_bufinfo.dwSize.Y = bufy;
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
 } else {
  cl_bufinfo.dwSize.Y = bufy;
  SetConsoleScreenBufferSize(cl_conout, cl_bufinfo.dwSize);
  SetConsoleWindowInfo(cl_conout, 1, & cl_bufinfo.srWindow);
 }

I intend to resume work on this library. I'll try to keep myself motivated on this project for the next few days. No guarantees, though. (Real life might catch up with me, or I might get bored.) As I've said elsewhere, I had already started on the keyboard and mouse input, but had never posted it.

Regards,
Michael
The QBASIC Forum Community: http://www.network54.com/index/10167 Includes off-topic subforums.
QB64 Off-topic subforum: http://qb64offtopic.freeforums.org/

mcalkins

  • Hero Member
  • *****
  • Posts: 1269
    • qbasicmichael.com
    • Email
Re: expirimental win32 console library
« Reply #2 on: September 06, 2012, 06:42:44 PM »
The attached file contains what I have now. It's not done, and significant parts are untested. I have observed some bugginess in cl_PRINTlow, when printing to the end of the screen buffer in full screen mode.

The main improvements over the previous code are that I have provided convenient wrapper functions to allow QB64 strings, instead of pointers and lengths, and I think that I have a working cl_INKEY. I had roughed out the input framework several months ago, but hadn't gotten around to testing and refining it. I spent most of today working on that.

Despite all the "inline"s, I believe CUSTOMTYPE prevents inlining. However, without CUSTOMTYPE, I have problems because the %& parameters are pointer sized integers instead of void pointers. Fixing that will probably take a bunch of casts...

Anyway, I plan to keep working on this, but I never really know what the future brings. Especially now, my real life situation should be getting my attention.

Regards,
Michael
The QBASIC Forum Community: http://www.network54.com/index/10167 Includes off-topic subforums.
QB64 Off-topic subforum: http://qb64offtopic.freeforums.org/

  • Print