RTR logo

BBC BASIC for Windows

Accessing the Windows API

Introduction to the Windows API

BBC BASIC for Windows allows you to access the Windows™ Application Program Interface (API) by means of the SYS statement. There are many hundreds of API functions, the vast majority of which are unlikely to be of use to a BASIC program, and no attempt will be made to provide a comprehensive list here. Further details can be found in Microsoft documentation and on their web site.

The SYS statement allows you to call an API function either by name or by specifying its memory address. Only the most commonly used functions can be called by name, specifically those which are present in the following Dynamic Link Libraries, which are automatically loaded into memory when BBC BASIC for Windows is run:

Functions in other DLLs must be explicitly loaded into memory, and must be called by address rather than by name. For example to call the function OleUIChangeIconA which is in OLEDLG.DLL you must perform the following steps:
SYS "LoadLibrary", "OLEDLG.DLL" TO oledlg%
SYS "GetProcAddress", oledlg%, "OleUIChangeIconA" TO chicon%
SYS chicon%, ci% TO uint%
Once you have finished with them, it is important to 'release' any DLLs which you have loaded:
SYS "FreeLibrary", oledlg%
To ensure that the DLLs are released however your program exits, include code similar to the following in a 'cleanup' routine called from your ON CLOSE and ON ERROR handlers:
oledlg% += 0 : IF oledlg% THEN SYS "FreeLibrary", oledlg%

Changing the window title

Normally, the text in the title bar of BASIC's output window is set to the name of the BASIC program itself. You can change the title by using the SetWindowText function:
title$ = "New title"
SYS "SetWindowText", @hwnd%, title$

Flashing the title bar

You can control whether the title bar of BASIC's output window is highlighted (which usually means the window will accept keyboard input) or not by using the FlashWindow function. This can be used, for example, to alert the user that your program requires some input:
SYS "FlashWindow", @hwnd%, 1
SYS "FlashWindow", @hwnd%, 0
If the final parameter is 1 the state of the title bar is inverted (if it was highlighted it is un-highlighted; if it was not highlighted it becomes highlighted). If the final parameter is 0 the title bar is returned to its original state.

You will probably want to flash the title bar more than once to attract attention.

Finding the display size

You may wish to know the width and height of the desktop area in order to select an appropriate screen mode for your program. You can do that using the GetSystemMetrics function:
SYS "GetSystemMetrics", 0 TO xscreen%
SYS "GetSystemMetrics", 1 TO yscreen%
The above program segment will load the variable xscreen% with the width of the desktop and the variable yscreen% with the height of the desktop, both in pixels.

Displaying a message box

If you want to display a message in its own 'message window', you can use the MessageBox function:
message$ = "Test message"
caption$ = "Test caption"
SYS "MessageBox", @hwnd%, message$, caption$, 0
The final numeric value determines what kind of symbol is displayed and what options are presented to the user:
16Stop symbol
32Question mark
48Exclamation mark
64Information symbol
ValueUser options
1OK and Cancel
2Abort, Retry and Ignore
3Yes, No and Cancel
4Yes and No
5Retry and Cancel
6Cancel, Try Again and Continue (Windows 2000 or later only)
The 'symbol' value and the 'options' value should be added together. When more than one option is offered, you can tell which was selected by storing the return value as follows:
SYS "MessageBox", @hwnd%, message$, caption$, 0 TO result%
The value of result% will be one of the following:
10Try Again
Note: If a dialogue box is open when you display the message, you should normally specify as the first parameter not @hwnd% but the window handle of the dialogue box itself, for example:
SYS "MessageBox", !dlg%, message$, caption$, 0
where dlg% is the value returned from FN_newdialog (note the exclamation point).

Updating the screen

BBC BASIC for Windows doesn't always update the screen immediately a change is made (for example if a line is drawn). If several things are being plotted in quick succession it is more efficient to wait until all the changes have been made and then update the screen. This is normally perfectly satisfactory, but there may be special circumstances (for example if displaying an animated graphic) when you need to force the display to update immediately. You can do that by calling the UpdateWindow function:
SYS "UpdateWindow", @hwnd%
See also the *REFRESH command.

Producing a warning sound

Although you can produce your own sounds using the SOUND and ENVELOPE statements, your program can also produce one of the standard Windows™ warning sounds by using the MessageBeep function:
beep% = 0
SYS "MessageBeep", beep%
The sound is determined by the value of beep%, as follows:
0Default sound
16Critical stop
The actual sound produced will depend on the user's sound scheme as selected in the Windows™ Control Panel.

Playing WAV files

If you want to play a sound which is stored in a standard WAV file, you can do that using the PlaySound function:
wave$ = "\windows\media\tada.wav"
SYS "PlaySound", wave$, 0, SND_FILENAME + SND_ASYNC
You can control the playback volume as follows:
SYS "waveOutSetVolume", -1, volume% + (volume% << 16)
where volume% is in the range 0 (minimum) to 65535 (maximum). Note that the volume change will affect all subsequent wave audio output.

Alternatively you can load the WAV file into memory and then play the sound from there. You can load the file using the following code:

