User Tools

Site Tools


writing_20graphics_20to_20the_20printer

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 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 (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:


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: Winding:

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
This website uses cookies for visitor traffic analysis. By using the website, you agree with storing the cookies on your computer.More information
writing_20graphics_20to_20the_20printer.txt · Last modified: 2018/04/13 20:06 by richardrussell