RTR logo

BBC BASIC for Windows

Hints and Tips



This page contains some hints, tips and advanced techniques in the use of BBC BASIC for Windows which you won't find elsewhere in the manual. Don't worry if some of them seem like gobbledegook: if you can't understand them you don't need to know about them! If you have any suggestions for additions to this section please let us know.

Trapped OPENIN, OPENOUT or OPENUP

If you pass the OPENIN, OPENOUT or OPENUP functions a null filename ("") or a wildcard filename ("*.dat") BBC BASIC for Windows displays a file selector window. This is a handy way of allowing the user to choose a file for reading or writing. However there is a small snag: if the user clicks on the Cancel button BASIC behaves as if the Escape key was pressed (even if the *ESC OFF command was issued). This can be undesirable, as Escape results in an error being generated. A simple solution to this problem is to use the following functions, which instead return −1 in the event of Cancel being pressed:

DEF FNopenin(file$)
ON ERROR LOCAL = -1
= OPENIN(file$)

DEF FNopenout(file$)
ON ERROR LOCAL = -1
= OPENOUT(file$)

DEF FNopenup(file$)
ON ERROR LOCAL = -1
= OPENUP(file$)

Using windows larger than 1920 x 1440 pixels

The default output bitmap used by BBC BASIC for Windows is 1920 x 1440 pixels. Whilst this should be large enough for the majority of display settings and applications you may occasionally want to increase its size. For example you might want to extend the technique used in the example program SCROLL.BBC to scroll over an even larger 'canvas'. Alternatively you might want to create an output bitmap much larger than your screen to improve the quality of graphics printed using *HARDCOPY. The procedure below allows you to do that:

DEF PROChugewindow(x%, y%) : LOCAL bmih{}, bits%, hbm%, oldbm%
DIM bmih{Size%, Width%, Height%, Planes{l&,h&}, BitCount{l&,h&}, \
\        Compression%, SizeImage%, XPelsPerMeter%, YPelsPerMeter%, \
\        ClrUsed%, ClrImportant%}
bmih.Size% = DIM(bmih{})
bmih.Width% = x%
bmih.Height% = y%
bmih.Planes.l& = 1
bmih.BitCount.l& = 24
SYS "CreateDIBSection", @memhdc%, bmih{}, 0, ^bits%, 0, 0 TO hbm%
IF hbm% = 0 ERROR 100, "Couldn't create DIBSection"
SYS "SelectObject", @memhdc%, hbm% TO oldbm%
SYS "DeleteObject", oldbm%
SYS "SetWindowPos", @hwnd%, 0, 0, 0, x%, y%, 6
@vdu%!208 = x%
@vdu%!212 = y%
VDU 24,0;0;x%*2-2;y%*2-2;
CLG
ENDPROC
Note that increasing the size of the text viewport beyond 1920 x 1440 pixels (e.g. using VDU 26 or VDU 28) is liable to crash BASIC. The routine above deliberately leaves the text viewport unchanged.

Note also that the above routine does not support 'palette animation'.

TrackPopupMenu

If you've tried using SYS "TrackPopupMenu" to display a 'floating' (context) menu you will have found it doesn't work! This is because it is one of the few Windows™ API calls which must be made from a thread with a message loop (mind you, nowhere does Microsoft bother to tell you that!). To solve this problem use the following function instead:

DEF FNtrackpopupmenu(hmenu%,flags%,x%,y%)
LOCAL M%, O%, P%, T%
DIM P% LOCAL 60
[OPT 2
.T%
push 0
push @hwnd%
push 0
push y%
push x%
push flags%
push hmenu%
call "TrackPopupMenu"
ret 16
.M% cmp dword [esp+8],&500 : jz T%
pop eax : push [^O%] : push eax
jmp "CallWindowProc"
]
SYS "GetWindowLong", @hwnd%, -4 TO O%
SYS "SetWindowLong", @hwnd%, -4, M%
SYS "SendMessage", @hwnd%, &500, 0, 0 TO T%
SYS "SetWindowLong", @hwnd%, -4, O%
SYS "SendMessage", @hwnd%, 0, 0, 0
= T%

