=====Writing graphics to the printer===== //by Richard Russell, May 2006//\\ \\ The only built-in means for writing graphics to the printer provided by //BBC BASIC for Windows// is the ***HARDCOPY** command: this transfers an area of the screen (which may include graphics) to the printer. However this is not a very satisfactory method because the graphics are limited to the resolution of the screen, which is far poorer than the resolution of a typical (laser or inkjet) printer; graphics can therefore appear much coarser than they need to. Whilst this can be partially overcome by creating a sufficiently [[http://www.bbcbasic.co.uk/bbcwin/manual/bbcwini.html#hint2|large output 'canvas']] (which can be bigger than your screen) it still imposes some limitations.\\ \\ The main Help documentation touches on how one can use the **Windows API** to write graphics to the printer ([[http://www.bbcbasic.co.uk/bbcwin/manual/bbcwine.html#grafprint|Drawing graphics to a printer]]) but it barely scratches the surface of what is possible. This article expands on what is written there.\\ \\ ===== Coordinate system ===== \\ Fundamental to outputting any graphics is the coordinate system by which the positions of lines, objects etc. are described. In the case of the printer the origin (0,0) is the top-left-hand corner of the printable area, with the horizontal (X) coordinate increasing to the right and the vertical (Y) coordinate increasing downwards.\\ \\ The resolution of the printer (usually measured in **dots per inch**) varies between models, and may even be different in the horizontal and vertical directions. Therefore - assuming you want your program to work with a range of different printers - you should not use //absolute// coordinates when outputting graphics. Instead you can either determine what the resolution is and adjust your values accordingly, or you can scale your graphics to fit between the page margins.\\ \\ For the first method you call the Windows API as follows: SYS "GetDeviceCaps", @prthdc%, 88 TO dpix% SYS "GetDeviceCaps", @prthdc%, 90 TO dpiy% This returns the number of dots-per-inch in the horizontal direction in **dpix%** and the number of dots-per-inch in the vertical direction in **dpiy%**. You will often find that these values are the same. For the second method you read the margin values using //BBC BASIC for Windows//'s system variables: marginl% = @vdu%!232 marginr% = @vdu%!236 margint% = @vdu%!240 marginb% = @vdu%!244 The first method is more appropriate if you need your graphics to come out a certain size, whatever the size of paper or margin settings, and the second method is more appropriate if you want your graphics to fit the page.\\ \\ ===== Pens and brushes ===== \\ Before you can draw anything you need to create a pen and/or a brush. A pen is used for drawing **lines** and a brush for filling **areas**. Quite often you will need both, even for a single object. For example when drawing a polygon the pen will be used to draw the outline and the brush to fill the inside.\\ \\ You can create all the pens and brushes you need at the start, and delete them at the end, or you can create and delete them one at a time as they are needed. Apart from a small impact on memory usage the choice is yours.\\ \\ ==== Pens ==== \\ There are three main ways to create a pen, depending on what kind you need. The first is to use "SYS "GetStockObject"": SYS "GetStockObject", 6 TO pen% : REM. White pen SYS "GetStockObject", 7 TO pen% : REM. Black pen SYS "GetStockObject", 8 TO pen% : REM. Null pen These are the most basic kinds of pen. The only one likely to be useful when outputting to the printer is the **Null pen** which you would use when you want to draw an object //without any outline//.\\ \\ The second method is to use "SYS "CreatePen"". This gives you more control over the pen as follows: SYS "CreatePen", penstyle%, penwidth%, pencolour% TO pen% Ths possible values of **penstyle%** are:\\ * **0**: PS_SOLID * **1**: PS_DASH * **2**: PS_DOT * **3**: PS_DASHDOT * **4**: PS_DASHDOTDOT * **5**: PS_NULL The **penwidth%** is specified in pixels (dots) so you may need to change it according to the dots-per-inch value for the printer. The width can be changed only for the PS_SOLID style: it must be 1 otherwise.\\ \\ The **pencolour%** is specified as a value equivalent to the hexadecimal number "&00BBGGRR" where "BB", "GG" and "RR" are the amounts of blue, green and red respectively ("00"=none, "FF"=maximum). So, for example, "&00008000" would be a dark green.\\ \\ The third method of creating a pen is to use "SYS "ExtCreatePen""; this gives you even more control over the pen: DIM lb{style%,color%,hatch%} lb.style% = lb.color% = lb.hatch% = SYS "ExtCreatePen", penstyle%, penwidth%, lb{}, 0, 0 TO pen% The **penstyle%** value can be any of those specified for "CreatePen" above, but it can be combined using the **OR** operator with one or more of the following //geometric// styles:\\ * **&10000**: PS_GEOMETRIC * **&10100**: PS_GEOMETRIC + PS_ENDCAP_SQUARE * **&10200**: PS_GEOMETRIC + PS_ENDCAP_FLAT * **&11000**: PS_GEOMETRIC + PS_JOIN_BEVEL * **&12000**: PS_GEOMETRIC + PS_JOIN_MITER The **penwidth%** is specified as for "CreatePen", but for widths greater than 1 you must specify one of the //geometric// pen styles above.\\ \\ The structure **lb{}** specifies the colour and other attributes of the pen. If none of the //geometric// styles is specified **lb.color%** determines the colour (as for "CreatePen") and **lb.style%** must be zero.\\ \\ If one or more of the //geometric// styles is specified then **lb.style%** can be one of the following values:\\ * **0**: BS_SOLID (lb.color% specifies the colour, lb.hatch% is ignored) * **2**: BS_HATCHED (lb.color% specifies the colour, lb.hatch% is one of the values listed below) * **3**: BS_PATTERN (lb.color% is ignored, lb.hatch% is a handle to a bitmap) * **5**: BS_DIBPATTERN (lb.color% is zero, lb.hatch% is a __handle__ to a packed DIB) * **6**: BS_DIBPATTERNPT (lb.color% is zero, lb.hatch% is a __pointer__ to a packed DIB) If **lb.style%** is 0 (BS_SOLID) or 2 (BS_HATCHED) then **lb.color%** specifies the colour in the same format as for "CreatePen" above. \\ \\ If **lb.style%** is 2 (BS_HATCHED) then **lb.hatch%** can be one of the following values:\\ * **0**: HS_HORIZONTAL * **1**: HS_VERTICAL * **2**: HS_FDIAGONAL * **3**: HS_BDIAGONAL * **4**: HS_CROSS * **5**: HS_DIAGCROSS ==== Brushes ==== \\ There are three main ways to create a brush, depending on what kind you need. The first is to use "SYS "GetStockObject"": SYS "GetStockObject", 0 TO brush% : REM. White brush SYS "GetStockObject", 1 TO brush% : REM. Light grey brush SYS "GetStockObject", 2 TO brush% : REM. Grey brush SYS "GetStockObject", 3 TO brush% : REM. Dark grey brush SYS "GetStockObject", 4 TO brush% : REM. Black brush SYS "GetStockObject", 5 TO brush% : REM. Null brush You would use a **Null brush** when you want to draw //just the outline// of an object.\\ \\ The second method is to use "SYS "CreateSolidBrush"": SYS "CreateSolidBrush", brushcolour% TO brush% The **brushcolour%** is specified as a value equivalent to the hexadecimal number "&00BBGGRR" where "BB", "GG" and "RR" are the amounts of blue, green and red respectively ("00"=none, "FF"=maximum). So, for example, "&000080FF" would be orange.\\ \\ The third method is to use "SYS "CreateBrushIndirect"": DIM lb{style%,color%,hatch%} lb.style% = lb.color% = lb.hatch% = SYS "CreateBrushIndirect", lb{} TO brush% The structure **lb{}** specifies the colour and other attributes of the brush. The member **lb.style%** can be one of the following values:\\ * **0**: BS_SOLID (lb.color% specifies the colour, lb.hatch% is ignored) * **2**: BS_HATCHED (lb.color% specifies the colour, lb.hatch% is one of the values listed below) * **3**: BS_PATTERN (lb.color% is ignored, lb.hatch% is a handle to a bitmap) * **5**: BS_DIBPATTERN (lb.color% is zero, lb.hatch% is a __handle__ to a packed DIB) * **6**: BS_DIBPATTERNPT (lb.color% is zero, lb.hatch% is a __pointer__ to a packed DIB) If **lb.style%** is 0 (BS_SOLID) or 2 (BS_HATCHED) then **lb.color%** specifies the colour in the same format as for "CreateSolidBrush" above. \\ \\ If **lb.style%** is 2 (BS_HATCHED) then **lb.hatch%** can be one of the following values:\\ * **0**: HS_HORIZONTAL * **1**: HS_VERTICAL * **2**: HS_FDIAGONAL * **3**: HS_BDIAGONAL * **4**: HS_CROSS * **5**: HS_DIAGCROSS There are other methods of creating brushes but they mainly duplicate the options available from "CreateBrushIndirect". ===== Writing graphics ===== \\ Before you can output any graphics you must have enabled the printer using **VDU 2** and have printed at least one conventional text character. You may in any case want to print a title or something similar, but if not you can send just a single space character to the printer as follows: VDU 2,1,32,3 ==== Lines and curves ==== \\ The simplest thing you can draw is a straight line; to do that use code similar to the following: SYS "SelectObject", @prthdc%, pen% SYS "MoveToEx", @prthdc%, x1%, y1%, 0 SYS "LineTo", @prthdc%, x2%, y2% Here **x1%,y1%** are the coordinates of the start of the line and **x2%,y2%** are the coordinates of the end of the line. If you need to draw more lines with the same pen you don't need to reselect it.\\ \\ If you want to draw a number of connected lines, for example as a graph, then you can simply add additional calls to "LineTo": SYS "SelectObject", @prthdc%, pen% SYS "MoveToEx", @prthdc%, x1%, y1%, 0 SYS "LineTo", @prthdc%, x2%, y2% SYS "LineTo", @prthdc%, x3%, y3% REM. etc........ However if there are a large number of connected lines it may be easier to use the "Polyline" function: SYS "SelectObject", @prthdc%, pen% SYS "Polyline", @prthdc%, ^points%(0,0), npoints% Here **points%()** is a 2D array of coordinates between which the lines will be drawn, where the second subscript is **zero** for the X-coordinate and **one** for the Y-coordinate. So for example you might declare and initialise the array as follows: DIM points%(npoints%-1,1) points%() = x1%,y1%,x2%,y2%,x3%,y3%,x4%,y4%,x5%,y5%...... In practice it is rather more likely that the coordinates will be initialised in a **FOR...NEXT** loop: DIM points%(npoints%-1,1) FOR I% = 0 TO npoints%-1 points%(I%,0) = ... : REM x coordinate points%(I%,1) = ... : REM y coordinate NEXT As well as straight lines you can draw smooth curves: SYS "SelectObject", @prthdc%, pen% SYS "PolyBezier", @prthdc%, ^points%(0,0), npoints% The **points%()** array is declared exactly as before, but instead of drawing straight line segments Windows draws Bezier curves using the set of control points. In this case the number of points must be at least four.\\ \\ Another kind of curve is an (axis-aligned) elliptical arc: SYS "SelectObject", @prthdc%, pen% SYS "Arc", @prthdc%, xmin%,ymin%, xmax%,ymax%, xstart%,ystart%, xend%,yend% You specify the position and size of the ellipse in terms of its //bounding rectangle// and you specify the start and end of the arc as the end-points of two radial lines which intersect it. The arc is drawn anticlockwise:\\ * {{arclabel.gif}} \\ You can draw a complete ellipse or circle by specifying the same point for the start and the end (often the point 0,0 is suitable). ==== 2D objects ==== \\ Lines and curves are drawn with a pen. 2D objects are drawn with both a pen (for the outline) and a brush (to fill the interior). As mentioned earlier if you don't want to draw the outline you can select a //Null pen//; similarly if you don't want to fill the interior you can select a //Null brush//. A simple example of a 2D object is a rectangle: SYS "SelectObject", @prthdc%, pen% SYS "SelectObject", @prthdc%, brush% SYS "Rectangle", @prthdc%, xmin%, ymin%, xmax%, ymax% A rectangle is a special case of a polygon. Other polygons can be drawn as follows: SYS "SelectObject", @prthdc%, pen% SYS "SelectObject", @prthdc%, brush% SYS "Polygon", @prthdc%, ^vertices%(0,0), nvertices% Here **vertices%()** is a 2D array of coordinates of the polygon's vertices, where the second subscript is **zero** for the X-coordinate and **one** for the Y-coordinate. So for example you might declare and initialise the array as follows: DIM vertices%(nvertices%-1,1) vertices%() = x1%,y1%,x2%,y2%,x3%,y3%,x4%,y4%,x5%,y5%...... In practice it is rather more likely that the coordinates will be initialised in a **FOR...NEXT** loop: DIM vertices%(nvertices%-1,1) FOR I% = 0 TO nvertices%-1 vertices%(I%,0) = ... : REM x coordinate vertices%(I%,1) = ... : REM y coordinate NEXT This is fine for normal polygons where none of the sides intersect one other, but there is a slight complication if you try to draw, for example, a five-pointed star. In this case you should specify the required //fill mode// as follows: SYS "SetPolyFillMode", @prthdc%, fmode% where **fmode%** is **1** for //alternate// and **2** for //winding//. The effect of the different modes can be seen below:\\ * Alternate: {{alternate.gif}} Winding: {{winding.gif}} Other 2D objects you can draw are sectors and segments. For a **sector** you would use: SYS "SelectObject", @prthdc%, pen% SYS "SelectObject", @prthdc%, brush% SYS "Pie", @prthdc%, xmin%,ymin%, xmax%,ymax%, xstart%,ystart%, xend%,yend% Here the parameters are exactly the same as for the **arc** described earlier. For a **segment** you do the following: SYS "SelectObject", @prthdc%, pen% SYS "SelectObject", @prthdc%, brush% SYS "Chord", @prthdc%, xmin%,ymin%, xmax%,ymax%, xstart%,ystart%, xend%,yend% Again the parameters are the same as for the **arc** and the **sector**.\\ \\ For a complete (filled) ellipse or circle draw a **sector** but specify the same point for the start and the end (often the point 0,0 is suitable).\\ ==== Arbitrary shapes ==== \\ You can draw an arbitrary filled shape by first defining its outline and then instructing Windows to fill it: SYS "BeginPath", @prthdc% REM. Define the outline here, for example using combinations REM. of SYS "PolyLine", SYS "PolyBezier" and SYS "Arc" SYS "EndPath", @prthdc% SYS "SelectObject", @prthdc%, pen% SYS "SelectObject", @prthdc%, brush% SYS "StrokeAndFillPath", @prthdc% ===== Completing the printout ===== \\ After you've output all the graphics, along with any text and/or images on the same page, you are ready to commit it to paper.\\ \\ ==== Tidying up ==== \\ You must delete all the pens and brushes you created. Firstly ensure that none of them is still selected: SYS "GetStockObject", 5 TO nullbrush% SYS "SelectObject", @prthdc%, nullbrush% SYS "GetStockObject", 8 TO nullpen% SYS "SelectObject", @prthdc%, nullpen% If you created your pens and brushes as you needed them, and deleted them as soon as they were finished with, there will probably be no more than one of each left to delete. If however you created them all at the start there may be several to delete, for example: SYS "DeleteObject", pen1% SYS "DeleteObject", pen2% SYS "DeleteObject", brush1% SYS "DeleteObject", brush2% ==== Ejecting the page ==== \\ Now you're ready to tell the printer to go ahead and print everything you've output. You do that as follows: VDU 2,1,12,3 That's all there is to it!\\ \\ ---- ===== Writing to the screen ===== \\ Although this article has primarily been about writing graphics to the printer, all the Windows API commands are equally applicable to writing to the screen. Although there are often easier ways of doing it, using BBC BASIC's built-in graphics statements, it may sometimes be appropriate to use the API. For example if you want to provide a **Print Preview** facility, using the same code for both printer and screen will ensure the most accurate representation.\\ \\ To adapt the foregoing code for the screen rather than the printer do the following:\\ * Omit the initial "VDU 2,1,32,3" * Replace all occurrences of "@prthdc%" with "@memhdc%" * Use screen (pixel) coordinates rather than printer coordinates * Replace the final "VDU 2,1,12,3" with the following code: SYS "InvalidateRect", @hwnd%, 0, 0