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.
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$)
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:
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.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 also that the above routine does not support 'palette animation'.
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%
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:
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 CALL code, 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):fpvalue *= 1.0 SYS dllfunction%, ^fpvalue
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).fparray() *= 1.0 SYS fft%, ^fparray(0)
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:
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:DEF PROChint LOCAL localarray() DIM localarray(10) SWAP globalarray(),localarray() : REM Don't do this! globalarray() = localarray() : REM This is OK 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.DIM temp(10) : REM Global array DEF PROChint LOCAL temp() : REM Don't do this if ON ERROR is active ENDPROC
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:Here hstat% is the handle of the status bar and htool% is the handle of the toolbar.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%, 1For 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%, 1For 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
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:
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.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
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%
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:
The second parameter must be the address in memory at which an icon file is loaded.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 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:
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 withDIM 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
SYS "GetOpenFileName"
or SYS "GetSaveFileName"
in the normal way:
SYS "GetOpenFileName", fs{} TO result% IF result% filename$ = $$fp{}
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:
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 commencingDIM 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
string$ =
and ending PRINT string$
.
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:
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: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 ...
Using this technique you can hide your data away in a separate module.PROCrestore1 READ A, B, C, D, E, F, G, etc PROCrestore2 READ a, b, c, d, e, f, g, etc
The manual gives the following example of the use of ON MOVE:
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:ON MOVE PROCmove(@msg%,@lparam%) : RETURN
It is important that the three elements of the Move%() array are used in the same statement, so another interrupt cannot occur between them.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
Testing for several mutually-exclusive possibilities using nested IF...ENDIF statements can be messy:
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: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
Notice the use of CASE TRUE to force the interpreter to test the truth of each of the conditional expressions.CASE TRUE OF WHEN abc%<10: state%=1 WHEN abc%=10: state%=2 WHEN abc%>20: state%=3 OTHERWISE: state%=99 ENDCASE
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:
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.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
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:
Here psh% is the value returned from FN_newpropsheet.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
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:
DEF PROC_nostyle(hw%) LOCAL uxt%, swt% SYS "LoadLibrary", "uxtheme.dll" TO uxt% SYS "GetProcAddress", uxt%, "SetWindowTheme" TO swt% IF swt% SYS swt%, hw%, CHR$0, CHR$0 SYS "FreeLibrary", uxt% ENDPROC
CONTENTS |
HOME |