Passing floats to assembler code or DLLs

If you need to speed up floating point (real number) calculations you may want to do some of the work in assembly language. The BBC BASIC for Windows assembler makes this relatively easy by incorporating all the floating point instructions but you have to be careful when passing floating point numbers from BASIC to the assembler code. Firstly you must have selected the *FLOAT 64 mode (only then are BASIC's numbers in a format which the processor can understand directly) and secondly you need to be aware that BASIC stores integers in a special way. To solve this latter problem you should multiply all values by 1.0 (the decimal point is important) before passing them to the assembler code:

fpvalue *= 1.0
CALL code, fpvalue
If you don't do that your assembler code will do completely the wrong thing in the event that fpvalue contains an integer. Exactly the same applies if you are passing a floating point value to a function in a DLL (which will typically require a pointer to the value):
fpvalue *= 1.0
SYS dllfunction%, ^fpvalue
In the event that you need to pass an array of floating point values to a DLL function each element of the array must be multiplied by 1.0 for the same reason (and the address passed must be that of the first element of the array):
fparray() *= 1.0
SYS fft%, ^fparray(0)
Note that the assembler code or DLL must be expecting double precision (64-bit) floating-point numbers. BBC BASIC for Windows has no built-in support for ordinary 32-bit floats (but see the FN_f4 library routine for a conversion function).

Precautions when using LOCAL arrays

LOCAL arrays are stored on the stack and special precautions are required as a result. Firstly, avoid swapping a local array with a global array (i.e. one stored on the heap). Because SWAP exchanges the array pointers (rather than the array data) the 'global' array may end up pointing to data on the stack, which will become invalid on exit from the function or procedure in which the array was defined. Any subsequent attempt to access the array data will fail, and may crash BASIC. You can safely copy the array, because the data is copied rather than the pointer:

DEF PROChint
LOCAL localarray()
DIM localarray(10)
SWAP globalarray(),localarray() : REM Don't do this!
globalarray() = localarray() : REM This is OK
ENDPROC
Secondly, be careful if your program uses LOCAL arrays and ON ERROR. If an error occurs in a procedure or function in which a local array is defined, BASIC will jump immediately to the ON ERROR routine without passing through the ENDPROC or end-of-function statement. The 'local' array will still exist, but will point to an invalid area of memory (errors cause the stack to be discarded), so again any subsequent attempt to access the contents of the array will fail and may cause a crash. To protect against this either ensure errors (even Escape) cannot occur in those functions or procedures, use ON ERROR LOCAL and RESTORE LOCAL to clear the local array(s), or make sure all your local arrays have different names from any global arrays:
DIM temp(10) : REM Global array
DEF PROChint
LOCAL temp() : REM Don't do this if ON ERROR is active
ENDPROC
Simply changing the name of the global array from temp to (for example) Temp would avoid any problems in the event of an error occurring inside the procedure.

Using status bars and toolbars with the Multiple Document Interface

The WINLIB library allows you to incorporate status bars and toolbars in your program, and the MDILIB library allows you to use the Multiple Document Interface. However if you try to do both at the same time there are problems, because the MDI window covers up the toolbar and the status bar. The solution is to change the size and position of the MDI window so that the toolbar and/or status bar are not covered; this can be done by adding the following code immediately after the call to PROC_initmdi():

For a toolbar and status bar:
DIM rc{l%,t%,r%,b%}
SYS "GetWindowRect", hstat% , rc{}
sbh% = rc.b%-rc.t%
SYS "GetWindowRect", htool% , rc{}
tbh% = rc.b%-rc.t%
SYS "GetClientRect", @hwnd%, rc{}
SYS "MoveWindow", @hmdi%, 0, tbh%, rc.r%, rc.b%-sbh%-tbh%, 1
For just a toolbar:
DIM rc{l%,t%,r%,b%}
SYS "GetWindowRect", htool% , rc{}
tbh% = rc.b%-rc.t%
SYS "GetClientRect", @hwnd%, rc{}
SYS "MoveWindow", @hmdi%, 0, tbh%, rc.r%, rc.b%-tbh%, 1
For just a status bar:
DIM rc{l%,t%,r%,b%}
SYS "GetWindowRect", hstat% , rc{}
sbh% = rc.b%-rc.t%
SYS "GetClientRect", @hwnd%, rc{}
SYS "MoveWindow", @hmdi%, 0, 0, rc.r%, rc.b%-sbh%, 1
Here hstat% is the handle of the status bar and htool% is the handle of the toolbar.

In order that the toolbar and status bar remain visible after the user resizes the window, the same code should be duplicated in your ON MOVE routine, for example:

hwndorig% = @hwnd%
DIM Move%(2)
ON MOVE Move%()=@msg%,@wparam%,@lparam%:PROCmove:RETURN
...
DEF PROCmove
LOCAL rc{},sbh%,tbh%
DIM rc{l%,t%,r%,b%}
SYS "GetWindowRect", hstat% , rc{}
sbh% = rc.b%-rc.t%
SYS "GetWindowRect", htool% , rc{}
tbh% = rc.b%-rc.t%
SYS "PostMessage", hstat%, Move%(0), Move%(1), Move%(2)
SYS "PostMessage", htool%, Move%(0), Move%(1), Move%(2)
SYS "GetClientRect", hwndorig%, rc{}
SYS "MoveWindow", @hmdi%, 0, tbh%, rc.r%, rc.b%-tbh%-sbh%, 1
ENDPROC
Here hstat% is the handle of the status bar and htool% is the handle of the toolbar. Note that PROCmove uses a copy of the original @hwnd% because programs using the Multiple Document Interface often modify @hwnd% during the course of their execution.

Foreign language equivalent of TIME$

The built-in TIME$ function always returns the day and month names in English. The function below returns a string identical in format to TIME$ but with the day and month names in the language for which the PC has been configured:

DEF FNtime$ : LOCAL D%, L%
DIM D% LOCAL 255
SYS "GetDateFormat", 0, 0, 0, "ddd.dd MMM yyyy,", D%, 255 TO L%
SYS "GetTimeFormat", 0, 0, 0, "HH:mm:ss", D%+L%-1, 255-L%
= $$D%

Loading a sprite (or icon) from memory

The FN_createsprite routine in the SPRITELIB library creates a sprite from an icon (.ICO) file. There may be situations where you would prefer to load the file into memory first (perhaps containing many different icons or other data) and create the sprite from the memory contents. You can do that using the routine below, which takes a memory pointer rather than a filename but is otherwise used in exactly the same way as FN_createsprite:

DEF FN_createspritefrommemory(N%, P%, W%, H%) : LOCAL S%
IF N% >= `sprites%!0 THEN = FALSE
S% = `sprites% + N%*24 + 32
SYS "CreateIconFromResourceEx", P%+P%!18, P%!14, 1, &30000, W%, H%, 0 TO !S%
S%!4 = W%
S%!8 = H%
= !S%
The second parameter must be the address in memory at which an icon file is loaded.

Setting the File Open dialogue's initial directory

The File Open and File Save dialogue boxes remember the directory (folder) which was last viewed, and by default select it as the initial directory on the next occasion. This is generally a useful feature, although it only works properly when your BASIC program has been compiled to a stand-alone executable (the 'remembered' directory can be changed to something different when running under the interactive environment). However there may be occasions when you want to override this behaviour and determine the initial directory yourself. You can do that by using the following code instead of that listed in the manual:

DIM fs{lStructSize%, hwndOwner%, hInstance%, lpstrFilter%, \
\      lpstrCustomFilter%, nMaxCustFilter%, nFilterIndex%, \
\      lpstrFile%, nMaxFile%, lpstrFileTitle%, \
\      nMaxFileTitle%, lpstrInitialDir%, lpstrTitle%, \
\      flags%, nFileOffset{l&,h&}, nFileExtension{l&,h&}, \
\      lpstrDefExt%, lCustData%, lpfnHook%, lpTemplateName%}
DIM fp{t&(260)}
InitialDir$ = "C:\bbcbasic"+CHR$0
FileFilter$ = "BMP files"+CHR$0+"*.BMP"+CHR$0+CHR$0
fs.lStructSize% = DIM(fs{})
fs.hwndOwner% = @hwnd%
fs.lpstrFilter% = PTR(FileFilter$)
fs.lpstrFile% = fp{}
fs.nMaxFile% = DIM(fp{}) - 1
fs.lpstrInitialDir% = PTR(InitialDir$)
fs.flags% = 6
The usual precautions for the use of DIM apply (make sure you do it only once, or use DIM LOCAL). Note that the string containing the initial directory must be terminated with a NUL (CHR$0). Once you have executed this code you can call up the File Open or File Save dialogue with SYS "GetOpenFileName" or SYS "GetSaveFileName" in the normal way:
SYS "GetOpenFileName", fs{} TO result%
IF result% filename$ = $$fp{}

Centering a hardcopy printout on the page

The manual tells you how to centre printed text on a particular column, but not how to centre it on the page. The following routine prints (on the printer) a string centred between the left and right margins:

DIM size{cx%,cy%}
*PRINTERFONT Arial,24
*MARGINS 10,10,10,10
VDU 2,1,32
string$ = "The quick brown fox"
SYS "GetTextExtentPoint32", @prthdc%, string$, LEN(string$), size{}
@vdu%!-12 = (@vdu%!232 + @vdu%!236 - size.cx%)/2
PRINT string$
VDU 1,12,3
The VDU 1,32 is only needed if nothing else has previously been printed on the page. For multiple lines of text simply repeat the four statements commencing string$ = and ending PRINT string$.

Using DATA in installed modules

Because CALLed and INSTALLed modules cannot use line numbers, the traditional form of the RESTORE statement cannot be used to move the data pointer into such modules. However the relative form RESTORE +n can. A convenient way of arranging this is to associate a restore procedure with each independent block of data you might want to use:

DEF PROCrestore1 : RESTORE +1 : ENDPROC
DATA 1, 2, 3, 4, 5, 6, 7, etc
DATA ...

DEF PROCrestore2 : RESTORE +1 : ENDPROC
DATA 8, 9, 10, 11, 12, 13, etc
DATA ...
Then, when you want to READ the data (which can be done from the main program, or another installed module) you just call the appropriate restore procedure first:
PROCrestore1
READ A, B, C, D, E, F, G, etc

PROCrestore2
READ a, b, c, d, e, f, g, etc
Using this technique you can hide your data away in a separate module.

Re-entrant ON MOVE interrupts

The manual gives the following example of the use of ON MOVE:

ON MOVE PROCmove(@msg%,@lparam%) : RETURN
This will work, and guarantees that the PROCmove procedure will be called once for each ON MOVE event, but there is a snag: because the procedure can itself be interrupted by a subsequent ON MOVE, the events may not be processed in the order in which they arrive! It may well be more important to ensure that the last ON MOVE is processed last, even if it means that some earlier ones are discarded. You can achieve this behaviour by passing the parameters in a global array. For example, to forward the ON MOVE event to a toolbar and status bar so they resize themselves correctly:
DIM Move%(2)
ON MOVE Move%()=@msg%,@wparam%,@lparam% : PROCmove : RETURN
......
DEF PROCmove
SYS "SendMessage", hToolbar%, Move%(0), Move%(1), Move%(2)
SYS "SendMessage", hStatbar%, Move%(0), Move%(1), Move%(2)
ENDPROC
It is important that the three elements of the Move%() array are used in the same statement, so another interrupt cannot occur between them.

Cunning use of the CASE statement

Testing for several mutually-exclusive possibilities using nested IF...ENDIF statements can be messy:

IF abc%<10 THEN
  state% = 1
ELSE IF abc%=10 THEN
    state% = 2
  ELSE IF abc%>20 THEN
      state% = 3
    ELSE
      state% = 99
    ENDIF
  ENDIF
ENDIF
The unhelpful indentation and the multiple ENDIFs make it unclear what is happening. The same thing can be achieved more elegantly by using the CASE statement in a cunning way:
CASE TRUE OF
  WHEN abc%<10: state%=1
  WHEN abc%=10: state%=2
  WHEN abc%>20: state%=3
  OTHERWISE: state%=99
ENDCASE 
Notice the use of CASE TRUE to force the interpreter to test the truth of each of the conditional expressions.

'Docking' a dialogue box

On occasions you may want your program's user interface to consist solely of a dialogue box, for example if all input/output is by means of buttons, list boxes etc. The easiest way to achieve that is to hide your program's main output window (using a SYS "ShowWindow" command or by selecting the initial window state as hidden when you compile it).

However there is a disadvantage in this approach: the user cannot 'minimise' your program's window because dialogue boxes don't have a minimise button. Even if you add a minimise button, the window won't minimise to an icon on the task bar as a conventional application would.

A solution is to 'dock' the dialogue box to the main window so that it appears and behaves as a dialogue box but can be minimised just like an ordinary window. This requires three steps:

The program segment below achieves all three effects:

dlg% = FN_newdialog("",0,0,width%,height%,font%,size%)
dlg%!16 = (dlg%!16 OR &40000000) AND NOT &80400000
REM Add dialogue box contents here in the usual way
PROC_showdialog(dlg%)

DIM rc{l%,t%,r%,b%}
SYS "GetWindowRect", !dlg%, rc{}

SYS "GetWindowLong", @hwnd%, -16 TO style%
SYS "SetWindowLong", @hwnd%, -16, style% AND NOT &50000
SYS "AdjustWindowRect", rc{}, style% AND NOT &50000, 0
SYS "SetWindowPos", @hwnd%, 0, 0, 0, rc.r%-rc.l%, rc.b%-rc.t%, 102
This code should be executed just once at the beginning of your program. Note that the second and third parameters of FN_newdialog must be zero; if you create your dialogue box template using DLGEDIT remember to change them.

You can use a similar technique to 'dock' a Property Sheet or a Wizard. In those cases add the following code after the PROC_showpropsheet statement:

DIM rc{l%,t%,r%,b%}
SYS "GetClientRect", !psh%, rc{}

SYS "SetParent", !psh%, @hwnd%
SYS "GetWindowLong", !psh%, -16 TO style%
SYS "SetWindowLong", !psh%, -16, (style% OR &40000000) AND NOT &80400000
SYS "SetWindowPos", !psh%, 0, 0, 0, 0, 0, 37

SYS "GetWindowLong", @hwnd%, -16 TO style%
SYS "SetWindowLong", @hwnd%, -16, style% AND NOT &50000
SYS "AdjustWindowRect", rc{}, style%, 0
SYS "SetWindowPos", @hwnd%, 0, 0, 0, rc.r%-rc.l%, rc.b%-rc.t%, 102
Here psh% is the value returned from FN_newpropsheet.

Disabling Windows XP Visual Styles

BBC BASIC for Windows by default uses Windows XP™ Visual Styles. If for some reason you don't want to use the new-style controls in your program, or you simply don't like their appearance, you have three options:

Left CONTENTS

HOME Right


Best viewed with Any Browser Valid HTML 3.2!
© Richard Russell 2021