wave$ = "\windows\media\tada.wav"
file% = OPENIN(wave$)
size% = EXT#file%
CLOSE #file%
DIM tada% size%-1
OSCLI "LOAD """+wave$+""" "+STR$~tada%
Then you can play the sound as many times as you like with:
SYS "PlaySound", tada%, 0, SND_MEMORY + SND_ASYNC
You can abort a sound which is already playing as follows:
SYS "PlaySound", 0, 0, 0
The PlaySound function also allows you to play 'system sounds':
SND_ALIAS = &10000
SYS "PlaySound", "SystemAsterisk", 0, SND_ALIAS + SND_ASYNC
SYS "PlaySound", "SystemExclamation", 0, SND_ALIAS + SND_ASYNC
SYS "PlaySound", "SystemExit", 0, SND_ALIAS + SND_ASYNC
SYS "PlaySound", "SystemHand", 0, SND_ALIAS + SND_ASYNC
SYS "PlaySound", "SystemQuestion", 0, SND_ALIAS + SND_ASYNC
SYS "PlaySound", "SystemStart", 0, SND_ALIAS + SND_ASYNC
Note that the SND_ALIAS signifies that a system sound should be played, whereas SND_FILENAME signifies that a WAV file should be played and SND_MEMORY signifies a sound in memory.

The PlaySound function may not work if your program has previously executed a SOUND statement. In that case, use SOUND OFF before calling PlaySound.

Checking for a sound card

If no suitable sound card is fitted, using the SOUND statement will result in the trappable Device unavailable error. Although it is rare to find a modern PC without a sound card, you may wish your programs to work even if one is not installed. You can test for the presence of a sound card using the waveOutGetNumDevs function:
SYS "waveOutGetNumDevs" TO ndevs%
If ndevs% is non-zero it should be safe to use the SOUND statement, although it can still fail if (for example) another program is currently using the sound system.

Timing program execution

You can discover how long Windows™ has been running by calling the GetTickCount function; this can be a useful adjunct to the built in TIME pseudo-variable:
SYS "GetTickCount" TO tick%
The value of tick% will be set to the number of milliseconds since Windows was started (it wraps around to zero if Windows has been running continuously for approximately 49 days and 17 hours!).

Pausing a program

A common way of pausing a program under software control is to use the INKEY function:
pause = INKEY(delay%)
or to use the TIME pseudo-variable:
TIME = 0
However both of these methods have their disadvantages. The INKEY delay can be truncated by pressing a key, which may be undesirable, and the TIME method keeps the processor fully occupied, so other applications will run slowly whilst your program is paused. A better method is to use WAIT:
WAIT delay%
This is probably the best method for long delays, but an alternative is to use the Sleep function:
SYS "Sleep", delay%
The program will pause for approximately delay% milliseconds. Note that during this time the ESCape key is not tested, nor can the window be closed. Therefore this method should be used only for short delays.

Reading the command line

You can discover the command which was issued to execute BBC BASIC for Windows (or your executable file if generated using the Compile utility) by calling the GetCommandLine function:
SYS "GetCommandLine" TO cmdline%
cmdline$ = $$cmdline%
The command line is returned in memory and is terminated with a NUL character (CHR$0). The $$ indirection operator converts it to a normal BASIC string.

If you simply want to know the command line 'tail' (i.e. everything after the filename) you can use the system variable @cmd$.

Reading a NUL-terminated string

BBC BASIC for Windows versions 5.10a and later support NUL-terminated strings directly using the $$ indirection operator. Earlier versions can use the following function to convert a NUL-terminated string in memory to a normal BASIC string:
DEF FNnulterm$(P%)
  A$ += CHR$?P%
  P% += 1
= A$

Finding the filename

If, rather than the entire command line, you just want know the path and filename of BBC BASIC for Windows (or of your executable program) you can use the GetModuleFileName function:
DEF FNgetmodulefilename
LOCAL filename%
DIM filename% LOCAL 260
SYS "GetModuleFileName", 0, filename%, 260
= $$filename%
To discover the directory from which your program was loaded you can use the @dir$ system variable.

Discovering an 'unknown error'

If Windows reports an error condition which BASIC was not expecting, an Unknown error (error code 255) results. You can discover the true cause of the error by using the GetLastError and FormatMessage functions:
DEF FNwinerror
LOCAL message%, winerr%
DIM message% LOCAL 255
SYS "GetLastError" TO winerr%
SYS "FormatMessage", &1000, 0, winerr%, 0, message%, 255, 0
= $$message%

Repositioning the window

You can set the position of BASIC's output window without changing its size by calling the SetWindowPos function:
SYS "SetWindowPos", @hwnd%, 0, xpos%, ypos%, 0, 0, \
\                   SWP_NOSIZE + SWP_NOZORDER
The position is specified as the offset, in pixels, from the top-left corner of the desktop to the top-left corner of BASIC's output window. To change the window's size without moving it you can do the following:
SYS "SetWindowPos", @hwnd%, 0, 0, 0, width%, height%, \
\                   SWP_NOMOVE + SWP_NOZORDER
VDU 26
The width and height are specified in pixels, including the border and the title bar. If want to specify the dimensions excluding the border and title bar, i.e. of the region usable by BASIC, then the best way of doing it is to select a user-defined mode with VDU 23,22.... However an alternative method is as follows:
DIM rc{l%,t%,r%,b%}
rc.l% = 0
rc.t% = 0
rc.r% = width%
rc.b% = height%
SYS "AdjustWindowRect", rc{}, &CF0000, 0
SYS "SetWindowPos", @hwnd%, 0, 0, 0, rc.r%-rc.l%, rc.b%-rc.t%, 6
VDU 26
If you want to alter both the size and the position you can use the MoveWindow function:
SYS "MoveWindow", @hwnd%, xpos%, ypos%, width%, height%, 1
VDU 26
Whenever you intentionally change the window size, you should reset BASIC's text and graphics clipping regions to the new size by issuing the VDU 26 command.

To discover the current size of the window you can use the GetClientRect function, which returns the width and height in pixels:

DIM rc{l%,t%,r%,b%}
SYS "GetClientRect", @hwnd%, rc{}
Width% = rc.r%
Height% = rc.b%
This gives the usable size of the window, i.e. excluding the title bar etc. Should you need to know the full size (and position) you can do the following:
DIM rc{l%,t%,r%,b%}
SYS "GetWindowRect", @hwnd%, rc{}
Xpos% = rc.l%
Ypos% = rc.t%
Width% = rc.r% - rc.l%
Height% = rc.b% - rc.t%
To find the 'normal' size of the window, even if it is maximised or minimised, you can use the GetWindowPlacement function:
DIM wp{length%,flags%,showcmd%, minpos{x%,y%}, \
\      maxpos{x%,y%}, normal{l%,t%,r%,b%}}
wp.length% = DIM(wp{})
SYS "GetWindowPlacement", @hwnd%, wp{}
Width% = wp.normal.r% - wp.normal.l%
Height% = wp.normal.b% - wp.normal.t%
Don't use the window position returned by GetWindowPlacement since it is in workspace (not screen) coordinates and probably won't be what you want if the Taskbar is at the top of the screen.

Fixing the window size

Normally the user can re-size the output window by dragging the side or corner to a new position. However there may be circumstances when you would prefer to fix the window size, and prevent it from being changed by the user. You can do that as follows:
SYS "GetWindowLong", @hwnd%, GWL_STYLE TO ws%
SYS "SetWindowLong", @hwnd%, GWL_STYLE, ws% AND NOT WS_THICKFRAME \
If necessary change the MODE 8 to whichever MODE is required, or replace it with a suitable VDU 23,22 command. It is important to execute a MODE or VDU 23 statement after changing the window style.

Minimising or maximising the window

You can minimise BASIC's output window under program control using the ShowWindow function:
SYS "ShowWindow", @hwnd%, SW_MINIMIZE 
This will have exactly the same effect as clicking on the minimise button in the title bar, or right-clicking on the title bar and selecting Minimize from the menu. The ShowWindow function can also be used to restore the window to its original size and position, maximise the window (equivalent to clicking on the maximise button) or hide the window (so it appears neither on the desktop nor in the taskbar):
SYS "ShowWindow", @hwnd%, SW_RESTORE
SYS "ShowWindow", @hwnd%, SW_MAXIMIZE
SYS "ShowWindow", @hwnd%, SW_HIDE
If you maximise the window you should normally use a VDU 26 to reset the text and graphics viewports so they fill the full area:
SYS "ShowWindow", @hwnd%, SW_MAXIMIZE
VDU 26
If you hide the window, you are liable to confuse the BBC BASIC for Windows IDE when you exit your program. To prevent this you should execute the following code before quitting:
SYS "ShowWindow", @hwnd%, SW_NORMAL
SYS "SetForegroundWindow", @hwnd%

Forcing the window to stay on top

You can set your window to be 'topmost', thus forcing it to appear on top of all other non-topmost windows, by using the SetWindowPos function:
SYS "SetWindowPos", @hwnd%, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE+SWP_NOMOVE
The above example leaves the window position and size unchanged, but you can if you like change these at the same time:
SYS "SetWindowPos", @hwnd%, HWND_TOPMOST, x%, y%, cx%, cy%, 0
VDU 26
Here x%,y% is the position and cx%,cy% the size (using the same units as described previously for Repositioning the window).

If you don't need your window to stay on top, but simply want to move it to the top, you can use the BringWindowToTop function:

SYS "BringWindowToTop", @hwnd%

Removing the title bar

You can remove the title bar and system menu from the output window as follows:
WS_BORDER = &800000
SYS "GetWindowLong", @hwnd%, GWL_STYLE TO ws%
SYS "SetWindowLong", @hwnd%, GWL_STYLE, ws% AND NOT WS_BORDER
SYS "SetWindowPos", @hwnd%, 0, 0, 0, 0, 0, SWP_NOSIZE + \
As this removes the close button, make sure you provide an obvious means for quitting the program (Alt-F4 will still work so long as you haven't used ON CLOSE).

Using the entire screen

If you want your program to run 'full screen', without even a title bar or borders, you can do that as follows:
WS_VISIBLE = &10000000
SYS "GetSystemMetrics", 0 TO xscreen%
SYS "GetSystemMetrics", 1 TO yscreen%
SYS "SetWindowLong", @hwnd%, GWL_STYLE, WS_VISIBLE + \
SYS "SetWindowPos", @hwnd%, HWND_TOPMOST, 0, 0, xscreen%, yscreen%, 0
VDU 26
When you run your program not even the Windows™ taskbar will be visible, nor will the user be able to re-size or move the window, so make sure you provide an obvious means for quitting the program (Alt-F4 will still work so long as you haven't used ON CLOSE).

Bear in mind that display resolutions vary, from 640 x 480 pixels to 1920 x 1440 pixels or more, and widescreen (16:9) displays are becoming increasingly common. Ideally you should write your program in such a way that it will behave sensibly with any display resolution and shape. If that isn't practical you can check that the dimensions are suitable (xscreen% and yscreen% in the above example contain the width and height in pixels) and prompt the user if not.

In the unlikely event that the screen dimensions exceed 1920 x 1440 pixels it is important that you do not issue the VDU 26 listed above. Instead you should use the code provided at Using windows larger than 1920 x 1440 pixels.

Drawing graphics to a printer

One way of outputting graphics to a printer is to first display them on the screen, then print them using *HARDCOPY. However this approach has a major disadvantage: the quality of the graphics is limited to the quality of the screen display (printers typically have resolutions of 600 or more dots-per-inch, compared to a typical value for the screen of 96 dpi). Whilst this can be partially overcome by creating a sufficiently large output 'canvas' (which can be bigger than your screen) it still imposes some limitations.

The solution is to output the graphics directly to the printer. Unfortunately you cannot use BASIC's built-in graphics statements to do this, you must use the Windows™ API instead. However this is quite easy, and there are equivalents for all of the normal graphics operations, for example:

BASIC statementPrinter API equivalent
MOVE x,y SYS "MoveToEx",@prthdc%,x,y,0
DRAW x,y SYS "LineTo",@prthdc%,x,y
RECTANGLE x,y,dx,dy  SYS "Rectangle",@prthdc%,x1,y1,x2,y2
ELLIPSE x,y,a,b SYS "Ellipse",@prthdc%,x1,y1,x2,y2

The main differences are that the coordinate system is different (vertical coordinates are measured downwards, with the origin at the top of the page) and the resolution in dots-per-inch varies between printers. If you only ever expect to output to a particular printer then you can use absolute coordinates, but it is far better to scale your graphics output so that it will look correct whatever the printer. There are two ways of achieving that, scaling to a particular size (in inches or cm) or scaling to fit the size of the paper.

To scale your printed graphics to a particular size you can find the resolution of the printer in dots-per-inch as follows:

SYS "GetDeviceCaps", @prthdc%, _LOGPIXELSX TO dpix%
SYS "GetDeviceCaps", @prthdc%, _LOGPIXELSY TO dpiy%
The two values will often be the same. To scale your graphics to fit the paper you can discover the coordinates of the left, right, top and bottom margins as follows:
marginl% = @vdu%!232
marginr% = @vdu%!236
margint% = @vdu%!240
marginb% = @vdu%!244
As usual you need to output at least one conventional character to the printer at the start; you may want to print a title anyway, but if not you can simply output a space. The following code draws a straight line diagonally across the page, according to the current *MARGINS setting:
VDU 2,1,32,3
SYS "MoveToEx", @prthdc%, @vdu%!232, @vdu%!240, 0
SYS "LineTo", @prthdc%, @vdu%!236, @vdu%!244
VDU 2,1,12,3
You may find that the line is too thin, or you want to print it in a colour other than black. You can change the line thickness and colour as follows:
VDU 2,1,32,3
pcol% = red% + (green% << 8) + (blue% << 16)
SYS "CreatePen", 0, thickness%, pcol% TO pen%
SYS "SelectObject", @prthdc%, pen%
SYS "MoveToEx", @prthdc%, @vdu%!232, @vdu%!240, 0
SYS "LineTo", @prthdc%, @vdu%!236, @vdu%!244
VDU 2,1,12,3
The colour must be specified as the amounts of red, green and blue, in the range 0 (none) to 255 (maximum).

Printing coloured text

If you have a colour printer, you may want to print text in different colours. You can do this using the SetTextColor function:
pcol% = red% + (green% << 8) + (blue% << 16)
SYS "SetTextColor", @prthdc%, pcol%
This sets the colour of subsequently printed text according to the amounts of red, green and blue specified, in the range 0 (none) to 255 (maximum). For example:
SYS "SetTextColor", @prthdc%, &FF0000
would result in blue text.

You should ensure that at least one text character is sent to the printer before issuing the SetTextColor call, otherwise it might not be effective. You may want to print some black text anyway but if not you can simply send an initial space character:

VDU 2,1,32,3

Printing in landscape format

You may want to produce your printed output in landscape orientation (i.e. with the long edge of the paper horizontal). You can achieve that by executing the following procedure before outputting data to the printer:
DEF PROClandscape
LOCAL psd%, dm%
DIM psd% LOCAL 83
!psd% = 84
psd%!16 = 1024
SYS "PageSetupDlg", psd%
SYS "GlobalLock", psd%!8 TO dm%
dm%!40 = 1
dm%?44 = 2
SYS "ResetDC", @prthdc%, dm%
SYS "GlobalUnlock", psd%!8
SYS "GlobalFree", psd%!8
SYS "GlobalFree", psd%!12
*MARGINS 10,10,10,10
The *MARGINS command is necessary for BBC BASIC for Windows to take note of the changed page dimensions. If you want to set the margins to values other than the default (10 mm), specify the appropriate values in the command.

To set the format back to portrait, change the 2 to 1 in the ninth line:

dm%?44 = 1
As an alternative, and possibly more reliable, method you can present the user with the Print, Print Setup or Page Setup dialogue box which will allow him not only to select the required orientation but also the printer, printer options (e.g. print quality) and paper size.

Adding a menu bar

A rather more complicated, but very valuable, use for the Windows™ API functions is to add a menu bar to your program. Below is a complete program which uses the CreateMenu, SetMenu, AppendMenu and DrawMenuBar functions to implement a simple menu. Clicking on the menu items causes the background colour of the program window to change:
SYS "CreateMenu" TO hmenu%
SYS "SetMenu", @hwnd%, hmenu%
SYS "AppendMenu", hmenu%, 0, 0, "Blac&k"
SYS "AppendMenu", hmenu%, 0, 1, "&Red"
SYS "AppendMenu", hmenu%, 0, 2, "&Green"
SYS "AppendMenu", hmenu%, 0, 3, "&Yellow"
SYS "AppendMenu", hmenu%, 0, 4, "&Blue"
SYS "AppendMenu", hmenu%, 0, 5, "&Magenta"
SYS "AppendMenu", hmenu%, 0, 6, "&Cyan"
SYS "AppendMenu", hmenu%, 0, 15, "&White"
SYS "DrawMenuBar", @hwnd%
Click% = -1
ON SYS Click% = @wparam% : RETURN
  WAIT 1
  click% = -1
  SWAP click%,Click%
  IF click%<>-1 THEN COLOUR 128+click% : CLS
The program creates a menu bar containing eight items, consisting of the colour names listed. In each case one letter of the name is preceded by an ampersand (&): this determines the keyboard shortcut letter associated with the menu item.

The ON SYS statement is activated whenever a menu item is selected (either by clicking with the mouse or using the keyboard shortcut). The system variable @wparam% contains a menu identifier which is equal to the third parameter of the relevant AppendMenu function (these values have been chosen to correspond directly to colour numbers), and this is copied to the global variable Click%.

The value of Click% is monitored by polling it within the program's main loop; when its value has been modified by the menu selection the background colour is changed.

If for any reason you want to remove the menu bar, you can do that as follows:

SYS "SetMenu", @hwnd%, 0
however in that case you should make sure you destroy the menu before quitting your program, so that the memory it uses is released:
SYS "DestroyMenu", hmenu%

Adding popup and sub-menus

The foregoing example simply generates a menu bar with a number of clickable items. More usually these top-level items activate 'popup' (or 'drop down') menus each containing a set of selectable items (or their own sub-menus). The following example program is functionally compatible with the previous one, but the colour selection is provided with popup and sub-menus:
SYS "CreatePopupMenu" TO hsub%
SYS "AppendMenu", hsub%, 0, 0, "&Black"
SYS "AppendMenu", hsub%, 0, 15, "&White"
SYS "CreatePopupMenu" TO hpop1%
SYS "AppendMenu", hpop1%, 0, 1, "&Red"
SYS "AppendMenu", hpop1%, 0, 2, "&Green"
SYS "AppendMenu", hpop1%, 0, 4, "&Blue"
SYS "CreatePopupMenu" TO hpop2%
SYS "AppendMenu", hpop2%, 0, 3, "&Yellow"
SYS "AppendMenu", hpop2%, 0, 5, "&Magenta"
SYS "AppendMenu", hpop2%, 0, 6, "&Cyan"
SYS "AppendMenu", hpop2%, 16, hsub%, "&Others"
SYS "CreateMenu" TO hmenu%
SYS "AppendMenu", hmenu%, 16, hpop1%, "&Primary"
SYS "AppendMenu", hmenu%, 16, hpop2%, "&Secondary"
SYS "SetMenu", @hwnd%, hmenu%
SYS "DrawMenuBar", @hwnd%
Click% = -1
ON SYS Click% = @wparam% : RETURN
  WAIT 1
  click% = -1
  SWAP click%,Click%
  IF click%<>-1 THEN COLOUR 128+click% : CLS
Note that in the AppendMenu calls which specify popup or sub-menus the second parameter is set to 16 rather than to zero. In this case the third parameter is set to the handle of the popup menu (hpop1% or hpop2%) or sub-menu (hsub%) rather than an arbitrary ID number.

To insert one or more separators (horizontal dividing lines) within the popup or sub-menu, add the following statement at the appropriate point(s):

SYS "AppendMenu", hpop2%, &800, 0, 0

Ticking a menu item

You can place (or remove) a tick mark against an item in a popup menu by using the CheckMenuItem function:
SYS "CheckMenuItem", hpopup%, itemid%, 8
SYS "CheckMenuItem", hpopup%, itemid%, 0
The first parameter is the handle of the popup menu (as returned by CreatePopupMenu), the second parameter is the menu item identifier (specified as the third parameter of AppendMenu) and the third parameter is 8 to add a tick mark or 0 to remove the tick mark.

Disabling a menu item

You can disable (or enable) an item in a popup menu by using the EnableMenuItem function:
SYS "EnableMenuItem", hpopup%, itemid%, 0
SYS "EnableMenuItem", hpopup%, itemid%, 1
The first parameter is the handle of the popup menu (as returned by CreatePopupMenu), the second parameter is the menu item identifier (specified as the third parameter of AppendMenu) and the third parameter is 0 to enable the item and 1 to disable the item (and change it to grey to indicate that it is disabled). You can also use the value 2 which disables the item but does not make it grey.

An alternative method of specifying the item you want to disable (or enable) is to give its position in the menu rather than its ID; this is particularly useful when the item is a sub-menu (which doesn't have an ID as such). To do this you add &400 to the third parameter:

SYS "EnableMenuItem", hpopup%, itempos%, &400
SYS "EnableMenuItem", hpopup%, itempos%, &401
If the menu item you are enabling or disabling is visible, you will need to update the display in order for its appearance to reflect its state (e.g. greyed-out when disabled):
SYS "DrawMenuBar", @hwnd%
You can discover the current state of a menu item using the GetMenuState function:
SYS "GetMenuState", hpopup%, itemid%, 0 TO state%
The returned variable state% will be a combination of one or more values, including:
1   Greyed

Deleting and inserting menu items

You can delete an item in a popup menu by using the DeleteMenu function:
SYS "DeleteMenu", hpopup%, itemid%, 0
The first parameter is the handle of the popup menu (as returned by CreatePopupMenu) and the second parameter is the menu item identifier (specified as the third parameter of AppendMenu).

An alternative method of specifying the item you want to delete is to give its position in the menu rather than its ID; this is particularly useful when the item is a sub-menu (which doesn't have an ID as such). To do this you specify &400 as the third parameter:

SYS "DeleteMenu", hpopup%, itempos%, &400
You can insert a new item into an existing menu using the InsertMenu function:
SYS "InsertMenu", hpopup%, position%, 0, itemid%, "Name"
SYS "InsertMenu", hpopup%, position%, 16, hsubmenu%, "Name"
The parameters are the same as for AppendMenu apart from position% which specifies where in the menu the new item should be inserted.

Creating a toolbar

The procedure for creating a toolbar is sufficiently complex that it has been provided in a Library; see FN_createtoolbar for details. You can, of course, cut and paste the relevant code from the library file into your own program if you prefer.

Creating a status bar

The procedure for creating a status bar is sufficiently complex that it has been provided in a Library; see FN_createstatusbar for details. You can, of course, cut and paste the relevant code from the library file into your own program if you prefer.

Using the clipboard

You may want your program to be able to access the clipboard, either to put text on the clipboard or to read text from the clipboard. The following program segment writes the string text$ to the clipboard; if you want to include a new-line you should append a carriage-return, line-feed pair to the string as shown in the example:
text$ = "The five boxing wizards jump quickly"+CHR$13+CHR$10
SYS "GlobalAlloc", &2000, LEN(text$)+1 TO hdata%
SYS "GlobalLock", hdata% TO tmp%
$$tmp% = text$
SYS "GlobalUnlock", hdata%
SYS "OpenClipboard", @hwnd%
SYS "EmptyClipboard"
SYS "SetClipboardData", 1, hdata%
SYS "CloseClipboard"
To check whether the clipboard contains any text, you can use the following function:
SYS "IsClipboardFormatAvailable", 1 TO res%
which will set res% to 1 if there is text in the clipboard and to 0 otherwise. Once you have established that there is text available, you can read it with the following program segment:
SYS "OpenClipboard", @hwnd%
SYS "GetClipboardData", 1 TO hdata%
IF hdata% THEN
  SYS "GlobalLock", hdata% TO tmp%
  text$ = $$tmp%
  SYS "GlobalUnlock", hdata%
  REM Do something with text$
SYS "CloseClipboard"

Using dialogue boxes

The procedure for creating a custom dialogue box is sufficiently complex that it has been provided in a Library; see FN_newdialog for details. You can, of course, cut and paste the relevant code from the library file into your own program if you prefer.

Alternatively, and more easily, your BASIC program can utilise one or more of the standard dialogue boxes provided by Windows™, for example the File Open (or File Save), Choose Colour, Choose Font, Browse for Folder, Print, Print Setup or Page Setup dialogue boxes. In each case you must create and initialise a data structure in memory before calling the Dialogue Box function. The examples given below illustrate only the simplest use of the dialogue boxes; there are many more options available which are beyond the scope of this manual but may be found in Microsoft documentation.

File Open and File Save

To use the File Open or File Save dialogue you must first create the data structure fs{} as follows:
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)}
ff$ = "BMP files"+CHR$0+"*.BMP"+CHR$0+CHR$0
fs.lStructSize% = DIM(fs{})
fs.hwndOwner% = @hwnd%
fs.lpstrFilter% = PTR(ff$)
fs.lpstrFile% = fp{}
fs.nMaxFile% = DIM(fp{}) - 1
fs.flags% = 6
The string ff$ specifies a file filter which tells the dialogue box function what file type(s) it should display by default. In the example shown the BMP file type is specified. You can specify more than one type by adding an appropriate description and extension for each, for example:
ff$ = "BMP files"+CHR$0+"*.BMP"+CHR$0+"GIF files"+CHR$0+"*.GIF"+CHR$0+CHR$0
The string is terminated by two CHR$0 characters. If you do not supply a filter string, all file types will be displayed.

To list more than one file type under the same description, separate the extensions with semicolons:

ff$ = "Image files"+CHR$0+"*.BMP;*.GIF;*.JPG"+CHR$0+CHR$0
Once you have created and initialised the data structure you can call the File Open dialogue as follows:
SYS "GetOpenFileName", fs{} TO result%
IF result% filename$ = $$fp{}
If the returned value is non-zero, the selected filename can be found in memory at fp{}. If the returned value is zero, the file selection failed (for example the user selected Cancel).

You can call GetSaveFileName rather than GetOpenFileName. This will display the Save As title and will prompt the user if the selected file already exists.

Choose Colour

To use the Choose Colour dialogue ('colour picker') you must first create the data structure cc{} as follows:
DIM cc{lStructSize%, hwndOwner%, hInstance%, \
\      rgb{r&,g&,b&,z&}, lpCustColors%, flags%, \
\      lCustData%, lpfnHook%, lpTemplateName%}
DIM cb%(15)
cc.lStructSize% = DIM(cc{})
cc.hwndOwner% = @hwnd%
cc.lpCustColors% = ^cb%(0)
Once you have created and initialised the data structure you can call the Choose Colour dialogue as follows:
SYS "ChooseColor", cc{} TO result%
IF result% COLOUR 1, cc.rgb.r&, cc.rgb.g&, cc.rgb.b&
If the returned value is non-zero, the selected colour can be found in memory (red at cc.rgb.r&, green at cc.rgb.g& and blue at cc.rgb.b&). In the example shown this is used to set the physical colour of logical colour 1 (see COLOUR for details). If the returned value is zero, the colour selection failed (for example the user selected Cancel).

Choose Font

To use the Choose Font dialogue you must first create the data structures lf{} and cf{} as follows:
DIM lf{Height%, Width%, Escapement%, Orientation%, \
\      Weight%, Italic&, Underline&, StrikeOut&, \
\      CharSet&, OutPrecision&, ClipPrecision&, \
\      Quality&, PitchAndFamily&, FaceName&(30)}
DIM cf{lStructSize%, hwndOwner%, hdc%, lpLogFont%, \
\      iPointSize%, flags%, rgbColors%, lCustData%, \
\      lpfnHook%, lpTemplateName%, hInstance%, lpszStyle%, \
\      nFontType{l&,h&}, pad{l&,h&}, nSizeMin%, nSizeMax%}
cf.lStructSize% = DIM(cf{})
cf.hwndOwner% = @hwnd%
cf.lpLogFont% = lf{}
cf.flags% = 1
Once you have created and initialised the data structures you can call the Choose Font dialogue as follows:
SYS "ChooseFont", cf{} TO result%
If the returned value is non-zero, the selected font name can be found in memory at ^lf.FaceName&(0) and the font size (in decipoints) at cf.iPointSize%. If the returned value is zero, the font selection failed (for example the user selected Cancel). If you also want to know the font style (i.e. bold or italic) you can discover these as follows:
bold% = lf.Weight% >= 600
italic% = lf.Italic& <> 0
You can use this information to set the current font, size and style (see *FONT for details):
SYS "ChooseFont", cf{} TO result%
IF result% THEN
  font$ = lf.FaceName&()+","+STR$(cf.iPointSize%/10)
  style$ = ""
  IF lf.Weight% > 600 style$ += "B"
  IF lf.Italic& <> 0 style$ += "I"
  IF style$ <> "" font$ += "," + style$
  OSCLI "FONT "+font$

Browse for Folder

To use the Browse for Folder dialogue you must first create the data structure bi{} as follows, where title$ contains a title which can be used to prompt the user:
DIM bi{hOwner%, pidlRoot%, pszDisplayName%, \
\      lpszTitle%, ulFlags%, lpfn%, lParam%, iImage%}
DIM folder{t&(260)}
titlez$ = title$+CHR$0
bi.hOwner% = @hwnd%
bi.pszDisplayName% = folder{}
bi.lpszTitle% = PTR(titlez$)
Once you have created and initialised the data structure you can call the Browse for Folder dialogue as follows:
SYS "SHBrowseForFolder", bi{} TO pidl%
IF pidl% SYS "SHGetPathFromIDList", pidl%, folder{}
folder$ = $$folder{}
SYS "SHGetMalloc", ^malloc%
SYS !(!malloc%+20), malloc%, pidl% : REM. IMalloc::Free
If the value of pidl% is non-zero the path to the selected folder will be found (as a NUL-terminated string) at address folder%. In the above example this is converted to a standard BASIC string folder$ using the $$ indirection operator. If the user selected the Cancel button the value of pidl% will be zero.

Print dialogue

To use the Print dialogue you must first create the data structure pd{} as follows:
DIM pd{lStructSize%, hwndOwner%, hDevMode%, hDevNames%, \
\      hdc%, flags%, nFromPage{l&,h&}, nToPage{l&,h&}, \
\      nMinPage{l&,h&}, nMaxPage{l&,h&}, nCopies{l&,h&}, \
\      hInstance%, lCustData%, lpfnPrintHook%, lpfnSetupHook%, \
\      lpPrintTemplateName%, lpSetupTemplateName%, \
\      hPrintTemplate%, hSetupTemplate%}

pd.lStructSize% = DIM(pd{})
pd.hwndOwner% = @hwnd%
pd.nMinPage.l& = 1
pd.nMaxPage.l& = 99
pd.flags% = &100 
Once you have created and initialised the data structure you can call the Print dialogue as follows:
SYS "PrintDlg", pd{} TO ok%
  SYS "DeleteDC", @prthdc%
  @prthdc% = pd.hdc%
  *MARGINS 10,10,10,10
  REM. Add your conventional printing code here.
You should add your printing code where shown. You can use the values returned from the Print dialogue to control printing according to the user's choices, for example the chosen number of copies is in pd.nCopies.l& and the selected page range (if any) is in pd.nFromPage.l& and pd.nToPage.l&.

The *MARGINS command is necessary for BBC BASIC for Windows to take note of the (possibly) changed page dimensions. If you want to set the margins to values other than the default (10 mm), specify the appropriate values in the command.

Print Setup dialogue

Microsoft discourages the use of the Print Setup dialogue and instead recommends the Print or Page Setup dialogue in new applications. If you do want to use the Print Setup dialogue you must first create the data structure pd{} as follows:
DIM pd{lStructSize%, hwndOwner%, hDevMode%, hDevNames%, \
\      hdc%, flags%, nFromPage{l&,h&}, nToPage{l&,h&}, \
\      nMinPage{l&,h&}, nMaxPage{l&,h&}, nCopies{l&,h&}, \
\      hInstance%, lCustData%, lpfnPrintHook%, lpfnSetupHook%, \
\      lpPrintTemplateName%, lpSetupTemplateName%, \
\      hPrintTemplate%, hSetupTemplate%}

pd.lStructSize% = DIM(pd{})
pd.hwndOwner% = @hwnd%
pd.flags% = &140
Once you have created and initialised the data structure you can call the Print Setup dialogue as follows:
SYS "PrintDlg", pd{} TO ok%
  SYS "DeleteDC", @prthdc%
  @prthdc% = pd.hdc%
  *MARGINS 10,10,10,10
On exit from this routine, assuming the user has selected OK rather than Cancel, the current printer settings will have been changed to those chosen by the user. These may include the printer itself, any printer-specific options (e.g. print quality), the paper size and the page orientation (portrait or landscape). Subsequent output to the printer from your BBC BASIC program will use these settings.

If you need to know what the new settings are (for example the paper orientation and size) you can do that as follows:

SYS "GlobalLock", pd.hDevMode% TO dm%
orientation% = dm%?44
papersize% = dm%?46
paperlength% = dm%!48 AND &FFFF
paperwidth% = dm%!48 >>> 16
SYS "GlobalUnlock", pd.hDevMode%
This will set orientation% to 1 for portrait and to 2 for landscape and papersize% to 0 for a custom size or to one of the following constants for a standard size:
1Letter8½ x 11 in
2Letter Small  8½ x 11 in
3Tabloid11 x 17 in
4Ledger17 x 11 in
5Legal8½ x 14 in
6Statement5½ x 8½ in
7Executive7¼ x 10½ in
8A3297 x 420 mm
9A4210 x 297 mm
10A4 Small210 x 297 mm
11  A5148 x 210 mm
Other values are used for less common paper sizes. In the case of a custom size (only), paperlength% and paperwidth% are set to the dimensions in tenths of a millimetre.

The *MARGINS command is necessary for BBC BASIC for Windows to take note of the (possibly) changed page dimensions. If you want to set the margins to values other than the default (10 mm), specify the appropriate values in the command.

Note that this is a rare occasion when it is correct to write to a system variable (in this case @prthdc%).

Page Setup dialogue

To use the Page Setup dialogue you must first create and initialise the data structure psd{} as follows:
DIM psd{lStructSize%, hwndOwner%, hDevMode%, hDevNames%, \
\      flags%, ptPaperSize{w%,h%}, rtMinMargin{l%,t%,r%,b%}, \
\      rtMargin{l%,t%,r%,b%}, hInstance%, lCustData%, \
\      lpfnPageSetupHook%, lpfnPagePaintHook%, \
\      lpPageSetupTemplateName%, hPageSetupTemplate%}
psd.lStructSize% = DIM(psd{})
psd.hwndOwner% = @hwnd%
psd.flags% = 10
psd.rtMargin.l% = 1000 : REM left
psd.rtMargin.t% = 1000 : REM top
psd.rtMargin.r% = 1000 : REM right
psd.rtMargin.b% = 1000 : REM bottom
(the values of 1000 are the default page margins in hundredths of a millimetre):

When you want to display the Page Setup dialogue (typically in response to a menu selection) execute the following code:

SYS "PageSetupDlg", psd{} TO ok%
  IF psd.hDevMode% THEN
    SYS "GlobalLock", psd.hDevMode% TO dm%
    SYS "ResetDC", @prthdc%, dm%
    SYS "GlobalUnlock", psd.hDevMode%
  OSCLI "MARGINS "+STR$(psd.rtMargin.l%DIV100)+","+ \
  \                STR$(psd.rtMargin.b%DIV100)+","+ \
  \                STR$(psd.rtMargin.r%DIV100)+","+ \
  \                STR$(psd.rtMargin.t%DIV100)
The paper size, orientation and margins will be changed according to the user's selections.

Using system colours

For maximum authenticity, you might want your BASIC program to take account of system-wide settings, such as the default window background colour (set in Display Properties... Appearance... Item... Window). This is normally white, but can be set to any other colour by the user. The following program segment sets the background colour to the current Windows™ default, then clears the screen so that BASIC's output window is filled with that colour:
SYS "GetSysColor", 5 TO winbk%
COLOUR 15, winbk%, winbk% >> 8, winbk% >> 16
COLOUR 128+15
The parameter value 5 selects the window background colour. Other values which you might want to use with GetSysColor are given in the following table:
ValueColour returned
1Desktop background
4Menu background
5Window background
7Menu text
8Window text

Loading or saving part of a file

To load a file into memory you can use the *LOAD command, which is very fast. However, this only allows you to load an entire file. If you want to load part of a file into RAM, the only way to do it with BASIC statements is one byte at a time:
DIM store% size%-1
file% = OPENIN(filename$)
PTR#file% = offset%
FOR addr% = store% TO store%+size%-1
  ?addr% = BGET#file%
NEXT addr%
CLOSE #file%
The above program segment loads memory addresses store% to store%+size%-1 inclusive with the part of the file starting at offset offset% from its beginning. This will work perfectly well, but is significantly slower than *LOAD. If you need to load a part of a file as quickly as possible, you can use the ReadFile API function:
DIM store% size%-1
file% = OPENIN(filename$)
PTR#file% = offset%
SYS "ReadFile", @hfile%(file%), store%, size%, ^temp%, 0
CLOSE #file%
Note the use of the system variable @hfile%() to discover the Windows™ file handle, and the need to allocate a 4-byte area of memory at temp% to hold data returned from the API function (the number of bytes loaded).

Similarly to write part of a file you can use the WriteFile function:

DIM store% size%-1
file% = OPENUP(filename$)
PTR#file% = offset%
SYS "WriteFile", @hfile%(file%), store%, size%, ^temp%, 0
CLOSE #file%
These functions are the Windows™ equivalent of the Acorn OSGBPB function.

If you need to call this routine multiple times ensure that you move the DIM statement so that it will be executed only once. If you don't, you may eventually run out of memory. Alternatively, consider using DIM LOCAL.

Aligning proportional-spaced text

Normally, centering or right-justifying text is straightforward, because the width of the text string can simply be determined from the number of characters (depending on the MODE in use, all characters are either 16, 32, or 64 BASIC graphics units wide). In MODE 8, for example, the display width of a string would be given by:
Width% = 16 * LEN(string$)
However, if you have used *FONT to change to a proportional-spaced character font, things are not so simple. Because the characters are different widths, there is no straightforward way of calculating the total display width of a string. In that case you can use the GetTextExtentPoint32 function (see also the built-in WIDTH function for an alternative method when available):
DIM size{cx%,cy%}
*FONT "Arial",24
SYS "GetTextExtentPoint32", @memhdc%, string$, LEN(string$), size{}
Width% = size.cx% * 2
Height% = size.cy% * 2
Note that the dimensions returned by GetTextExtentPoint32 are in pixels, so they are doubled to obtain values in BASIC graphics units (one pixel equals two graphics units).

Once you know the dimensions of the string you can calculate where it needs to be plotted to achieve the required alignment (e.g. centred or right-justified). Use MOVE to set the position then select VDU 5 mode before you PRINT the string to the screen.

If you want to achieve the same effect on the printer rather than the screen, it is slightly more complicated. Firstly, you must specify @prthdc% rather than @memhdc% in the GetTextExtentPoint32 function call; the dimensions returned will then be in printer units (and no doubling is required). Secondly, since there is no equivalent to VDU 5 mode for the printer, you must control the print position using the @vdu% system variable.

The following program segment prints a string centred on a particular column:

DEF PROCprintcentred(string$, column%)
LOCAL size{}
DIM size{cx%,cy%}
*MARGINS 10,10,10,10
SYS "GetTextExtentPoint32", @prthdc%, string$, LEN(string$), size{}
PRINT TAB(column%);
@vdu%!-12 -= size.cx%/2
PRINT string$
Here, @vdu%!−12 determines the horizontal print position, which is moved to the left by half the width of the string. To right-justify the string rather than centre it, delete the /2 at the end of the ninth line.

Displaying enhanced metafiles

Windows™ Enhanced Metafiles are files (usually with the extension .EMF) which contain pictures in a device-independent format. Unlike bitmaps (for example .BMP files) they do not have a fixed size in pixels. Metafiles can be displayed at different sizes, without the distortion sometimes associated with scaling a bitmap; they are also often smaller than the equivalent bitmap.

BBC BASIC for Windows has no built-in commands for displaying Enhanced Metafiles, but the following procedure illustrates how this may be achieved by means of calls to API functions:

DEF PROCenhmetafile(emffile$,left%,top%,right%,bottom%)
LOCAL rect{}, hemf%
DIM rect{l%,t%,r%,b%}
SYS "GetEnhMetaFile", emffile$ TO hemf%
IF hemf% = 0 ERROR 214, "File "+emffile$+" not found"
rect.l% = left%
rect.t% = top%
rect.r% = right%
rect.b% = bottom%
SYS "PlayEnhMetaFile", @memhdc%, hemf%, rect{}
SYS "InvalidateRect", @hwnd%, rect{}, 0
SYS "DeleteEnhMetaFile", hemf%
SYS "UpdateWindow", @hwnd%
The position and size of the displayed image are determined by the values stored in the structure rect. The values of left%, top%, right% and bottom% must be specified in pixels, and are measured from the top-left corner of BASIC's output window. If necessary, these values must be converted from BASIC graphics units, taking into account that one pixel corresponds to two graphics units, and that BASIC's graphics origin is usually the bottom-left corner of the window.

You may also encounter an older kind of Windows™ Metafile with a .WMF extension. The program WMF2EMF, supplied with BBC BASIC for Windows in the EXAMPLES folder, can be used to convert a .WMF file to a .EMF file.

Using multimedia timers

For critical timing applications, Windows™ provides multimedia or interrupt timers. These are more accurate and provide a finer resolution that the normal timer services (e.g. the TIME pseudo-variable, the INKEY function, the WAIT statement and the Sleep API function). The multimedia timers are most easily accessed using the TIMERLIB library but an alternative method using assembly language code is listed below. For the purposes of illustration, the timer interrupt simply increments an integer value Count%.
DIM tc{PeriodMin%, PeriodMax%}
DIM P% 10
inc dword [^Count%]
ret 20
Interval% = 5
SYS "timeGetDevCaps", tc{}, 8 TO res%
IF res% ERROR 100, "Multimedia timer not available"
SYS "timeBeginPeriod", tc.PeriodMin%
SYS "timeSetEvent", Interval%, tc.PeriodMin%, TimerProc, 0, 1 TO TimerID%
IF TimerID% = 0 ERROR 100, "Could not start timer" 
ON CLOSE SYS "timeKillEvent", TimerID% : QUIT
ON ERROR SYS "timeKillEvent", TimerID% : PRINT 'REPORT$ : END
  PRINT Count%
The value of Interval% determines the period of the timer interrupt, in milliseconds. In this case it is set to a period of 5 ms (a frequency of 200 Hz). The minimum period you can use is system dependent, and is returned from the timeGetDevCaps function as tc.PeriodMin% (in milliseconds). Note that interrupt timers are a scarce resource, so you must always free the timer with timeKillEvent when you have finished with it. You should also be aware that short-period timers can consume a significant portion of CPU time.

Because it is called as the result of an interrupt, there are restrictions on what you can do within the assembly-language timer routine. The details are beyond the scope of this document, but the safest thing to do is to restrict the code to performing calculations on, and modifying, data values stored in memory.

Changing the process priority

There may be rare occasions when your BASIC program has to perform a time-critical operation, during which time you don't want any other programs (or the operating system) to interrupt it. You can achieve this by setting the priority of your process to its highest possible value:
SYS "GetCurrentProcess" TO hprocess%
SYS "SetPriorityClass", hprocess%, &100
The following program segment restores the priority to its normal value:
SYS "GetCurrentProcess" TO hprocess%
SYS "SetPriorityClass", hprocess%, &20
You should use this facility with extreme care, because while the priority of your program is raised other programs may run very slowly or not at all. You may not even be able to stop or interrupt your own program! You should raise the priority only for the shortest possible period, and then return it to normal.

Using scroll bars

If your program outputs more data than will fit on the screen at any one time, you may want to incorporate a scroll bar to allow the user to view the region of interest. Although BBC BASIC for Windows has no built-in support for scroll bars, you can implement them relatively easily with API calls. Using a scroll bar involves four steps: creating the scroll bar, initialising the scroll bar, processing scroll commands and actually scrolling the display.

Creating a scroll bar

The following program segment will create and display a vertical scroll bar at the right-hand side of your program's output window:
WS_HSCROLL = &100000
WS_VSCROLL = &200000
SYS "GetWindowLong", @hwnd%, GWL_STYLE TO ws%
SYS "SetWindowLong", @hwnd%, GWL_STYLE, ws% OR WS_VSCROLL
MODE md%
VDU 26
where md% is the screen mode you wish to use. Note the extra VDU 26 which causes the text and graphics clipping regions to be re-sized to take account of the presence of the scroll bar. To create a horizontal scroll bar rather than a vertical scroll bar change the WS_VSCROLL to WS_HSCROLL; to create both a vertical and a horizontal scroll bar change it to (WS_HSCROLL+WS_VSCROLL)

If you subsequently want to remove the scroll bar(s) you can do that as follows:

SYS "ShowScrollBar", @hwnd%, SB_BOTH, 0
VDU 26
To restore the scroll bar(s) thereafter you can do:
SYS "ShowScrollBar", @hwnd%, SB_BOTH, 1
VDU 26
This command restores both vertical and horizontal scroll bars. To restore just a vertical bar change the SB_BOTH to SB_VERT (1); to restore just a horizontal bar change it to SB_HORZ (0).

Initialising the scroll bar

Once you have created the scroll bar you must set the scroll range. Normally this will correspond to the number of lines of text (in the case of a vertical scroll bar) or characters (in the case of a horizontal scroll bar) through which you want to scroll. This can be achieved for a vertical scroll bar as follows:
SYS "SetScrollRange", @hwnd%, SB_VERT, minscroll%, maxscroll%, 0
In most cases you would set minscroll% to zero and maxscroll% to the total number of data lines minus the number of lines displayed at any one time. So if the total number of data lines was 100 and the number displayed was 32 you would set maxscroll% to 68. For a horizontal scroll bar replace the SB_VERT with a SB_HORZ (0).

Processing scroll commands

You can intercept scroll commands using the ON MOVE statement, as follows:
ON MOVE PROCscroll(@msg%,@wparam%) : RETURN
For a vertical scroll bar @msg% will have the value &115 and the low word of @wparam% will have one of the values 0 (scroll up one line), 1 (scroll down one line), 2 (scroll up one 'page'), 3 (scroll down one 'page') or 5 (scroll to the position indicated by the high word of @wparam%). A suitable PROCscroll would be something like the following:
DEF PROCscroll(msg%,wp%)
  WHEN 0: vscroll% -= 1
  WHEN 1: vscroll% += 1
  WHEN 2: vscroll% -= vpage% 
  WHEN 3: vscroll% += vpage%
  WHEN 5: vscroll% = wp% >> 16
IF vscroll% < minscroll% vscroll% = minscroll%
IF vscroll% > maxscroll% vscroll% = maxscroll%
SYS "SetScrollPos", @hwnd%, SB_VERT, vscroll%, 1
Here vscroll% is the wanted scroll position and vpage% is the number of lines to scroll when the scroll bar is clicked above or below the thumb box (typically the number of displayed lines).

For a horizontal scroll bar change WM_VSCROLL to WM_HSCROLL (&114) and change the SB_VERT to SB_HORZ (0). Also replace vscroll% by hscroll% and vpage% by hpage% throughout.

If you want more control over the scroll bars, you can use the SetScrollInfo API.

Scrolling the display

The value of vscroll% identifies which data item should be the first to be displayed. Your program must respond to changes in this variable by scrolling the display appropriately. The method used to perform the actual scrolling will depend on the nature of the data to be displayed and how it is stored. One method, appropriate when the data is stored in a string array, can be found in the supplied example program VSCROLL.BBC (in the WINDOWS folder). Another, appropriate for scrolling over a large graphics canvas (up to 1920 x 1440 pixels), is illustrated in the example program SCROLL.BBC (in the GENERAL folder).

In the case of a vertical scroll bar you can take advantage of BASIC's VDU drivers to scroll the visible display (text viewport). To scroll the display down you should position the cursor on the top line of the text viewport and issue a VDU 11 command. Similarly to scroll the display up you should position the cursor in the bottom line and issue a VDU 10. Alternatively you can use VDU 23,7 to scroll the display in any direction.

When the screen is scrolled vertically a blank line is 'scrolled' into the top line or the bottom line respectively. To simulate scrolling through a greater amount of data than the display can show, your program must update the top or bottom line with the appropriate data item as soon as the scroll has taken place. Depending on the nature of your program, this data may either be read from a RAM buffer (e.g. an array), read from a file or generated 'on the fly'.

Displaying GIF and JPEG images

The built-in *DISPLAY command will only display images in Windows Bitmap (.BMP) format. If you want to display images in other formats, for example JPG, PNG or GIF, you should use the IMGLIB library.

In the special case of wanting to display Windows Icon (.ICO) or Windows Metafile (.WMF) images you can use the following procedure (remember that you can copy and paste this code from the help window into your program):

DEF PROCdisplay(picture$,xpos%,ypos%,xsize%,ysize%)
LOCAL oleaut32%, olpp%, iid%, gpp%, hmw%, hmh%, picture%, res%

SYS "LoadLibrary", "OLEAUT32.DLL" TO oleaut32%
SYS "GetProcAddress", oleaut32%, "OleLoadPicturePath" TO olpp%
IF olpp%=0 ERROR 100, "Could not get address of OleLoadPicturePath"

DIM iid% LOCAL 15, picture% LOCAL 513
SYS "MultiByteToWideChar", 0, 0, picture$, -1, picture%, 256

iid%!0  = &7BF80980
iid%!4  = &101ABF32
iid%!8  = &AA00BB8B
iid%!12 = &AB0C3000

SYS olpp%, picture%, 0, 0, 0, iid%, ^gpp%
IF gpp% = 0 ERROR 100, "OleLoadPicturePath failed"

SYS !(!gpp%+24), gpp%, ^hmw% : REM. IPicture::get_Width
SYS !(!gpp%+28), gpp%, ^hmh% : REM. IPicture::get_Height
SYS !(!gpp%+32), gpp%, @memhdc%, xpos%, ypos%, xsize%, ysize%, 0, \
\                      hmh%, hmw%, -hmh%, 0 TO res%
IF res% ERROR 100, "IPicture::Render failed"

SYS !(!gpp%+8), gpp% : REM. IPicture::Release
SYS "InvalidateRect", @hwnd%, 0, 0
SYS "UpdateWindow", @hwnd%
Note that the required display position (xpos%, ypos%) must be specified in pixels, and is measured from the top-left corner of your program's output window to the top-left corner of the image. Note also that the file name must include the drive letter (e.g. C:) to distinguish it from a web URL, which can be used as an alternative means of specifying the image.

Listing the disk directory

Although you can list the contents of the current directory (folder) using the *DIR command, you have no control over the display format (other than what can be achieved by changing the font or the dimensions of the text viewport) nor can you determine file attributes other than read only.

If you need more control over the directory listing, you can use the FindFirstFile, FindNextFile and FindClose API functions. The following procedure displays the names of all the files in the current directory:

DEF PROClistdirectory
LOCAL dir%, sh%, res%
DIM dir% LOCAL 317
SYS "FindFirstFile", "*", dir% TO sh%
IF sh% <> -1 THEN
    PRINT $$(dir%+44)
    SYS "FindNextFile", sh%, dir% TO res%
  UNTIL res% = 0
  SYS "FindClose", sh%
By adapting this routine you can display or otherwise process the disk directory in any way that you want. If you want to filter only certain files, specify an appropriate ambiguous file specification (e.g. "*.BBC") rather than "*" in the FindFirstFile line. As well as the filenames themselves, you can discover other properties of the files from the contents of the dir% structure, as follows:
dir%!0File attributes (see below)
dir%!4Time created (LS 32 bits)
dir%!8Time created (MS 32 bits)
dir%!12Time last accessed (LS 32 bits)
dir%!16Time last accessed (MS 32 bits)
dir%!20Time last modified (LS 32 bits)
dir%!24Time last modified (MS 32 bits)
dir%!28File size in bytes (MS 32 bits)
dir%!32File size in bytes (LS 32 bits)
The common file attribute values are 1 (read only), 2 (hidden), 4 (system), 16 (directory) and 32 (archive); other bits are used for specialised purposes. The values may be combined.

The time stamps are 64-bit values representing the number of 100-nanosecond intervals since 00:00 on 1st January, 1601. You can convert these values to a more conventional form using the FileTimeToSystemTime function, as follows:

DEF PROCfiletimetosystime(dir%)
LOCAL systime%
DIM systime% LOCAL 15
SYS "FileTimeToSystemTime", dir%+20, systime%
year% = !systime% AND &FFFF
month% = systime%?2
weekday% = systime%?4
day% = systime%?6
hour% = systime%?8
minute% = systime%?10
second% = systime%?12
This example converts the time last modified. To convert the time last accessed or time created replace the dir%+20 with dir%+12 or dir%+4 respectively. The times returned will be UTC, not your local clock time.

Opening a document file

If you want your BASIC program to open (for display or editing) a document file, you can use the ShellExecute function, as follows:
document$ = "C:\Path\DocFile.doc"
SYS "ShellExecute", @hwnd%, "open", document$, 0, 0, 1
For this to work the document type must be associated with an application (for example .doc files are usually opened with Microsoft Word™). ShellExecute can also be used to open a web page, by specifying a URL rather than a document name.

Beware that ShellExecute seems to misbehave with Windows™ 95, 98 and Me, especially when opening .doc and .pdf files. Use with care on these systems.

If you want to open a directory (folder) rather than a file you can do that very easily using Windows Explorer:

*EXPLORER C:\Program Files\BBC BASIC for Windows

Discovering the Windows version

You may wish to know on which version of Windows™ (95, 98 etc.) BBC BASIC for Windows is running. For example, use of the mouse wheel is supported only in Windows 98 and later. You can do that using the GetVersionEx function, as follows:
DEF PROCgetversion
LOCAL osvi{}
DIM osvi{Size%,     \ Size of structure
\        Major%,    \ Major version number
\        Minor%,    \ Minor Version number
\        Build%,    \ Build number
\        Platform%, \ Platform ID
\        SP&(127)   \ Service Pack string
\       }
osvi.Size% = 148
SYS "GetVersionEx", osvi{}
MajorVersion% = osvi.Major%
MinorVersion% = osvi.Minor%
PlatformID% = osvi.Platform%
From the MajorVersion%, MinorVersion% and PlatformID% values you can deduce the version of Windows, as follows:

Windows 95:MajorVersion% = 4 MinorVersion% = 0PlatformID% = 1
Windows 98:MajorVersion% = 4 MinorVersion% = 10PlatformID% = 1
Windows Me:MajorVersion% = 4 MinorVersion% = 90PlatformID% = 1
Windows NT4:MajorVersion% = 4 MinorVersion% = xPlatformID% = 2
Windows 2000:MajorVersion% = 5 MinorVersion% = 0PlatformID% = 2
Windows XP:MajorVersion% = 5 MinorVersion% = 1PlatformID% = 2
Windows Vista:MajorVersion% = 6 MinorVersion% = 0PlatformID% = 2
Windows 7:MajorVersion% = 6 MinorVersion% = 1PlatformID% = 2
Windows 8:MajorVersion% = 6 MinorVersion% = 2PlatformID% = 2
Windows 8.1:MajorVersion% = 6 MinorVersion% = 3PlatformID% = 2
Windows 10:MajorVersion% = 10 MinorVersion% = 0PlatformID% = 2

(here 'x' means "don't care")

If you want to find out whether the version of Windows is 32-bits or 64-bits you can use this function, which will return FALSE for 32-bits and TRUE for 64-bits:

DEF FNis64bit
LOCAL yes%
SYS "IsWow64Process", -1, ^yes%
= yes% <> 0

Finding special folders

You may want to discover the location of one of the special Windows™ folders, for example Application Data or the Start Menu (you should not assume that they will always be in the same place). The following routine performs that function:
DEF FNspecialfolder(id%)
LOCAL ppidl%, folder%, malloc%
DIM folder% LOCAL 255
SYS "SHGetSpecialFolderLocation", @hwnd%, id%, ^ppidl%
SYS "SHGetPathFromIDList", ppidl%, folder%
SYS "SHGetMalloc", ^malloc%
SYS !(!malloc%+20), malloc%, ppidl% : REM. IMalloc::Free
= $$folder% + "\"
The special folder returned depends on the parameter id% as follows:
2:Start Menu\Programs
8:Recent files
9:Send To
11:Start Menu
26:Application Data
32:Temporary Internet Files
To find the locations of the Windows directory and the System directory use the following routines:
DEF FNwindowsdirectory
SYS "GetWindowsDirectory", T%, 260
= $$T% + "\"
DEF FNsystemdirectory
SYS "GetSystemDirectory", T%, 260
= $$T% + "\"
The locations of the Temporary directory and the user's Documents folder are available in the system variables @tmp$ and @usr$ respectively.

Finding the current font name

As explained in the documentation for the *FONT command, the font selected by Windows™ may not be the same as that requested. For example if you specify the Helvetica font, and it is not installed on your system, you may find that the Arial font has been selected instead.

You can discover the font currently in use with the GetTextFace function:

DEF FNfont
LOCAL face%
DIM face% LOCAL 31
SYS "GetTextFace", @memhdc%, 32, face%
= $$face%
The above routine can be used not only to discover the actual font selected as the result of a *FONT command, but also to find the default font used following a MODE change.

Distributing the BBCWIN font

BBC BASIC for Windows is supplied with the special BBCWIN font, for use in 40-column screen modes (MODEs 1, 4, 6, 7, 9, 17 and 19). Depending on the version of Windows™ it may be selected automatically in these modes or you may need to specify it explicitly using the *FONT command:
This is fine so long as you are running your program on a machine on which BBC BASIC for Windows is installed, but if you have compiled it to a stand-alone executable, for distribution to others, there is no guarantee that the BBCWIN font will be installed on the target machine.

If the BBCWIN font is not available Windows™ will select an alternative, and possibly unsuitable, font. In that case you might prefer to supply the BBCWIN.FON file (which can be found in your fonts folder) with your program to ensure the on-screen appearance is what you expect. You will then need to install the font on the target machine using the AddFontResource API function as follows:

SYS "AddFontResource", "BBCWIN.FON"
This assumes that the file BBCWIN.FON is in the current directory (folder). In practice you will probably want to load it from the directory which contains your executable file. This can be achieved using the @dir$ system variable as follows:
SYS "AddFontResource", @dir$+"BBCWIN.FON"
On exit from your program, or when you have finished with the font, release it as follows:
SYS "RemoveFontResource", @dir$+"BBCWIN.FON"

Checking for input focus

You may wish to know whether your program currently has the input focus, that is whether keyboard and mouse input is being directed to your program. For example, if your program directly checks the state of the mouse buttons with MOUSE or the state of the keyboard using INKEY (with a negative parameter), it may want to act upon these only when it has the focus. You can test that using the GetForegroundWindow function as follows:
SYS "GetForegroundWindow" TO hw%
IF hw% = @hwnd% THEN
  REM program has input focus

Displaying icon files

Icons are useful because, unlike standard bitmaps, they can have transparent backgrounds. When they are displayed the previous screen contents 'show through' the transparent region. To display an icon file use the LoadImage and DrawIconEx functions:
SYS "LoadImage", 0, iconfile$, IMAGE_ICON, w%, h%, LR_LOADFROMFILE TO hicon%
SYS "DrawIconEx", @memhdc%, x%, y%, hicon%, w%, h%, 0, 0, DI_MASK OR DI_IMAGE
SYS "InvalidateRect", @hwnd%, 0, 0
Here w% and h% are the width and height of the icon in pixels, and x% and y% are the offsets in pixels from the top left-hand corner of the screen to the top left-hand corner of the icon. The maximum size of an icon is 256 x 256 pixels.

For simplicity the InvalidateRect function here updates the entire screen, not just the region where the icon was displayed. If speed is important specify an update rectangle as in Displaying enhanced metafiles.

Putting an icon in the SysTray

To display an icon in the SysTray (more correctly called the taskbar notification area) you first need to obtain a handle to the icon. If the icon image is present in a file (e.g. generated by the supplied ICONEDIT.BBC program) you can use the LoadImage function to return a handle:
iconfile$ = @dir$+"resources\bbcmicro.ico"
SYS "LoadImage", 0, iconfile$, IMAGE_ICON, 16, 16, \
\                LR_LOADFROMFILE TO hicon%
Next you must create a data structure in memory as follows:
DIM nid{cbSize%, hwnd%, uID%, uFlags%, uCallbackMessage%, \
\       hIcon%, szTip&(63)}
nid.cbSize% = DIM(nid{})
nid.hwnd% = @hwnd% 
nid.uID% = 1234 
nid.uFlags% = NIF_ICON OR NIF_TIP
nid.uCallbackMessage% = WM_COMMAND
nid.hIcon% = hicon%
nid.szTip&() = "Tooltip"
Here Tooltip is the string which is displayed if you 'hover' the mouse over the icon.

Finally to display or remove the icon you use the Shell_NotifyIcon function:

SYS "Shell_NotifyIcon", NIM_ADD, nid{}
SYS "Shell_NotifyIcon", NIM_DELETE, nid{}
If you want your program to respond to clicking on the SysTray icon you should add NIF_MESSAGE to the nid.uFlags% value in the code above. This will cause messages to be sent which can be intercepted with ON SYS, in exactly the same way as those from a menu. These messages will have an @wparam% value equal to the specified nid.uID% (1234 in the example above) and an @lparam% value dependent on the mouse operation, as follows:
@lparam%mouse operation
512mouse move
513left button down
514left button up
515left button double click
516right button down
517right button up
518right button double click
519middle button down
520middle button up
521middle button double click
You can expect to receive many 'mouse move' messages (@lparam% = 512) so in most circumstances you will want to ignore these in an efficient manner to avoid being 'flooded'.

Using the system registry

The registry is a repository of data maintained by Windows™, which an application can use to hold configuration or other information from one session to the next. The registry has many other uses which are outside the scope of this documentation, and the example below illustrates only the simplest way in which it can be utilised.

The registry has a hierarchical structure, not unlike a filing system. Analogous with directories or folders are keys, which identify locations within the registry. Within each key there can be a number of named values (analogous with files) each of which can contain binary data (typically a number or a string).

So for example to save a string called String$ and an integer value called Integer% in the registry under the key name Settings do the following:

Key$ = "Software\YourName\YourProgram\Settings"
SYS "RegCreateKeyEx", &80000001, Key$, 0, "", 0, &F003F, 0, ^K%, ^D% TO R%
IF R% = 0 THEN
  SYS "RegSetValueEx", K%, "String", 0, 1, String$, LEN(String$)+1
  SYS "RegSetValueEx", K%, "Integer", 0, 4, ^Integer%, 4
  SYS "RegCloseKey", K%
The key name and value names shown above are purely illustrative, but by adopting the style of key in the example (beginning Software\YourName\) you ensure that it is unique to your application.

To read the values back from the registry do the following:

DIM TempBuffer% 255
Key$ = "Software\YourName\YourProgram\Settings"
SYS "RegOpenKeyEx", &80000001, Key$, 0, &20001, ^K% TO R%
IF R% = 0 THEN
  L% = 255 : SYS "RegQueryValueEx", K%, "String", 0, ^T%, TempBuffer%, ^L% TO R%
  IF R% = 0 TempBuffer%?(L%-1) = 13 : String$ = $TempBuffer%
  L% = 4 : SYS "RegQueryValueEx", K%, "Integer", 0, ^T%, ^V%, ^L% TO R%
  IF R% = 0 Integer% = V%
  SYS "RegCloseKey", K%
As always, ensure that you execute the DIM only once to avoid eventually running out of memory. Alternatively, consider using DIM LOCAL.

Creating a screen saver

To create a Windows™ screen saver using BBC BASIC for Windows carry out the following steps: Your screen saver should now appear in the list displayed on the Screen Saver tab of the Display Properties dialogue.

Drawing angled text

You may sometimes want to draw text at an angle other than horizontal. For example you might want to label the vertical axis of a graph or make a title screen more interesting. Using the procedure listed below you can draw text at any angle:
DEF PROCangled(font$, height%, weight%, text$, angle, X%, Y%)
LOCAL font%, oldfont%
X% = (X%+@vdu%!0)>>1
Y% = @vdu%!212-((Y%+@vdu%!4)>>1)
SYS "CreateFont", height%, 0, angle*10, angle*10, weight%, \
\   0, 0, 0, 0, 0, 0, 0, 0, font$ TO font%
SYS "SelectObject", @memhdc%, font% TO oldfont%
SYS "SetTextColor", @memhdc%, &1000000+@vdu%?70
SYS "SetBkColor", @memhdc%, &1000000+@vdu%?71
SYS "TextOut", @memhdc%, X%, Y%, text$, LEN(text$)
SYS "InvalidateRect", @hwnd%, 0, 0
SYS "SelectObject", @memhdc%, oldfont%
SYS "DeleteObject", font%
The parameters are the name of the font, the height of the font (in pixels), the weight (400 is normal, 800 is extra bold), the text string you want to draw, the angle to the horizontal (anticlockwise, degrees) and an X,Y starting position in graphics coordinates. The text is drawn in the current text foreground and background colours, as set by COLOUR.

If you want to draw the text with a transparent (VDU 5 style) background rather than an opaque (VDU 4 style) background, replace the  SYS "TextOut"  line with the following:

SYS "SetBkMode", @memhdc%, 1
SYS "TextOut", @memhdc%, X%, Y%, text$, LEN(text$)
SYS "SetBkMode", @memhdc%, 2

Downloading files from the internet

The following code sample will download a file from the internet to your local disk, assuming you have an internet connection:
url$ = "https://www.bbcbasic.co.uk/index.html"
file$ = "c:\index.html"
SYS "LoadLibrary", "URLMON.DLL" TO urlmon%
SYS "GetProcAddress", urlmon%, "URLDownloadToFileA" TO UDTF%
SYS UDTF%, 0, url$, file$, 0, 0 TO fail%
IF fail% ERROR 100, "File download failed"
If you need to flush the cache to ensure you retrieve the latest version (e.g. it's a webcam picture) then it becomes slightly more complicated:
url$ = "https://www.bbcbasic.co.uk/index.html"
file$ = "c:\index.html"
SYS "LoadLibrary", "URLMON.DLL" TO urlmon%
SYS "GetProcAddress", urlmon%, "URLDownloadToFileA" TO UDTF%
SYS "LoadLibrary", "WININET.DLL" TO wininet%
SYS "GetProcAddress", wininet%, "DeleteUrlCacheEntryA" TO DUCE%
SYS DUCE%, url$
SYS UDTF%, 0, url$, file$, 0, 0 TO fail%
IF fail% ERROR 100, "File download failed"
The URL can specify either an HTTP (Hyper Text Transfer Protocol) document or an anonymous FTP (File Transfer Protocol) file.



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