Introduction to Reporting and Printing

Navigation: »No topics above this level«

Introduction to Reporting and Printing

 Return to chapter overview

Printing, love it or hate it, we all need to do it !

Without printing we can' have reports. Even reports shown on screen are printed before being displayed.

So if we get our heads around printing, and make things easy for ourselves, then reporting will be easier too.

Many people have had a 'bad' experience with printing in the past, and try to stay clear of it if they can. Often such people only wish to know about, and use, third party printing solutions.

This is a shame, as taking control of your own printing can be very rewarding. You also have much more flexibility than when using third party solutions.

If you get yourself a functional, and straight forward 'Printing Class', then you can add and change code, and methods, to suit your needs.

The 'Printing Class' shown in these notes has been constantly evolving for a few years, and no doubt it will keep on changing as I find new challenges.

The basis of the ideas for this class are working in a Visual Objects format, in a large network application at this very moment, and have been for a couple of years or more.

In order to make things work in VO as I required, I had to resort to using a couple of 'add-ins'.

However, the world of .NET has made things a lot easier, so hold on to your hats as we have some real fun.

No more 'add-ins', all dotNet, and the code gets clearer all the time :-)

Dr. Philip H. Hepburn - BEST consultancy

September 2008.


Overview

Navigation: Introduction to Reporting and Printing >

Overview

 Return to chapter overview

The good news is that printing in Windows is easier now that ever it was previously.

What made printing a difficult subject in the recent past was a lack of a way to see what you were doing. In other words, the lack of a 'preview' tool.

A couple of years ago I discovered PDF995 which is a 'virtual' printer driver. This could be set up to automatically create and display a PDF file, and it used Adobe Reader for display purposes.

This is what really made my VO printing class possible, or at least made it much more flexible in the sorts of problems we could solve.

There are now a number of similar virtual printer drivers, all available cheaply over the net.

Unfortunately, in order to incorporate virtual printer driver capability into a printer class you need to purchase and install one at each PC network terminal. So the developer loses control of 'things', also their use is not exactly 'seemless'.

Today, we have a 'PrintPreview' dialog available from dotNet and this has really opened things up as far as a standard user 'Printing Class' is concerned.

The second, and last, problem I had to overcome in VO to make the printing class, was the ability to print images from bitmaps, directly from disk file. We do it for signatures on the printed letters we produce, but we had to source a third party solution - and thanks to Michael Rubinstein and his "IPicture" library we have printed hundreds of thousands by now.

Thank you Michael :O)

This really got me into my own printing - but the image above was done as all dotNet stuff.

At least the developer still has control of such a library - well, till there are major software changes, but that is another matter.

It is nice to be able to print signatures on letters, and manage the signature files, and associations, without need to change code to make resources. Now the 'IT Manager' has control over the signature management.

Later in the notes you will see such a 'tool' to view and manage signature files, as this is a VERY necessary part of signature printing on letters.

Thankfully, dotNet has solved this problem too - we have signature, and any image file 'direct' printing from file. Obviously, if you can print directly to canvas, then you can print an image from file to a Windows form, or control.

Before we move on, I need to say that 'overloading' in dotNet has made it much easier to design and construct a 'Printing Class'.

I have completely remade the methods from the VO format, and used overloading whenever it made good sense. This has resulted in a much clearer class to use, particularly for any user who is coming to it through ClickStart.

Now, instead of having to include text in the method name, which partly explains the input parameters, we can have one clear name, and leave the dirty business of the various parameter types to the compiler.

You will appreciate more on the overload situation when we come to it.

PHH - Sept. 2008


The way ahead

Navigation: Introduction to Reporting and Printing >

The way ahead

 Return to chapter overview

If you follow through these sections in order, the you will be able to use the 'Printing Class' included, and be able to code your own letters and documents.

The Report Generator will also make good sense to you, and you will be able to apply that to data table information, and produced quality reports.

You will be able to customize the methods to solve your own printing needs, if the existing code does not do so already.

As well as being able to make your own coded printing solutions, you will be able to use the Microsoft 'high level' tools to cover 'Reporting Services'.

There is already a Report Viewer control to drop onto your forms to allow display of reports created with the Microsoft GUI tools.

As well as learning about printing, you will learn about printers - a necessity if you are to get the most from the 'Printing Class'.

Laser label printing, grids, and mail merging, all simply follow on from being able to apply the basic printing methods, and following the same general approach.

PHH - Sept. 2008.


Making things easier

Navigation: Introduction to Reporting and Printing >

Making things easier

 Return to chapter overview

One thing that has been made clear to me, after years of doing my own printing, is that you must not try to do everything at once.

By this I mean that when you come to place graphical elements on a page (some call this printing), make sure you have done all the data preparation, and so you only have to deal with the 'printing'.

This may even go as far as the page layout preparation. It is best to know what is to go on which page, before you try to print pages. If it is possible paginate before printing. This also help you manage the task of selecting one, an range, or indeed all of the pages in the 'set'.

In looking at other solutions to printing I have seen the processes overlapped at times, and to my mind this has caused unnecessary difficulties.

If you can separate out the coordinate system used to point to places on a page where elements are to be placed, from the methods to place certain elements, as also keep the structuring of the data to be used, from such printing class methods, the all gets a lot easier.

Because the printing process is a complex business in many ways, there are times that the edges will get a little blurred on these issues, BUT, if you are aware of spreading out and isolating the problems, then thing will generally become a lot easier.

I would even go further an suggest that there may be a range of ways we can prepare the data for different types of document style.

Multiple Pages :

When we come to the point of printing out multiple pages, as in a report, we will see that it is very desirable to have the information already ordered and designated for a given page.

We will need to write code to handle the 'page layout' of the graphical elements as we move from one page to the next. As well as let the system know if there are further pages to print after the one being processed.

As with the range of ways for preparing data for the required document style, there are a range of ways the page layout code needs to be adapted to the document style too.

I would even go so far as to make up my own rules on this, and say that the more complex the final document type then the more effort needs to be put into organizing the data, and handling the page layout.

Quite often people are in too much of a hurry to get ink on paper to spend time thinking about the organization and preparation. However, time spent on these will definitely pay off in the end.

PHH - Sept. 2008.


Aknowledgements

Navigation: Introduction to Reporting and Printing >

Aknowledgements

 Return to chapter overview

It has to be made clear from the offset that I owe a great deal to Alwyn Jennings-Bramly.

A huge amount of what is in these notes is firmly built on foundations laid down by him.

I dedicate the whole of these notes on Reporting and Printing to Alwyn - thanks for the motivation.

PHH - Sept. 2008


Section 1 - the Need for a Printing Class

Navigation: »No topics above this level«

Section 1 - the Need for a Printing Class

 Return to chapter overview

This can be VERY simply put - we need a 'Printing Class' to make printing easy !

If we design our class sensibly and 'clearly', it will be very easy to use.

Because the overall process of printing in Windows is such a complex one, we need to have split the required tasks down into 'parts' and solved each of these 'parts' before we wish to start thinking of printing anything.

Notice I said 'complex' and not complicated. The printing process is not difficult to understand, but there are a host of details to be got right before you see any results, never mind them being correct results !

The need for a 'Printing Class' also comes from analyzing the process of drawing and writing on a sheet of paper.

When we 'mark' a sheet of paper we can place just about anything we like, wherever we like, as long as it is on the paper. We can use colour, text, lines, and pictures - and place then in which order we choose too.

So it should be for our printing, if we have the all the necessary 'small' functions, then we can build up a finished image on the page which looks quite stunning.

For example, a complex 'grid' is just made up from straight lines. If we can print one straight line then we can write a method to draw a 'set' of lines. Some grids have a vertical 'set' and a horizontal 'set'. So the method to draw the grid, takes information about the two sets, and the uses itself the class methods to draw the vertical and horizontal lines.

In many ways this 'Printing Class' shows OOP at its best, as many methods use methods already defined in the class, and to a reasonable depth too.

One final, and probably impossible problem, is overcome from a class built in this way - and that is that the 'canvas' positions for a single piece of paper vary depending on the target printer to be used. This will be explained in detail later, but let us say that at the stage of us designing a paper or page layout of a document, we don't wish to be worrying about large numbers and which number we can and can't use.

At a page layout stage we need to be thinking in terms of ... half way down and over to the right we need a blue circle. If we conveniently use up and across to have limits from 0 to 100 then we can use our methods in just this manner.

However, underneath the code will need to know just what the large numbers are for up and across on an "Epson XYZ" printer. We need all our methods which the developer uses to populate the page, to be printer independent. This is done very nicely indeed by by a 'Printing Class' like the one explained in the following sections.

PHH - Sept. 2008.


what is a Printing class ?

Navigation: Section 1 - the Need for a Printing Class >

what is a Printing class ?

 Return to chapter overview

Put simply, a 'Printing Class' (PC) is a class of object which provides a rich set of methods to allow easy 'placement' of graphical elements on any page to be printed.

By graphical elements I mean - text, line, shape, image and path. Things of that nature, which are defined in the GDI+ literature.

The PC allows the developer to think in terms of a 'page of paper', and position any graphical element somewhere on that page, to suit his / her artistic fancy.

The developer / designer thinks in terms of a page of paper, and the PC code places elements on a 'canvas'. The 'canvas' area is actually the area defined by the selected printer's software 'driver.

These canvas coordinates are fundamental to printing, and are what probably puts most developers off doing their own printing. Quite honestly, I am not surprised, except that it does not take much work to 'hide' the canvas coordinates altogether. From then on the user works with page an paper positions, in numbers we all can happily handle, like zero to one hundred and in decimals too.

Once the canvas coordinates are 'buried' from view, to be used by the PC itself, and not the user - then the user can get on with designing, and other artistic issues, rather than fighting numbers.

I never deal in these canvas coordinate numbers - thank God !

The computer surface onto which the elements get placed in these canvas coordinates is called a 'graphics surface'.

So, before anything can be drawn by the PC software tools, both a printer, and a graphics surface has to be selected. We will see more of this later, in sufficient detail as to be able to copy, and get it all to work nicely.

Once we have selected a printer, the printers software driver will supply the canvas coordinates when queried at run time, and supply these back to the PC itself.

What the 'Printing Class' needs to support it is some suitable coordinate classes, to help the designer place the graphics elements. Before you can place an element, you need to specify where it is to be placed.

See more on choosing coordinate systems in a subsequent sub-section of this chapter.

For now lets go on to see how we use a 'Printing Class'.

PHH - Sept. 2008.


how is a Printing class used ?

Navigation: Section 1 - the Need for a Printing Class >

how is a Printing class used ?

 Return to chapter overview

It is my considered opinion, that if you design your 'Printing Class' (PC) correctly, then it relieves the developer / designer of the 'nuts and bolts' procedures of getting graphical elements onto a page.

If you like, it allows creativity to take place easily, and at a pace which feels 'productive'.

PHH - Sept. 2008.


setting page boundaries

Navigation: Section 1 - the Need for a Printing Class >

setting page boundaries

 Return to chapter overview

Oh! dear, I had to do it somewhere, and here is the place !

This is the stuff which puts a lot of people off doing printing themselves. It is necessary to see what is done 'on your behalf', but the good news is we can then forget about it - forever !

If we are to work in nicer units for placing graphics elements on a paper page, then at some point we need code to translate the 'canvas' coordinate values for the selected printer, which are supplied by the printer driver software.

The origin of the 'canvas' system (called 0,0 ) is at the top left of the page.

The printer driver supplies values for the extreme edges of the right and bottom of the page. If you get turned on by these sorts of things then look out more information about the PrinterSettings class in the SDK.

Having instantiated a PrinterSettings object by supplying the printer name, and also specifying the orientation, the 'Printable Area' is returned by the printer driver.

With details of the area available for printing (just means a rectangle X by Y), and the hard margins, we can do some tedious arithmetic to end up with values that our 'underlying' PC methods can use to provide us with a much more convenient coordinate system.

The main reasons for doing things this way is to remove the 'numeric' dependency of our 'document description' to the printer specific canvas values. We need to get a document description which will be the same for all printers used. This is just the first part of massaging the numbers into a form which can provide us with three 'higher level' coordinate systems - which as you will see are MUCH easier to use for page design.

Having put off the evil moment to the latest possible time, here is the "necessary" SetPageBoundaries method :-

METHOD SetPageBoundaries() AS VOID

LOCAL oPS AS PrinterSettings

LOCAL oMyArea AS RectangleF

LOCAL nHardX, nHardY AS Double

LOCAL nMLeft, nMRight, nMTop, nMBottom AS Double

LOCAL nPCMLeft, nPCMRight, nPCMTop, nPCMBottom AS Double

LOCAL nPAreaLeft, nPAreaRight, nPAreaTop, nPAreaBottom AS Double

oPS := PrinterSettings{}

oPS:PrinterName := SELF:PrinterName

//------------ need to select the printer first ---------------

IF SELF:Landscape

oPS:DefaultPageSettings:Landscape := TRUE

ELSE

oPS:DefaultPageSettings:Landscape := FALSE

ENDIF

oPageSettings := oPS:DefaultPageSettings

oMyArea := oPageSettings:PrintableArea

nHardX := oPageSettings:HardMarginX

nHardY := oPageSettings:HardMarginY

SELF:HardMarginX := nHardX

SELF:HardMarginY := nHardY

nPAreaLeft := oMyArea:LEFT

nPAreaRight := oMyArea:RIGHT

nPAreaTop := oMyArea:Top

nPAreaBottom := oMyArea:Bottom

SELF:CanvasLength := nPAreaBottom - nPAreaTop

SELF:CanvasWidth := nPAreaRight - nPAreaLeft

SELF:PCCL := SELF:CanvasLength*0.01

SELF:PCCW := SELF:CanvasWidth*0.01

nPCMLeft := 2*SELF:PCCW

nPCMRight := 2*SELF:PCCW

nPCMTop := 2*SELF:PCCL

nPCMBottom := 2*SELF:PCCL

nMLeft := nPAreaLeft

nMRight := nPAreaRight

nMTop := nPAreaTop

nMBottom := nPAreaBottom

SELF:LM := nMLeft

SELF:RM := nMRight

SELF:TM := nMTop

SELF:BM := nMBottom

SELF:PCLM := nPCMLeft

SELF:PCRM := nPCMRight

SELF:PCTM := nPCMTop

SELF:PCBM := nPCMBottom

SELF:BLP := Point{(INT)Math.Floor(SELF:LM), (INT)Math.Floor(SELF:BM)}

SELF:BRP := Point{(INT)Math.Floor(SELF:CanvasWidth-SELF:LM), (INT)Math.Floor(SELF:BM)}

SELF:TLP := Point{(INT)Math.Floor(SELF:LM), (INT)Math.Floor(SELF:CanvasLength-SELF:BM)}

SELF:TRP := Point{(INT)Math.Floor(SELF:CanvasWidth-SELF:LM), (INT)Math.Floor(SELF:CanvasLength-SELF:BM)}

SELF:PageLength := SELF:CanvasLength

SELF:PageWidth := SELF:CanvasWidth

SELF:LineHeightP := SELF:PageLength/(SELF:LinesPerPage+2)//---- adjusted 'al dente' to fit the screen view ----

SELF:LineHeightL := SELF:PageWidth/(SELF:LinesPerPage+2)//---- adjusted 'al dente' to fit the screen view ----

IF oPS:DefaultPageSettings:Landscape

SELF:LineHeight := SELF:LineHeightL

SELF:PageLength := SELF:CanvasWidth

SELF:PageWidth := SELF:CanvasLength

ELSE

SELF:LineHeight := SELF:LineHeightP

ENDIF

SELF:PCPL := SELF:PageLength*0.01

SELF:PCPW := SELF:PageWidth*0.01

RETURN

Please do not think that because you do not really understand this method, that you can' do your own printing. There will be lots of dotNet classes and methods you use and that considerations does not bother you - so why here ?

I have just included this for those who may be interested, like AJB who taught me everything about these sorts of things :-)

I suggest we cheer ourselves up and move straight on to seeing how we can greatly improve matters by choosing, and using, more convenient coordinates systems for addressing the page.

Remember, if you look at Vulcan code to print a letter or report, you won't see anything as 'nasty' as this.

PHH - Sept. 2008.


choosing Coordinate systems

Navigation: Section 1 - the Need for a Printing Class >

choosing Coordinate systems

 Return to chapter overview

Here again we have good news, and it is dotNet we need to thank.

Because we can use overloading techniques in our Vulcan coding, we can use many coordinate systems all at the same time without causing ourselves problems.

In fact we use three systems here, and it actually makes life much easier than in the VO world of API, when we did not have overloading available to us. we will see just how this is below.

For now, realize that we use three coordinate systems throughout the PC, and I call these 'Paper', 'Page' and 'Graph'. Underneath all three is the 'canvas' coordinate system required to code to the graphics surface, which at run time is instantiated as a graphics object.

All three - 'Paper', 'Page' and 'Graph' - are in printer independent numbering, and this is explained below.

Paper coordinates start at the top left of the page of paper in focus (we call this the origin, or 0,0), and are in 0 to 100 decimal percentages. So we go down the paper, and across it too.

Graph coordinates are based on mathematical work we do with graphs and graph paper, so we have an origin (0,0) at the bottom left of the page of paper. Once again we use 0 to 100 decimal percentage values - for up and across the page.

Lastly, the 'Page' system is slightly different and is simply based on how we seem and read a page in a book. We start at the top left of the page (origin and 0,0) and work down the page and also across it. This time however, we treat the down position not as a percentage value, but as a number of lines down the page. Across is still a percentage.

This now means that for us to know where we are in 'down' the page terms we need to know how many lines there are to a page. The two things go hand in hand - to have a page coordinate system, we need to define the page length. This we do as an integer.

Do realize however, that we can change the lines per page value at any time during the placing of elements on the page, and still continue without error or problems occurring. For example if we have 60 lines per page, and place an element at 30 lines down and 50% across, then we are placing it at the middle of the page. Now we could change the lines per page to 40 lines per page and place a second element at 30 lines down and 50% across, and we have effectively placed the element three quarters of the way down the page.

Only the 'Page' coordinate system can change during the 'drawing' process. However, once any of the three systems is used, the 'Printing Class' code converts to canvas numbering to suit the printer and printer driver software.

This will all get clearer when we start to use the PC to design documents. It all becomes very easy, and we can use all three systems simultaneously if we so wish. In fact, as you will see, it is quite handy to be able to convert from one coordinate system into another, whenever we wish.

Since most people are familiar with graphs from school days, lets see the 'GraphCoords' class first :-

===========================================================

As you will agree this is quite simple, and any point X along and Y up is made in Vulcan code as shown below :-

================================

So what is the fourth line of code ? Well, it is nothing more than a call to a PC method to draw a line from point A to point B - with pen width 1.

The line will be 4% up the page and go almost completely across it.

What about the 'Paper' coordinate class ? Lets see how that looks :-

=============================================

Notice as we use this system as down first and then across, the order of the parameters is different.

Lets see how this class may be used in a real coding situation :-

===============================

This code snippets shows two points being specified in Paper coordinates and used to draw a line 5% down the paper.

The line is being used to separate the header from the data in a page of a report.

Finally, the 'Page' coordinate class :-

==========================================================

Notice that this is more complex in nature.

The constructor is overloaded to accept a simple line down and percentage across numeric pair of decimal numbers, as well as a number of lines on the page, and a pair of numbers.

Notice the Y and X coordinates are similar in position to the 'Paper' as it is down and across we are defining.

We will see how we can use the lines per page later in the notes, but for now realize that one constructor is for using the already set value of lines per page, whereas the other is expecting to alter it.

So lets see how this may be used in real coding, where we are creating a document :-

===============================

This is part of the report generator class where a line is being drawn at a value of nLine down and from 2% across the page to 98% across the page. This would do nicely at the top and bottom of a report, to separate the header and footer. And this is exactly what is being done in the code section from which this is copied.

Obviously, the points are in 'Page' coordinates, and nLine would be a value of typically, 6 and 58 for our header / footer lines.

Before we go to another section, remember we have not seen any 'difficult' and large numbers while doing this, so the 'canvas' has been hidden, to all intents and purposes.

Finally, we need to mention that each PC method which accepts an overload with either 'Page', 'Paper', or 'Graph', needs to be written in order to handle the input parameter correctly.

Let us take one example to see what I mean :-

===================================================================

Notice the 'Page' coordinate input, is converted to a point, and the point is in 'canvas' coordinates so that it is correct for the graphics surface for the selected printer.

So now the 'nasty' bit - we need to see some code dealing in 'canvas' coordinate numbers :O(

Remember, this is what puts most developers off their own printing - BUT - we normally don't have too look ! So keep calm.

=====================================================================

So inside each overloaded method, for every type of element the Printing Class has to deal with, we need to convert the input value to a canvas point. Obviously we need a method to convert each of the three different systems to the required canvas point.

Luckily, once this is done and tested, we can basically forget about them, also one of the conversions is very easy. To see the other two look in Appendix J - 'converting coordinates'.

In that appendix section you will also see the other few conversions which allow to go from 'Paper' to 'Page' and many more. It is actually easier to bite the bullet and make and test the conversion methods, than to fiddle around at coding time - when you should be focusing on the design aspects of your work.

What does the Conversion Do ?

For those who may be interested, I will briefly explain what is being done in the arithmetic of the conversion method. Note at this point that the conversion methods are not for the developer's use, just for use by the class methods to make the user interface much easier.

If the 'Page' coordinate system is to know exactly where we point to, when specifying the position on a page in terms of lines down and percentage across, then it needs to know just how deep a line is in canvas pixel terms, or figures. This LineHeight is found from PageLength which is found as part of the page boundary calculations in the previous section. For historic reasons the variables are named 'r8' for REAL8 and since the methods are tried and tested, and work, they have been translated into modern Vulcan code as such.

The 'LineHeight' property of the PC is set at the time the lines on a page is 'set'. So if we have a LineHeight set for 60 lines on a page, then 30 lines down we would expect to be half way from the top. Or 50% down the page.

However, before we can simply convert the position down and across into canvas coordinates, we have to take into account the top and left margins, these are areas in which we choose not to try and print. Many printers can print to the edge of the paper, and other bits of things come into this too.

So to end up with the point coordinates for the canvas system, we need to add the margins to the adjusted values for down and along, and then round to the nearest integer.

If we wish to see all about this conversion process, we need to examine how the LineHeight property is set at the time of 'setting' the number of lines on a page. View the code below :-

=========================================================================================================================

You will notice a nice human element in this, where for all the mathematics and arithmetic, I needed to adjust the formula for the line height by trial and error with what the printer produced. So this is the part of the production line where the robots are turned off :-)

The choice to have limits of 20 and 130 for the number of lines to a page, was mine, and an arbitrary choice after a few years of experience with the PC.

By now, you are probably seeing just what a good idea it is to hide the underlying canvas numbers, and leave the design process to a simpler, and much neater way of doing things.

However, if you wish to 'talk' to the printer, then you have to talk in its language - well at least let the underlying methods do the job.

PHH - Sept. 2008.


'overloading' to keep things simple

Navigation: Section 1 - the Need for a Printing Class >

'overloading' to keep things simple

 Return to chapter overview

This is now a favourite topic for me, as related to this 'Printing Class' (PC). Since my first working Visual Objects version of my PC, it has been possible to vastly simplify and 'clean up' the PC methods as we shall see below.

By providing different overloads, that is methods with the same name but different input parameter signatures, I now have a PC which is much easier to use. And that has to be good news for any user, including myself !

In the style of the dotNet SDK lets list out the methods which are most commonly used by me for printing to a page :-

========================

Text()

TextRight()

TextUp()

TextRestrictRHS()

TextCenterPage()

TextCenterBetween()

TextUnder()

TextCenterPageUnder()

Block()

BlockRestrictRHS()

BlockUnder()

DrawLine()

MemoPlace()

MemoPlaceFullyJustify()

BitmapPlace()

BitmapPlaceScaled()

ColTextLeft()

ColTextCenter()

ColTextRight()

ColTextDecimal()

GridRegularNbyM()

GridComplex()

========================

As you can see there are not many methods (22) - to think that so much varied printed output that can be achieved by their application. Before we look in detail, in the next sub-section, at what each group does, lets see how overloading has simplified the first one in the list, the Text() method.

The Text() method simply places a string of characters on the page at a point specified. The string of text will start at the point and go to the right as far as it needs to display the text.

So why have any overloads when the text string does not vary ? Well, remember we have a choice of three coordinate systems, so we need an overload for each. Let us look at all three together :-

========================================================

METHOD TEXT(oPGC AS PageCoords, cText AS STRING) AS VOID

LOCAL oPoint AS Point

LOCAL nLine AS double

nLine := oPGC:nLine

oPoint := ConvertPageCoords2Point(oPGC)

SELF:g:DrawString(cText , SELF:Font , SELF:Brush , oPoint)

SELF:NextLine := (INT)Math.ROUND(nLine + 1.5)

RETURN

METHOD TEXT(oPPC AS PaperCoords, cText AS STRING) AS VOID

LOCAL oPoint AS Point

oPoint := ConvertPaperCoords2Point(oPPC)

SELF:g:DrawString(cText , SELF:Font , SELF:Brush , oPoint)

RETURN

METHOD TEXT(oGC AS GraphCoords, cText AS STRING) AS VOID

LOCAL oPoint AS Point

LOCAL oPPC AS PaperCoords

oPPC := ConvertGraphCoords2PaperCoords(oGC)

oPoint := ConvertPaperCoords2Point(oPPC)

SELF:g:DrawString(cText , SELF:Font , SELF:Brush , oPoint)

RETURN

=======================================================

Since we are trying to make life easy for ourselves at the document coding level, we need to supply an overload for each of the three coordinate systems for most of the methods we create.

Also, by the three coordinate objects taking real numbers as parameters at instantiation, I have cleaned up what I had in my VO class. For those who may be interested, I had versions which only took integer values when I was working with lines down the page. I have found that this was restricting and that I then had 'real' versions of many methods which caused both 'bloat' and confusion. I can tell you that this PC as it is is easy to use and apply, and much better due to the use of overloading whenever necessary.

While on the subject of overloading, lets us now examine in some detail the methods for drawing a line. We will see when we should not get carried away with overloading for the sheer sake of it.

===========================================================================

METHOD DrawLine(oPoint1 AS Point, oPoint2 AS Point, nPenWidth AS INT) AS VOID

LOCAL oThisPen AS Pen

oThisPen := SELF:Pen

oThisPen:Width := nPenWidth

SELF:g:DrawLine(oThisPen, oPoint1, oPoint2)

RETURN

METHOD DrawLine(oPGC1 AS PageCoords, oPGC2 AS PageCoords, nPenWidth AS INT) AS VOID

LOCAL oPoint1, oPoint2 AS Point

oPoint1 := ConvertPageCoords2Point(oPGC1)

oPoint2 := ConvertPageCoords2Point(oPGC2)

SELF:DrawLine(oPoint1, oPoint2, nPenWidth)

RETURN

METHOD DrawLine(oPPC1 AS PaperCoords, oPPC2 AS PaperCoords, nPenWidth AS INT) AS VOID

LOCAL oPoint1, oPoint2 AS Point

oPoint1 := ConvertPaperCoords2Point(oPPC1)

oPoint2 := ConvertPaperCoords2Point(oPPC2)

SELF:DrawLine(oPoint1, oPoint2, nPenWidth)

RETURN

METHOD DrawLine(oGC1 AS GraphCoords, oGC2 AS GraphCoords, nPenWidth AS INT) AS VOID

LOCAL oPPC1, oPPC2 AS PaperCoords

oPPC1 := ConvertGraphCoords2PaperCoords(oGC1)

oPPC2 := ConvertGraphCoords2PaperCoords(oGC2)

LOCAL oPoint1, oPoint2 AS Point

oPoint1 := ConvertPaperCoords2Point(oPPC1)

oPoint2 := ConvertPaperCoords2Point(oPPC2)

SELF:DrawLine(oPoint1, oPoint2, nPenWidth)

RETURN

===========================================================================

Notice that we have a method for each coordinate system, and for some reason (must have been good) I also have one for the canvas system.

The point I wish to make is that we could have 'gone mad' and made extra overloads, one taking mixed coordinate points. However, this seemed to me to unnecessary as usually the user is working in one system or another, and will not be in the habit of swapping systems half way through drawing a single line. On the other hand, if you so wished, you could add further overloads to the method, as the PC as included is totally customizable.

Also, if you did add extra mixed system methods, it would not spoil the 'clean feel' of the class as seen above, as as applied to the document examples displayed in these notes.

Finally, notice that for each overload the 'DrawLine' method of the parent class is actually used to place the graphic line element on the canvas using canvas coordinates. For now we will keep well away from such coordinates.

If you are interested as to why we have a fourth form of the overload, which takes a canvas point object, then you will notice that the parameter format is consistent across all four. That is the number, type and order of the parameters is consistent.

PHH - Sept. 2008.


background to the PC methods

Navigation: Section 1 - the Need for a Printing Class >

background to the PC methods

 Return to chapter overview

Although 'Section 1' is not looking in detail at all the methods in the PC called 'BestDocument' it is preparing your for it by explaining the overall 'shape' to the class. What type of methods exist, why, and how they may be required to solve some of the problems faced with placing graphics elements on a page.

Right from the outset, let me explain that the methods which are included in the BestDocument printing class are simply there because I thought there was a need for them. There will be others which you may think are missing, which are not there simply because I have not yet had a need for them. One of the benefits of doing your own printing, with your own printing class, is that you can add to it whenever the need arises. As well as change and modify the methods which already exist.

When I started the process of constructing a printing class for myself, I looked at the sort of elements I needed to place on a page, and tried to simply make life easier for myself.

I will try to explain some of this by giving you a clear example. Look at the image below, which shows part of a typical document I need to create for my client. They print many such documents each day :-

==================================================================================

==================================================================================

Look at the middle section, at the columns of product item details. These are data driven and are actually derived from arrays of data.

Rightly or wrongly, it seemed a good idea to me at the time, to have a way of placing a 'block' of text on the page in one easy go. So in fact the middle section is created by my code, by placing five separate 'blocks' on the page.

Take the left block as an example, the 'Item', 'Code', '===', and 'Z7032' ....... are all part of a text block placement. In my code, I prepare the block (lets call it block one) on the left at the same time as the other four blocks. You may guess that the first text item in block two is actually "" or an empty string.

Because I know the line number of the top left position for each of the five blocks it is quite easy to make the document as it is shown. The secret to all printing is get you data prepared correctly before you try to place it on the page, don't mix printing and data preparation. So at the point at which I wish to place the blocks I have five strongly typed string arrays.

Lets look at the Block() method to see what it looks like :-

===================================================================

Simple I hear you say ! Just takes the point of the start of the first line of the block, in 'Page' coordinates, and the system array of the text items.

Notice that the code relies on placing a line of text on the page in the same coordinate system, going down a line at a time. If we had required that the block text items were right justified we could have used the TextRight() method. However, if this is what you need then a new method group called BlockRight() is probably best created.

More explanation will be given to other details later.

Before we leave this example, notice the paragraph 'parts' of the document. For historic reasons, I call the method to place a continuous 'chunk' of prose, text if you prefer, MemoPlace() as this was how it was described in the book I first followed.

What I have done to produce the printed output shown, is to fully justify the lines in the memo portion, so I have called this method MemoPlaceFullyJustify. We will not go into this in any detail till Section two, just notice that in order to keep a client satisfied you sometimes have to go to a lot of extra work.

Reports :

In this last part of this sub-section let us consider a different type of printed format, the columnated report. See below for an example :-

It does not take a genius to see that in order to create such a report type of document we need some specialized methods. Some to handle the column text, and at least one other to place the line grid on the page.

Look at the columns more closely and you see that some are left justified, some right, and the date column is centered. You may even think the decimal numeric columns are right justified, but you are only partly correct. We will leave it till later to discuss in more detail how we line up decimal numbers for a report, so as not to make the client go giddy :0)

So all we need to have is :-

ColTextLeft()

ColTextCenter()

ColTextRight()

ColTextDecimal()

VLineSet()

Obviously, the VLineSet() method could produce an output like that shown, but I actually found it easier to use the DrawLine() method with 'Page' coordinate points.

PHH - Sept. 2008


extending the Printing class

Navigation: Section 1 - the Need for a Printing Class >

extending the Printing class

 Return to chapter overview

You may a first think that there seems to be so many methods that you could do everything you wished using these alone.

However, my feeling is that there will always be some task you come up against in the future which would be easiest tackled if a new method was added to the existing class.

As mentioned earlier, it is easy to add and adapt the BestDocument class, to provide an even more comprehensive general printing class. The methods you see in the class at the moment are only there because my clients had a need for such to be created. I am well aware that they have only a narrow set of very specialized needs, so there is probably much more which needs adding to solve other common printing problems.

One area in which there may be many methods 'lacking', is that of shapes. Polygons, both filled and outlined, have not been included - simply because of lack of need.

I used to have a 'double box' method, which placed a purple rectangle made from two lines close together (a box with in a box if you prefer), but they no longer require this to be printed on the letters. Consequently, this method get not 'make it' into the latest re-write of the class.

The laser label printing code has been done to solve my personal need for an easy way to send out my family Christmas cards. I am sure this side of things could be vastly improved.

Just provide me with a need, and I will make and test the new methods !

On a positive note, and one which seems to fit in with the modern dotNet approach to software production, there will be no need to upgrade the BestDocument class in order to print graphs from ZedGraph, to freely available graphical package. As you can see from my tests (shown below) ZedGraph has it all built in. All you need to do is right click a graph and you have a menu choice. Copying to clipboard and getting a PrintPreview dialog is all at hand :-

Adding Methods :

If you are to add extras to this class, try to keep the the way things have been done so far, keep the method names clear and uncluttered, and use overloading in the same way as we have so far seen it used.

Of course you could also sub-class and add your own methods in you own style, but I will keep to extending things - as and when I see fit.

There is one area that I am aware of at present, that could have extra methods added to make life even easier, and that is to do with image placing. Sometimes you wish to place images by different corners, depending on th task in hand. The upper left corner of the image is what is used by the 'BitmapPlace' method in the BD class at the moment, and this is because the graphics class method used - DrawImage() - uses that positioning point.

It would appear that the 30 overloads of the DrawImage() method, all use top left as the reference point. For us, it may be handy to have bottom left, or even be able to specify an area on the page and have the area covered by adjusting the image scaling. There are a host of possibilities in this little problem on its own.

PHH - Sept. 2008.


Section 2 - the 'BestDocument' class

Navigation: »No topics above this level«

Section 2 - the 'BestDocument' class

 Return to chapter overview

Strictly speaking, the 'BestDocument' class (BD) is not a printing class but a document class.

When I created the BD class I was aiming at making document production as easy as possible. For this reason I included a Font, Color, Pen and Brush object as a property of the BD object itself. The reason behind this was that I wish to remove the burden from the coding, for the need to specify font and brush etc., each time I wanted to place an element on the paper /page.

My thinking was - apply a font and have the BD class methods use that font until changed by the code. This also goes for the color used by pen and brush.

Purists would say this should not be part of a 'Printing Class' - OK then we have a document class, and we make life easy :-)

PHH - Sept. 2008.


setting up the canvas

Navigation: Section 2 - the 'BestDocument' class >

setting up the canvas

 Return to chapter overview

In the last section we looked at the method in the 'BsetDocument' class which after communicating with the printer driver software for the selected printer, did all the necessary tedious arithmetic to set 'things' up ready for the developer to use convenient coordinate systems.

In fact I have a simple method, included in the class at the moment, which displays some of the calculated values after printer selection.

Check out the details below for my OKI C5600 colour laser printer :-

============================================================================================================================================

As you can see, this is pretty 'raw' stuff, and none of the values have been rounded or chopped. We see the real numbers in their full 'glory'.

Remember however, that all these values are used by methods at least a whole 'layer' below what the developer uses. The developer and code sees nothing this nasty, it is just that they are required for these 'low level' methods to talk with the printer driver and get the results he/she is desiring.

What may be of interest to you is the Line Height, as this is what changes for the 'Page' coordinate system when the lines on a page is changed. 12.796 is for 60 lines, and so we would expect this to increase if we applied 40 as a page line setting.

Let us try making the change to see which values change, and by how much :-

===============================================================================================

Notice that the LineHeight value has gone up to what you may have predicted it would.

This LineHeight value is used directly in the method to convert 'Page' coordinates into a point on the canvas - which is what the printer driver understands.

So, before we move on, let us summarize - these values are found by applying the SetPageBoundaries() method in the printing class.

I think I used the display method just to check what was going on, and that I had got my code correct. If you so wished you could improve it, or add to it, by printing extra values.

Myself, once I know things are working at a 'lower level', I prefer to deal on the more friendly level of 'Paper' and 'page' coordinates.

Fonts and Lines - 'Page' coordinate system :

I would like to make one important thing very clear to you before it causes any confusion. When you use a method to place an element on the page it can go wherever you choose.

One element may be placed over one already there, are partly over a previous element.

This means that when we place a text string on a particular line, it goes where we place it, no matter what the font size. It has nothing to do with text we may print on the line above or below.

This is simply expressed by saying the LineHeight and the TextHeight property are totally separate. Normally, the text will not be as high as the line height, but, it is quite possible to have overlap if we make out settings such as to bring this about.

Lets try to show this here :-

===============================================

============================================

============================================

=============================================

Okay, what exactly do we have in the four small images above ?

Well, with the same lines per page setting, I have used a font size of 14,12,10 and 8 to produce the page of the report.

Nothing else has changed, but you will see that the column printing methods as used here, don't allow text to flow into the adjacent column, so truncation or 'cut-off' takes place.

In the largest font size of 14 the lines have not yet overlapped, but we can soon change that :-)

============================================

With the font size set at 16 then we start to see 'element' overlap. So the space covered by the text is greater than that between the top of the line below and the bottom line above. Here we would say the text height is greater than the line height.

Later you will see that we need to consider which element is placed (and printed) first when we are dealing with signatures.

For now, just remember that each element placed by a method is done so quite independently of any other. But, the overall effect may mean we wish to place one element before another.

This issue becomes one of design - which element is partly over another etc. For the most part, many of us will not overlap elements at all.

Finally in this section, let us see how to make some code so that we instantiate a 'BestDocument' object and set up the canvas ready for placement of various elements using the higher coordinate systems.

EXPORT oBD AS BestDocument

..........................

..........................

======================================================================================

Here is some code which was used just before we created the report pages shown above. It is actually quite straight forward and also is very similar each time we want to print any type of document.

Also remember that the BestDocument class (BD) inherits from the dotNet PrintDocument class so all the SDK stuff for that applies to oBD.

You will have to take it on trust that the printer has been selected earlier in the application and that the focus printer is what was previously hard wired in the code, "OKI C5600".

After supplying the printer name to our newly instantiated BD object, we then supply the orientation. This is important as it is used in the SetPageBoundaries() method to calculate the necessary canvas values.

Once the boundaries have been calculated, the code sets the lines on a page, and here we are saving 8 lines for the header and footer region of the report page. We will deal with this later in necessary detail.

Finally, the code goes on to specify an event handler, and again we will cover this in detail later. It is just sufficient to say that we have the oBD object fully ready for code to be used to place elements on the page.

The placing of the elements, for this example, takes place in the "PreviewRG()" method pointed to in the event handler line.

Let us move to the next section and learn more about using the coordinate systems for placing elements on the page, without having to worry about 'nasty' canvas values :-)

PHH - Sept. 2008.


using coordinate systems

Navigation: Section 2 - the 'BestDocument' class >

using coordinate systems

 Return to chapter overview

The whole point of the choice and use of the coordinate systems provided in BD is to make life easy for the developer.

Simply be moving away from 'canvas' coordinates, life gets a lot easier. However, by providing three different 'high level' coordinate systems we make it even easier still.

Every time you wish to place a graphic element on the page, you first need to choose a coordinate system to use. Choose the one which makes the task the easiest.

Remember, the way I have set up the methods, the same coordinate system needs to be used in the one method, if it requires more than one point.

By "graphic element" I mean text, as well as line, shape and image. These are all 'drawn' by the GDI+ underlying code.

The 'Page' coordinate system is probably the one I use the most for my documents, and I have used it for the Report Generator too - see sections 9 and 10 for this.

There will be times when you wish to ignore the lines you have set up for the page, and just place an element by percentage position down and across, in that case use the 'Paper' coordinate system.

I suspect that most will not use the 'Graph' system very much at all, however, it is there if you need it.

Mixing how you place elements :

Before we get focused on details of placing elements to make nice documents and reports, we need to make an important point.

Setting the lines per page can be done many times during the creation of one page of output. Setting the number of lines on a page is just a way of setting the LineHeight property, which in turn effects the point at which 'Page' coordinates are calculated. But it is sometimes very useful to change the lines per page (LPP) setting in order to get the desired layout.

Look at the image below - which we saw earlier in the notes :-

==================================================================================

===================================================================================

When I make a document / letter like this, where the product items need to be listed, I often set a larger number of lines per page for the central section, than I do for the rest of the letter.

This means there is more space to place the product lines, as the central region has a fixed size. Obviously, once you change the LPP setting then line 13 in one system is NOT in the same place as line 13 in the other.

However, if you the switch back to the same LPP setting as previously, then line 13 is the same as it was for the same setting previously.

You will soon catch on to how this works, it is very easy really.

To reaffirm - both the change of LPP, and the change of coordinate system use, is available to make life easy. When the element in focus is being placed, it has nothing to do with what happened before, or what will happen afterwards. You are in control of that. If you wish to make code to have a 'page layout' system which counts lines, and points to the next line to be used, then fine. You can do that yourself.

What you have to do is make sure you do not restrict yourself in any way - the beauty of the BD class is that it is extremely flexible.

Defining some Points :

Okay then, lets now see how we can instantiate and use a coordinate object.

For a bit of fun, lets try to use the three different coordinates systems to specify a point on the page as close to the middle as possible.

First we will try the 'Graph' system as we all all familiar with that from school mathematics.

LOCAL oGC AS GraphCoords

oGC := GraphCoords{50, 50}

Now for the 'Paper' system :-

LOCAL oPPC AS PaperCoords

oPPC := PaperCoords{50,50}

Finally, the 'Page' system :-

LOCAL nLPP AS INT

nLPP := 60

oBD:ResetLinesPerPage(nLPP)

LOCAL oPGC AS PageCoords

oPGC := PageCoords{30,50}

Notice that in order for the '30' to mean what we require we have to set the LPP to 60. We could have used 40 and 20 just as well, or another suitable pair of values.

I hear you say, "that was not a good choice of position", since we have so many '50's that we can't see the difference in the correct use of the systems. I would have to agree, but it does cause us to focus on the problem now that it has been done.

To learn more, lets change our target position, lets say we are now aiming to specify a point at the extreme left of the page in focus, and half way down the page.

Now let see these three instantiations once more :-

LOCAL oGC AS GraphCoords

oGC := GraphCoords{0, 50}

Now for the 'Paper' system :-

LOCAL oPPC AS PaperCoords

oPPC := PaperCoords{50,0}

Finally, the 'Page' system :-

LOCAL nLPP AS INT

nLPP := 60

oBD:ResetLinesPerPage(nLPP)

LOCAL oPGC AS PageCoords

oPGC := PageCoords{30,0}

Now we see some differences. We notice that some numbers seem to be reversed.

Well that is because the systems are different - a graph system is X along, then Y up (both percentages).

A paper system is down and across, both percentages. While the page system is lines down and percentage across.

Once again it is easier than you think - AND- because we have the 'Print Preview' screen to do plenty of test viewing, without wasting paper, we can view our results and adapt our code as we go. This approach works well, and was used a great deal to develop the Report Generator. Saved loads of paper, I feel quite 'green' :-)

Let us move on to the next section to see you to convert from one coordinate system to another.

PHH - Sept. 2008.


converting coordinates

Navigation: Section 2 - the 'BestDocument' class >

converting coordinates

 Return to chapter overview

So far in the Section 1 we have seen how 'convert' methods are provided in the BD class in order to change the 'Paper', 'Page, and 'Graph' coordinate object values into canvas values to be used by the GDI+ system.

This was to keep the user at a distance from the more difficult to apply canvas coordinate system.

Now we will move on to look at some extra 'convert' methods which are there to make life easy for the developer.

Sometimes while laying out a document - by placing elements on the page - you have a point on the page in one coordinate system, but wish to use another system for the next element. To aid the developer, and for class methods to use themselves, a few others methods have been coded.

Developer methods :-

ConvertGraphCoords2PaperCoords( )

ConvertPaperCoords2GraphCoords( )

ConvertPageCoords2PaperCoords( )

ConvertPaperCoords2PageCoords( )

Class method use only :-

ConvertPoint2PaperCoords( )

ConvertPoint2PageCoords( )

ConvertPoint2GraphCoords( )

The names of the methods indicate both what is expected as an input parameter, and what will be returned.

It is expected that the developer may use any of the top four from time to time, but that the class methods themselves will exclusively use the bottom three. If the developer is not working in point objects based on canvas coordinates, then they won't need to convert from points to Paper, Page and Graph objects.

However, the developer may wish to create a Paper coordinate object from an existing Page object, and vice versa. Graph coordinate objects can also be converted in a similar way.

Although the complete set of 'convert' methods are listed in 'Appendix J' we will look at one in detail here :-

===================================================================

Because we are dealing in canvas coordinates we need to see the tedious arithmetic again. However, it is sufficient to just see what is being done in the method.

Point coordinates are X and Y based, and these X and Y values are turned into percentage down and percentage across the paper, using the numeric properties found by the SetPageBoundary() method.

Quite obviously, a PaperCoords object is returned.

Remember however, that these are not for your use, so don't worry about them - they are there for the internal use of some of the other class methods.

Now let us look at one the developer may use :-

=============================================================================

This method takes a Page coordinate object and returns a Paper coordinate object. Now you will notice that the easiest way to code the method was to use a couple of already defined methods.

Once these have been declared and tested at design time, all we need do as developers is know how to use the method to do a 'high level' coordinate conversion, if and when we need it.

It feels as if we are just about there - I mean a stage when we can get into creating documents rather than fighting with numbers.

Let us now move on to the next section where we will start to use the fundamental BD class methods to create document output.

PHH - Sept. 2008.


some fundamental methods

Navigation: Section 2 - the 'BestDocument' class >

some fundamental methods

 Return to chapter overview

Okay, lets move on from 'low level' coordinates and numerical values, to putting some graphic elements on the 'paper'.

I refer to 'paper' here as the area of the canvas on which we can have elements, or parts of elements, showing at time of printing.

Often later I will also refer to 'page. In many ways the paper and page are the same thing - they have the same physical area - but when the 'page' metaphor is used we are thinking of the 'printed page'.

The printed page has lines, as in a book, and so 'Page' is linked to page coordinates - with lines down and percentage across. Alternatively, 'paper' is linked with paper coordinates - with percentage down and percentage across.

Let us start simply, and use the paper metaphor in our example below, and then come on to the page metaphor straight after.

I wish to use the page metaphor in subsequent examples below, as this is most commonly used by me in my 'day-to-day' documentation.

We shall start be selecting just four fundamental methods :-

Text()

TextRight()

DrawLine()

BitmapPlace()

The image of the paper is quite stunning for our first attempt, and was quite easy to do as well.

Remember, we only used four different BD methods to achieve this result. I told you printing was easy :O)

Let us now look at the code which was used to generate this document :-

=============================================================

METHOD MyTestDocument(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont9A, oFont16A, oFont22Ab AS Font

oBD:SetGraphics(e:Graphics)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont9A := Font{"Arial",9}

oBD:Font := oFont10A

//----------------- Headings ------------------

LOCAL oPPC1, oPPC2, oPPC AS PaperCoords

LOCAL cHead AS STRING

oPPC1 := PaperCoords{1, 2}

oPPC2 := PaperCoords{1, 98}

cHead := "Testing Report Printing : "

oBD:TEXT(oPPC1, cHead)

LOCAL oDT AS DateTime

oDT := DateTime.TODAY

cHead := "Printed on :- "+oDT:ToLongDateString()

oBD:TextRight(oPPC2, cHead)

oPPC1 := PaperCoords{5, 2}

oPPC2 := PaperCoords{5, 98}

oBD:DrawLine(oPPC1, oPPC2, 1)

oPPC1 := PaperCoords{96, 2}

oPPC2 := PaperCoords{96, 98}

oBD:DrawLine(oPPC1, oPPC2, 1)

//------------------- Footings --------------------

oPPC1 := PaperCoords{98, 98}

oBD:TextRight(oPPC1, "Page : 1 of 1")

//------------------- Body -------------------

oBD:Font := oFont22Ab

oPPC := PaperCoords{20,15}

oBD:TEXT(oPPC, "Thanks to Michael Rubinstein !!!")

oPPC := PaperCoords{30,20}

oBD:TEXT(oPPC, "For supplying the 'IPicture' library.")

oBD:Color := Color.CadetBlue

oBD:ReSetBrushAndPen()

oPPC := PaperCoords{40,25}

oBD:TEXT(oPPC, "It has helped me do a lot.")

oBD:Color := Color.RosyBrown

oBD:ReSetBrushAndPen()

oPPC := PaperCoords{50,30}

oBD:TEXT(oPPC, "We printed signatures on letters.")

oBD:Color := Color.Purple

oBD:ReSetBrushAndPen()

oPPC := PaperCoords{60,35}

oBD:TEXT(oPPC, "And images too !")

oPPC := PaperCoords{70,40}

oBD:BitmapPlace(oPPC ,"C:\VIDE83\Projects\NewDocument\Applications\BestDocument\Resources\Qmk2.jpg")

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

===========================================================================================

The first thing we should notice is that we needed to set a font for the Text(0 method to use, so we need to declare a few font variables.

Since I have setup the BD class to have one 'focus' font as a property, we need to set this class font each time we wish to have a different font. I have found it more convenient to do it this way rather than have many more fonts as part of the class.

So after declaring a few fonts, we pick up the 'graphics' surface from 'e' the event. This is passed to the oBD object as the surface on which to draw.

After placing the headings and footings using the first chosen font, "Arial 10", we moved down to print the body section with text elements in "Arial 22 Bold".

Just for some variation I then changed the colour of the pen - for more on colors, pens and brushes see Appendix B.

Normally you would rarely use a font as large as this, but I wished the Print Preview to show up without having to enlarge it too much - just to fit these notes.

There are a few other important things to notice on this page, before we move on to using other methods.

To place an image on the paper (either a JPG or BMP file format), we simply use the BitmapPlace() method with the full path for the image file. Note if a file is not found then nothing happens - that is no image or error message.

Also notice that a method called ReSetBrushAndPen() is used to apply the new color to the pen and brush. If you do not do this the color of the text with stay as it was previously. I suppose I could force this to happen by creating an event related to the color change - I will look into this for the future.

Before we get to our last important point, let us notice that the TextRight() method is used when we are certain of the point at which we wish the right hand end of the text to finish. The method works back from there and finds where to start the placement. The interesting thing about this is that somewhere in the code, below the level at which the developer designs the page, we must be finding out just how long the string of text actually is, in the font family, style and size we are using.

If you are interested in this idea then follow the path back from the TextRight() method code, to the PCTextLength() method, and then the TextLength() method. By this time you will see that we are then using the Graphics class 'MeasureString' method. You may like to look this up in the SDK documentation too.

Finally, since this is a one page creation, we need to set the event property 'HasMorePages' to false.

This is a property / field which we have to control in our print event handler method, in order to get multiple pages. If we leave it out of our code it will still produce a single page as seen here - BUT - since it is crucial for multiple pages we need to get into the habit of doing things correctly.

We will see more of this later.

Let us now more on to using some less fundamental methods.

PHH - Sept. 2008.


some 'more complex' methods

Navigation: Section 2 - the 'BestDocument' class >

some 'more complex' methods

 Return to chapter overview

We have had our first pleasant success with just three fundamental methods, and one which was a bit more complicated.

You may argue that the TextRight() method was not a basic, or fundamental one, as it used one or two previously defined methods in order to achieve its 'goal' or effect.

In this section let us look at some other ways we may wish to place text on paper. Remember, when we place numbers and dates, these too become text, or string values, before we place them on the paper. So placing text is very fundamental to good printing.

The more tools we have for text placement, and the more flexible these are to use, then the easier the document creation task becomes.

Before we move on to placing 'chunks', 'sections' or 'blocks' of text in the next couple of sub-sections, let us see what can be done with some of the more complex methods for text placement. We have seen TextRight() in action previously - where we placed the header date, and footer page details - let us now look at the following :-

TextCenterPage()

TextCenterBetween()

TextUnder()

TextCenterPageUnder()

TextUp()

TextChop()

*TextRestrictRHS() //----- this is not used directly -----

( See sub-section .. "more advanced Methods" .... for TextUp() and TextUpTo() ... )

Note that when the method TextCenterPage() was created, the 'page of a book' metaphor was used. So we are referring to the width across the page. In order to place some text in the centre of a 'region' across the page the other method was created, called TextCenterBetween(). This does exactly as it 'says on the can' - place the text down the age and between two points. Obviously, only the across values of the points are used for the centering :-)

For now, lets get some experience with what we have available above. The only one which may need some explanation before we see them in use, is the last one named 'TextChop()' this limits, or restricts, the right hand side of the text. You need to use this when you don't wish the text to overflow into the next element on the right of the paper. You use this when you don't know how long the text will be before printing. However, before we get carried away with wishing to develop this further, we need to wait and see what the report 'column' methods can do for us a few sub-sections further on. And, for that matter the 'Block' methods.

=======================================================================================

Okay then, I have just spent half an hour enjoying myself with the creation of the document above, which is now my 'A' document in the experimental testbed application discussed in Section 6. At least I am prctising what I preach, and using the test 'tool' to gain experience with the 'BestDocument' methods.

The reason it took me so long to create was because I was both extending and editing the class methods as they were before hand.

Since I have not extensively tested the new re-write of the BD class, I am doing it 'on the fly' as it were. Saves time that way :O)

If nothing else, the image does look good, and interesting too. Let us go through some detail now to see what was done, and how.

Before we delve into the code, let us stand back and see what we have on our paper. I say 'paper' as I have used paper coordinates to create the document, rather than page of graph. They just seemed most apropriate for the task in hand - no other reason.

It is easy to see which text is underlined, and which is centred in some form or other. The lowest text 'region' has been centred between the vertical line 'across values'. The top centred text has used the page as its 'region' for reference.

The text on its side from TextUp() does not need much explaining, except to say the point specified was at the 'U' end and the text string flows down the paper. It would be nice to make a method where we can specify the finishing, or end, position. I will look into that, when I also look into placing text at an angle.

Finally, before we view code lines, let us talk about the stranger looking black and cadet blue three lines of text n the central region of the paper.

This text was made by printing the same three lines 'twice over'. Both placements used the TextChop() method to restrict characters at the righter most end of the lines.

The first placement was using a large value of 69 for a percentage width of the text lines, and as you can see only the 'e' is missing from the word 'otherwise' in the longest string. When the restricting width value was reduced to 39 there was far less of the text strings placed on the paper. Obviously we needed to change the colour from one placement to the next, otherwise we would not be able to observe what was going on.

==================================================================

METHOD MyTestDocumentA(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

oBD:SetGraphics(e:Graphics)

LOCAL oFont22Ab AS Font

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oBD:Font := oFont22Ab

//------------------------------------------------

LOCAL oPPC, oPPC1, oPPC2 AS PaperCoords

oPPC := PaperCoords{13,13}

oBD:TextCenterPage(oPPC, "[ Is this at Page center ? ]")

oBD:ApplyNewColor(Color.Green)

oPPC1 := PaperCoords{17,20}

oPPC2 := PaperCoords{17,80}

oBD:TextCenterBetween(oPPC1, oPPC2, "[ center between 20 / 80 ?? ]")

oBD:ApplyNewColor(Color.Chocolate)

oPPC1 := PaperCoords{21,30}

oPPC2 := PaperCoords{21,70}

oBD:TextCenterBetween(oPPC1, oPPC2, "[ now 30 & 70 !! ]")

oBD:ApplyNewColor(Color.Purple)

oPPC := PaperCoords{25,13}

oBD:TextCenterPageUnder(oPPC, "< underline center page >")

//--------------------------------------------------------

oBD:ApplyNewColor(Color.RosyBrown)

oPPC := PaperCoords{1,1}

oBD:TextUnder(oPPC, "Underlined Text :")

oBD:ApplyNewColor(Color.Blue)

oPPC := PaperCoords{26,94}

oBD:TextUp(oPPC, "Upstanding Text")

//--------------------------------------

LOCAL nChop AS Double

nChop := 69

oBD:ApplyNewColor(Color.CadetBlue)

oPPC := PaperCoords{39,1}

oBD:TextChop(oPPC, "This is my first line of text",nChop)

oPPC := PaperCoords{43,1}

oBD:TextChop(oPPC, "to demonstrate the chopping",nChop)

oPPC := PaperCoords{47,1}

oBD:TextChop(oPPC, "of characters which would otherwise",nChop)

//----------------------------------------------------------

nChop := 39

oBD:ApplyNewColor(Color.Black)

oPPC := PaperCoords{39,1}

oBD:TextChop(oPPC, "This is my first line of text",nChop)

oPPC := PaperCoords{43,1}

oBD:TextChop(oPPC, "to demonstrate the chopping",nChop)

oPPC := PaperCoords{47,1}

oBD:TextChop(oPPC, "of characters which would otherwise",nChop)

//-------------------------------------------------------------

oBD:ApplyNewColor(Color.Black)

oPPC1 := PaperCoords{59,50}

oPPC2 := PaperCoords{75,50}

oBD:DrawLine(oPPC1, oPPC2, 2.5)

oBD:ApplyNewColor(Color.Red)

oPPC1 := PaperCoords{59,80}

oPPC2 := PaperCoords{75,80}

oBD:DrawLine(oPPC1, oPPC2, 4.5)

oBD:ApplyNewColor(Color.BlueViolet)

oPPC1 := PaperCoords{59,50}

oPPC2 := PaperCoords{59,80}

oBD:TextCenterBetween(oPPC1, oPPC2, "will this work")

oPPC1 := PaperCoords{63,50}

oPPC2 := PaperCoords{63,80}

oBD:TextCenterBetween(oPPC1, oPPC2, "and have")

oPPC1 := PaperCoords{67,50}

oPPC2 := PaperCoords{67,80}

oBD:TextCenterBetween(oPPC1, oPPC2, "a few words on")

oPPC1 := PaperCoords{71,50}

oPPC2 := PaperCoords{71,80}

oBD:TextCenterBetween(oPPC1, oPPC2, "display ...")

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

=========================================================

Follow through this code for yourself, as there is not much of any difficulty - that is why we made the BD class as we did, easy to use !

You may like to refer to these lines when you make your own documents however, it is always easiest to have working code to copy and adapt.

What you may like to discuss in detail is the code in some of the new methods we have used. First let us look at the one method to underline text specified in the canvas coordinate system, as this is used by the other three overloads for Page, Paper and Graph :-

======================================================

METHOD TextUnder(oPoint AS Point, cText AS STRING) AS VOID

SELF:g:DrawString(cText , SELF:Font , SELF:Brush , oPoint)

LOCAL nWidth AS Double

nWidth := SELF:PCTextLength(cText)

LOCAL cUnder AS STRING

LOCAL r4Length AS Double

cUnder := "_"

r4Length := SELF:PCTextLength(cUnder)

DO WHILE r4Length < nWidth

cUnder += "_"

r4Length := SELF:PCTextLength(cUnder)

ENDDO

SELF:g:DrawString(cUnder , SELF:Font , SELF:Brush , oPoint)

RETURN

======================================================

Simply the line of text is printed as if it were a string of text placed in the normal way. Then a line of underline characters is built up to be as long (not longer) as the text string itself. Each time we add a new character the length of the growing string is found with the 'low level' class method 'PCTextLength()'. Remember PC stands for percentage.

This approach is used for all the underlining class methods.

METHOD TextUnder(oPPC AS PaperCoords, cText AS STRING) AS VOID

LOCAL oPoint AS Point

oPoint := ConvertPaperCoords2Point(oPPC)

SELF:TextUnder(oPoint, cText)

RETURN

As you can see above for the overload which takes a paper coordinate, the method is just a 'wrapper' type - and converts to canvas point first, before going on to use the base method.

Now let us see a 'center' method, and we can use the 'CenterPage' variety as an example :-

===========================================================

METHOD TextCenterPage(oPPC AS PaperCoords, cText AS STRING) AS VOID

LOCAL oPoint AS Point

LOCAL nDown, nAcross AS double

nDown := oPPC:nDown

nAcross := SELF:FindCenterStart(cText)

oPPC := PaperCoords{nDown, nAcross}

oPoint := ConvertPaperCoords2Point(oPPC)

SELF:g:DrawString(cText , SELF:Font , SELF:Brush , oPoint)

RETURN

===========================================================

This is very simple and just relies on the 'low level' class method called 'FindCenterStart()' to provide a position across starting position.

Let us look at the 'FindCenterStart()' method below :-

==============================================

METHOD FindCenterStart(cText AS STRING) AS Double

LOCAL nResult AS Double

nResult := 50

LOCAL r8LeftBit, r8CenterBit, r8Hundred AS Double

IF cText:Length < 1

RETURN nResult

ENDIF

r8Hundred := 99.9999

r8CenterBit := SELF:PCTextLength(cText)

r8LeftBit := 0.5*(r8Hundred-r8CenterBit)

nResult := Math.ROUND(r8LeftBit+0.5)

RETURN nResult

==============================================

Although it looks a little dated, and seems to have a style reminiscent of the old Visual Objects class, it does the job and uses just dotNet static methods (which to VOers are really just functions).

Once again you will see the 'low level' method 'PCTextLength()' is use, as it is quite fundamental to this approach of text placement.

The 'TextCenterBetween()' method was written just now, to allow me to create the page shown - so it is more modern in its style of coding (I hope!).

==================================================================================

METHOD TextCenterBetween(oPPC1 AS PaperCoords, oPPC2 AS PaperCoords, cText AS STRING) AS VOID

LOCAL oPoint AS Point

LOCAL nDown, nAcross, nDown1, nAcross1, nDown2, nAcross2 AS Double

LOCAL nDiff, nDiff2, nWidth, nLenText, nStart AS double

LOCAL oPPC AS PaperCoords

nDown1 := oPPC1:nDown

nDown2 := oPPC2:nDown

IF ! nDown1 = nDown2

RETURN

ENDIF

nAcross1 := oPPC1:nAcross

nAcross2 := oPPC2:nAcross

nWidth := nAcross2 - nAcross1

IF nWidth <= 0

RETURN

ENDIF

cText := cText:TRIM()

nLenText := SELF:PCTextLength(cText)

IF nLenText > nWidth

RETURN

ENDIF

//=====================================

nDiff := nWidth-nLenText

nDiff2 := nDiff/2.0

nStart := nAcross1+nDiff2

nDown := oPPC1:nDown

nAcross := nStart

oPPC := PaperCoords{nDown, nAcross}

oPoint := ConvertPaperCoords2Point(oPPC)

SELF:g:DrawString(cText , SELF:Font , SELF:Brush , oPoint)

RETURN

====================================================================================

Most of the code is for 'safety' and checks to see that the input parameters have values which are 'doable'. As with most BD methods, if there is a problem, nothing happens and things continue. This is the best way to handle printing, otherwise you may not get very far :0)

The system seems to use a similar approach - if you try and place and print at places off the canvas, it does its best to ignore and continue in some way. 'Keep going' seems to be the motto.

Below is a 'wrapper' method based on the one above :-

================================================================================

METHOD TextCenterBetween(oPGC1 AS PageCoords, oPGC2 AS PageCoords, cText AS STRING) AS VOID

LOCAL oPPC1, oPPC2 AS PaperCoords

oPPC1 := SELF:ConvertPageCoords2PaperCoords(oPGC1)

oPPC2 := SELF:ConvertPageCoords2PaperCoords(oPGC2)

SELF:TextCenterBetween(oPPC1, oPPC2, cText)

RETURN

================================================================================

Again, just a matter of point conversion, and then a call to the base method.

Right then, where does that leave us ? Well, it appears that all we need do to complete this section is see what goes on in the TextChop() method.

In fact as you will see below, it is not as complex as you may think :-

====================================================================

METHOD TextChop(oPPC AS PaperCoords, cText AS STRING, nWide AS Double) AS VOID

LOCAL cResult AS STRING

cResult := SELF:TextRestrictRHS(cText, nWide)

SELF:TEXT(oPPC, cResult)

RETURN

====================================================================

We seem to simply rely on a 'low level' class method to chop, or restrict, our text string to the required length, if in fact it is that long in the first place.

Let us see what this base method looks like (remember users will not use it directly in their document creating code) :-

==========================================================

METHOD TextRestrictRHS(cText AS STRING, nWide AS Double) AS STRING

LOCAL cResult AS STRING

cResult := cText

IF nWide < 5

nWide := 5

ENDIF

IF nWide > 100

nWide := 100

ENDIF

LOCAL nPCtarget, nPCnow AS Double

nPCtarget := nWide

nPCnow := PCTextLength(cText)

IF nPCnow <= nPCtarget

RETURN cResult

ENDIF

LOCAL nLen AS INT

DO WHILE nPCnow > nPCtarget

nLen := cText:Length

cText := cText:Substring(0,nLen-1)

nPCnow := PCTextLength(cText)

ENDDO

cResult := cText

RETURN cResult

===========================================================

This is just one of a couple of overloads but it is sufficient to just study this one at the moment.

After a bit of safety code at the start of the method, we are basically just reducing the string of characters provided until it just falls inside the required length. Obviously, no chopping gets done if the string is too short to start with.

Yet again the 'trusty' class base method 'PCTextLength()' comes into play. What would we do without it ?

Okay then, enough for now, lets have a break and then move on to placing blocks of text on the paper - a very useful technique indeed.

PHH - Sept. 2008


placing 'blocks' of text

Navigation: Section 2 - the 'BestDocument' class >

placing 'blocks' of text

 Return to chapter overview

Okay then, now we can move on to seeing some further complex methods of the BD class in action.

I suppose you could argue that because they are easy to use these are not complex - well, I would say they are not complicated to use, but because they use class 'base' methods to achieve their results, they are 'complex' !

The point that I had the idea of making this style of printing class 'tool', was when I really got going with my own printing. I suggest you think deeply about how you can use 'block' printing in your document creation. You may be very surprised just how often you can apply the use of some of the 'block' related methods.

Let us see which block methods we have at our disposal at this current moment :-

Block()

BlockRestrictRHS()

BlockRight()

BlockRightRestrictRHS()

BlockUnder()

BlockUnderRestrictRHS()

BlockCenter()

BlockCenterRestrictRHS()

BlockCenterUnder()

BlockCenterUnderRestrictRHS()

As you can see we have just pairs at the moment, no doubt we may make more if we see a need. Notice that each of the five 'flavours' of the block method has a 'restricting' version. This version simply allows the developer to stop any text running into the next column on the right hands side. We will see this is action later, after doing something simple to start with.

The whole point about the 'block' tools is that they take an array of text items, and then place them on the paper, one above the other. We simply need to pass the system array and the point which represents the top left position of the block to be placed.

I think you will soon catch on that if we are placing elements one above another, then we need a way to 'move to the next line'. This is straight forward for 'page' coordinates as we are using the lines down the page for positioning anyway. But, what do we do for the other two, 'Paper' and 'Graph' ?

The answer is simple, use one or more of the class 'base' methods to tell us how high a sample of text is - of the font style and size we are using. Then use this as an 'add-on' to step down the paper. Yes, the block methods as of now, always place the text items by stepping down the paper, or page.

For a change, instead of starting with a sample output image, let us look at the 'Block()' method code directly :-

====================================================

METHOD Block(oPGC AS PageCoords, aText AS STRING[]) AS VOID

LOCAL i, nLen AS INT

LOCAL cLine AS STRING

LOCAL nLine, nAcross AS Double

LOCAL oPGCthis AS PageCoords

oPGCthis := oPGC

nLine := oPGCthis:nLine

nAcross := oPGCthis:nAcross

nLen := aText:Length

FOR i := 1 UPTO nLen

cLine := aText[i]

SELF:Text(oPGCthis, cLine)

nLine +=1

oPGCthis := PageCoords{nLine,nAcross}

NEXT

SELF:NextLine := (INT)Math.ROUND(nLine + 0.5)

RETURN

====================================================

As you can see, we have a strongly typed array of strings as a second input parameter, the first being the point at which the top left of the block is to start.

Quite simply, the first item of text array is printed using the point supplied and the class method 'Text()'. Remember this places text 'left justified'.

Then we loop through the array items, adding a page line each time, to make a new point in 'Page' coordinates for the placing of the text item.

So what is the difference when we have no lines to guide us, and need the line height calculated from the font and attributes ?

=====================================================

METHOD Block(oPPC AS PaperCoords, aText AS STRING[]) AS VOID

LOCAL i, nLen AS INT

LOCAL cLine AS STRING

LOCAL nDown, nAcross, nAdTextHeight AS Double

LOCAL oPPCthis AS PaperCoords

oPPCthis := oPPC

nDown := oPPCthis:nDown

nAcross := oPPCthis:nAcross

nLen := aText:Length

nAdTextHeight := SELF:TextHeight("PLQjy")

FOR i := 1 UPTO nLen

cLine := aText[i]

SELF:Text(oPPCthis, cLine)

//----- move down for next line of block text -----

nDown += nAdTextHeight

oPPCthis := PaperCoords{nDown,nAcross}

NEXT

RETURN

=====================================================

The class 'base' method 'TextHeight()' is used to provide the 'step' down the page, as opposed adding another line as previously.

What does this 'base' class look like ?

METHOD TextHeight(cText AS STRING) AS Double

LOCAL oSizeF AS SizeF

LOCAL fResult, fHeight AS Double

oSizeF := SELF:g:MeasureString(cText, SELF:Font)

fHeight := oSizeF:Height

fResult := 100*fHeight/SELF:PageLength

RETURN fResult

The answer is quite simple - just find the height from the rectangle provided by the graphics class 'MeasureString()' method, then convert it from pixels to our percentage values - which we use in both 'Paper' and 'Graph' coordinate systems.

You may be interested to know at the time of writing this, there is not a graph overload for the 'Block()' method. This is simply because of need. I haven't needed one yet, and thought that this placement function was far less likely to be required when drawing pages with 'Graph' coordinates.

What I will say is, if we ever needed one we would simply make a 'wrapper' and convert our 'Graph' point to a 'Paper' point and then call on the same overload of the method as shown above. Make one for yourself as an extension exercise !

Remember this tip - the 'block' tools were created to handle the placement of line items which were of a column format. The sort of structure you come across often on invoices and delivery notes. At the healthcare companies I work with we do lots of 'customer enclosed notes', which are sent out with the healthcare products.

With proportionally spaced fonts it is not possible to line up such data by printing 'lines'. This 'line' approach is fine for fixed space fonts, and possibly 'quick and simple' internal reports (Section 12), but it is not of good enough quality these days, for external documents.

However, before we get carried away, and try to improve things too much - wait and see what our class methods are like for handling columned reports. In the end, we will have plenty of flexibility to help us design what we choose.

OK, let us make some nice output using blocks.

==============================================================================================================

Well, it is not 'that' nice to look at, but it does the job - AND - it was very easy to code. Let us see what the code looks like below.

Also notice that we are having to create our own data at the same time. This would normally be done elsewhere and by some business logic code.

However, the literal string declaration makes life a little easier :-)

==============================================================================

METHOD MyTestDocumentD(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont16Ai, oFont22Ab, oFont22Ai AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(48)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont22Ai := Font{"Arial",22,FontStyle.Italic}

oFont16Ai := Font{"Arial",16,FontStyle.Italic}

//------------------------------------------------

LOCAL aF, aL, aD AS STRING[]

aF := <STRING>{"first","name","=====","Tom","Dick","Harry","Sally","Mary","Jane","====="}

aL := <STRING>{"last","name","=========","Tomlinson","Dixson","Harrison","Shoebill","Mollitoff","Jensen","========="}

aD := <STRING>{"order","date","=========","01/01/2001","02/02/2002","06/06/2006","08/08/2008","03/03/2003","04/04/2004","========="}

oBD:Font := oFont22Ai

LOCAL oPPC1, oPPC2, oPPC3 AS PaperCoords

oPPC1 := PaperCoords{7,7}

oBD:Block(oPPC1, aF)

oPPC1 := PaperCoords{7,27}

oBD:Block(oPPC1, aL)

oPPC1 := PaperCoords{7,53}

oBD:Block(oPPC1, aD)

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

==============================================================================

If you are using system arrays prepared elsewhere - as you would normally - then only half a dozen lines prints you three column blocks, no matter how long they are.

Obviously, these simple block methods were designed to fit onto a single page.

Let us force the issue a little, and try to see when we may use the restricted version of the 'block' methods. Fist look at the output below :-

===============================================================================================

Notice I have placed the third column closer to the second one, and used the BlockRestrictRHS() method to display this second column.

I have had to provide an extra parameter, so the third parameter is the percentage across the page that the 'cut-off' takes place. I find that the 'rule of thumb' is make this one short of the value you use to start the next column and generally, things then look fine.

Notice that the 'on' has gone from the end of 'Tomlinson', and the 'n' from 'Harrison', as well as a couple of '=' signs in the header and footer.

Let us look at the part of the code which has changed :-

oBD:Font := oFont22Ai

LOCAL oPPC1, oPPC2, oPPC3 AS PaperCoords

oPPC1 := PaperCoords{7,7}

oBD:Block(oPPC1, aF)

oPPC1 := PaperCoords{7,27}

oBD:BlockRestrictRHS(oPPC1, aL, 43)

oPPC1 := PaperCoords{7,44}

oBD:Block(oPPC1, aD)

This shows we are 'chopping' the second column at 43, and starting the next at 44. Nothing else has changed.

Okay then, this is fine so far, but what about listing numbers when we wish them to align differently. Let us add an extra column of a numeric nature.

==================================================================================

A couple of things to notice here. Firstly, the numeric values are left aligned at the moment and don't look too good, and secondly, in order to keep the general alignment we have had to include empty strings in the data for the fourth column. Let us loot at the data and code before moving on to try to right align our numbers :-

aN := <STRING>{"","","====","6","13","2","47","0","28","===="}

oPPC1 := PaperCoords{7,69}

oBD:Block(oPPC1, aN)

This is all the code we needed to add to get our fourth column as it is at the moment - as displayed above.

Just before we move on to numeric alignment, we ought to see what output from the 'Page' coordinate overload of our block methods gives us :-

====================================================================================

Notice that the text is lower on the page as the value of '7' used to create the coordinates is not 7% down the paper but seven lines of a total of 40 set in the code as the lines per page (LPP).

I think you may fell, like me, that the lines have less space between then. Here is the code :-

oBD:ResetLinesPerPage(40)

LOCAL oPPC1, oPPC2, oPPC3 AS PageCoords

oPPC1 := PageCoords{7,7}

oBD:Block(oPPC1, aF)

oPPC1 := PageCoords{7,27}

oBD:BlockRestrictRHS(oPPC1, aL, 43)

oPPC1 := PageCoords{7,44}

oBD:Block(oPPC1, aD)

aN := <STRING>{"","","====","6","13","2","47","0","28","===="}

oPPC1 := PageCoords{7,69}

oBD:Block(oPPC1, aN)

As you get experience with blocks you will see that you can have lots of flexibility to change font size as well as LPP to get the effect you require.

Sometimes in enclosed notes - because I have a fixed and limited middle page region to use for product details - I set the LPP high and the font size smallish, that way I can get more product lines in the space available.

Numeric Alignment :

I have just made a 'BlockRight()' method for 'Paper' coordinates, based on the class method 'TextRight()' rather than 'Text()', and it produces results as shown below :-

===================================================================================================

I have returned to using 'Paper' coordinates for this output.

As you can see the alignment for integer values is quite acceptable, and the code for the new method is shown below :-

=========================================================

METHOD BlockRight(oPPC AS PaperCoords, aText AS STRING[]) AS VOID

LOCAL i, nLen AS INT

LOCAL cLine AS STRING

LOCAL nDown, nAcross, nAdTextHeight AS Double

LOCAL oPPCthis AS PaperCoords

oPPCthis := oPPC

nDown := oPPCthis:nDown

nAcross := oPPCthis:nAcross

nLen := aText:Length

nAdTextHeight := SELF:TextHeight("PLQjy")

FOR i := 1 UPTO nLen

cLine := aText[i]

SELF:TextRight(oPPCthis, cLine)

//----- move down for next line of block text -----

nDown += nAdTextHeight

oPPCthis := PaperCoords{nDown,nAcross}

NEXT

RETURN

==========================================================

Not a lot to change - isn't it nice to have 'tried and tested' class methods which you can use as 'base' methods for more complex definitions.

Obviously we can make similar small changes to the 'Page' variety to make it perform in a similar fashion.

Before I get too keen, and go and make all the other forms of the 'BlockRight' set of methods, we better bite the bullet and see what we get if we try and display decimal numbers in the same way.

If the result is not acceptable then we need to seek out a better solution, before we makes loads of code changes to the other members of the 'set'.

I have printed the output image differently in our notes, so we can see more clearly just what is happening with the decimal point.

For most output this would be quite acceptable, as long as we have the same number of decimal places displayed - that is digits after the point.

What may be of use to us, is to change our 'BlockRight' method to be based on the column text tool, which was developed for the Report Generator application. This handles decimal strings, as the same time as displaying them nicely after aligning the decimal point.

Let first see how we can make a 'nasty' mess of the method we are using at the moment :-

Try running your eye down the point - note easy I think you will agree. Some may call it 'darn nasty' !

Okay then, lets call on the 'ColTextDecimal()' method we have in the BD class, and which was developed for report column printing. This is quite well 'tried and tested' and has been used in 'Pearls' quite a lot.

Well, not quite as expected, but we have got our points lining up vertically, just as we required.

Obviously, the 'base' class we have used 'off the shelf' is not quite as we require, so we can try and use a newly named method from which we clone the original 'ColTextDecimal()' and make changes to suit our block needs.

Let us call our new method 'BlockTextDecimal()'. Obviously one thing we need to do to this, is to make it ignore array string entries which are not numeric values.

Lets give this a go - at least you will see how to extend a customized document class ;-)

That seems to be a little better, I think you will agree. However, it now looks even better in the display below :-

====================================================================

I think, with a little tidying to our code, we can use this new code for many useful tasks in the future,

Let us look at what we have done, and summarize what we need to do in the future.

The nice tidy print out above has been created using the new method called 'BlockDecimal()'. This is specifically written to display decimal values in a suitable fashion.

The method itself is based on a 'base' class method called 'BlockTextDecimal()' which we have made by copying and then adapting, the existing 'ColTextDecimal()' method which is used for report printing.

To examine the details, lets start at the opposite end, that where we code our sample document :-

=======================================================================

METHOD MyTestDocumentD(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont16Ai, oFont22Ab, oFont22Ai AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(48)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont22Ai := Font{"Arial",22,FontStyle.Italic}

oFont16Ai := Font{"Arial",16,FontStyle.Italic}

//------------------------------------------------

LOCAL aF, aL, aD, aN AS STRING[]

aF := <STRING>{"first","name","=====","Tom","Dick","Harry","Sally","Mary","Jane","====="}

aL := <STRING>{"last","name","=========","Tomlinson","Dixson","Harrison","Shoebill","Mollitoff","Jensen","========="}

aD := <STRING>{"order","date","=========","01/01/2001","02/02/2002","06/06/2006","08/08/2008","03/03/2003","04/04/2004","========="}

oBD:Font := oFont22Ai

oBD:ResetLinesPerPage(40)

LOCAL oPPC1, oPPC2, oPPC3 AS PageCoords

oPPC1 := PageCoords{7,7}

oBD:Block(oPPC1, aF)

oPPC1 := PageCoords{7,23}

oBD:BlockRestrictRHS(oPPC1, aL, 43)

LOCAL cLineHF AS STRING

cLineHF := "======="

aN := <STRING>{"over","paid(£)",cLineHF,"0.6","13.11","200.88","47.8","10.00","2.1",cLineHF}

oPPC1 := PageCoords{7,44}

oBD:BlockDecimal(oPPC1, aN, 2, oBD:PCTextLength(cLineHF) )

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

=======================================================================

As you can see, apart from a few cosmetic changes in the numeric data ( to make it decimal in format, and have a string variable for the underline in the header and footer ), we have only changed the method call to 'BlockDecimal' in place of the earlier 'BlockRight'.

So that the new method is flexible for us in many situations, it needs the developer to specify a couple of extra parameters.

The first extra value supplied is the number of decimal place to display, the second is the width of the column. It is easier to get the BD base class 'PCTextLength()' to find this for us by simply supplying it with the longest line of the column header.

Let us see what this new block methods looks like :-

=====================================================================================

METHOD BlockDecimal(oPGC AS PageCoords, aText AS STRING[], nDec AS INT, nWidth AS Double) AS VOID

LOCAL i, nLen AS INT

LOCAL cLine AS STRING

LOCAL nLine, nAcross AS Double

LOCAL oPGCthis AS PageCoords

oPGCthis := oPGC

nLine := oPGCthis:nLine

nAcross := oPGCthis:nAcross

nLen := aText:Length

FOR i := 1 UPTO nLen

cLine := aText[i]

SELF:BlockTextDecimal(oPGCthis, cLine, nDec, nWidth)

nLine +=1

oPGCthis := PageCoords{nLine,nAcross}

NEXT

SELF:NextLine := (INT)Math.ROUND(nLine + 0.5)

RETURN

========================================================

You will notice that the value for the number of decimals, as well as the width of the column, gets passed into the support class method 'BlockTextDecimal()'.

This base method handles each string item supplied to it in turn, and if it is not a number it prints it as it is, else it converts it into a value of the correct number of decimal places and prints both parts of the decimal after positioning the point correctly.

Let us look in detail now at the supporting base class 'BlockTextDecimal()'.

======================================================================================

METHOD BlockTextDecimal(oPGC AS PageCoords, cText AS STRING, nDec AS INT, nWide AS Double) AS VOID

LOCAL r8Len, nAcross, nLine, r4Half, r8Start AS Double

LOCAL i, iX, iY, INewX, nNewAcross AS INT

LOCAL cRight, cLeft AS STRING

LOCAL aParts AS STRING[]

IF cText:TRIM():Length < 1

RETURN

ENDIF

IF ! (Conversion.VAL(cText) > 0 .or. Conversion.VAL(cText) < 0)

SELF:ColTextLeft(oPGC, cText, nWide)

RETURN

ENDIF

//=============================================================

IF SELF:DecimalAligns:Length < 4

SELF:FillDecimalAlign()

ENDIF

//------------------------------------

nAcross := oPGC:nAcross

nLine := oPGC:nLine

aParts := STRING[]{2}

cText := cText:TRIM()

r8Len := SELF:DecimalAligns[nDec]

r8Start := nAcross+(nWide-r8Len)

aParts := SELF:GetDecimalParts(cText, nDec)

cRight := "."+aParts[2]:TRIM()

oPGC := PageCoords{nLine, r8Start}

SELF:ColTextLeft(oPGC, cRight, nWide)

cLeft := aParts[1]:TRIM()

r8Len := SELF:PCTextLength(cLeft)

r8Start -= r8Len-0.5 //----- al dente -----

oPGC := PageCoords{nLine, r8Start}

SELF:ColTextLeft(oPGC, cLeft, nWide)

RETURN

===============================================================

Don't worry too much about this if you are getting a bit "boggle eyed" :O) Just see how you can use the methods provided to get tidy column printout in your document - very easily.

I only followed the "code extending" exercise through to a conclusion for those readers who are more familiar with Vulcan and dotNet. Yes, I am talking to you Willie, I know you will be keeping an eye on what I am doing - thanks !

Underlining and Centring :

To finish this 'solid' section on placing blocks, lets go back to operations where we don't need to worry about the string content.

For the BlockUnder(), and BlockCenter() methods we apply them to our document element placement, just as easily as the now simple looking 'Block()' method itself.

Let us see some results first :-

================================================================================

Seems easy after the decimal dilemma :-)

What about the code to produce this ? Well here it is :-

//==========================================================

LOCAL aF, aL AS STRING[]

oBD:Font := oFont22Ai

oBD:ResetLinesPerPage(40)

LOCAL oPPC1, oPPC2, oPPC3 AS PaperCoords

aF := <STRING>{"Tom","Dick","Harry","Sally","Mary","Jane"}

aL := <STRING>{"Tomlinson","Dixson","Harrison","Shoebill","Mollitoff","Jensen"}

oPPC1 := PaperCoords{7,7}

oBD:BlockUnder(oPPC1, aF)

oPPC1 := PaperCoords{7,23}

oBD:BlockUnder(oPPC1, aL)

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

=======================================================

Because we are getting our text underlined, I have dropped the extra array elements for the header and footer sections.

The 'BlockUnder()' being used is the 'Paper' overload. So lets us see this code below :-

METHOD BlockUnder(oPPC AS PaperCoords, aText AS STRING[]) AS VOID

LOCAL i, nLen AS INT

LOCAL cLine AS STRING

LOCAL nDown, nAcross, nDiff AS Double

LOCAL oPPCthis AS PaperCoords

oPPCthis := oPPC

nDown := oPPCthis:nDown

nAcross := oPPCthis:nAcross

nDiff := SELF:TextHeight("AQTjyp")

nLen := aText:Length

FOR i := 1 UPTO nLen

cLine := aText[i]

SELF:TextUnder(oPPCthis, cLine)

nDown += nDiff

oPPCthis := PaperCoords{nDown,nAcross}

NEXT

RETURN

And, as you may expect, it is very similar to the 'Paper' version of the 'Block()' method, except that it calls the 'TextUnder()' method in place of the straight 'Text()' method.

By now you will be getting a flavour of all this, and should becoming to realize that if you choose you simple base methods correctly, they are then suitable to build more complex methods, which themselves become base methods for others. All I can say at the moment is ... " Thank goodness for overloading ! " ....

Don't worry, I will make sure that all the correct overloading versions exist for 'Page', 'Paper' and 'Graph' coordinate systems. But now let us try the centring block method set.

==========================================================================

All three columns are treated as string data, and the elements centred between two 'Paper' coordinate points.

If you are going to have a centre, and to use this for string element placing, then you need a start and and end point across the paper.

Let us look at the code for the 'BlockCeter()' method :-

==============================================================================

METHOD BlockCenter(oPPC1 AS PaperCoords, aText AS STRING[], oPPC2 AS PaperCoords) AS VOID

LOCAL i, nLen AS INT

LOCAL cLine AS STRING

LOCAL nDown, nAcross1, nAcross2, nDiff AS Double

LOCAL oPPCthis1, oPPCthis2 AS PaperCoords

oPPCthis1 := oPPC1

oPPCthis2 := oPPC2

nDown := oPPCthis1:nDown

nAcross1 := oPPCthis1:nAcross

nAcross2 := oPPCthis2:nAcross

nDiff := SELF:TextHeight("AQTjyp")

nLen := aText:Length

FOR i := 1 UPTO nLen

cLine := aText[i]

SELF:TextCenterBetween(oPPCthis1, oPPCthis2, cLine)

nDown += nDiff

oPPCthis1 := PaperCoords{nDown,nAcross1}

oPPCthis2 := PaperCoords{nDown,nAcross2}

NEXT

RETURN

======================================================

Nothing surprising here, we need to update two sets of points as we go down the array, because the 'TextCenterBetween()' method requires two points and a string.

Finally for this demanding section, the code to make the document :

=========================================================================

//==========================================================

LOCAL aF, aL, aN AS STRING[]

oBD:Font := oFont22Ai

oBD:ResetLinesPerPage(40)

LOCAL oPPC1, oPPC2 AS PaperCoords

aF := <STRING>{"Tom","Dick","Harry","Sally","Mary","Jane"}

aL := <STRING>{"Tomlinson","Dixson","Harrison","Shoebill","Mollitoff","Jensen"}

aN := <STRING>{"0.6","13.11","200.88","47.8","10.00","2.1"}

oPPC1 := PaperCoords{7,7}

oPPC2 := PaperCoords{7,21}

oBD:BlockCenter(oPPC1, aF, oPPC2)

oPPC1 := PaperCoords{7,23}

oPPC2 := PaperCoords{7,44}

oBD:BlockCenter(oPPC1, aL, oPPC2)

oPPC1 := PaperCoords{7,46}

oPPC2 := PaperCoords{7,60}

oBD:BlockCenter(oPPC1, aN, oPPC2)

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

=====================================================

Once again, nothing surprising :o)

Don't forget that all these demo documents will be available with the code for the experimental 'testbed' app and so you will be able to run it and make them as they appear here.

Good Luck with your use of block methods, and look at the full professional examples later in the notes for places where these have been used to good effect.

Make time to look at the samples of output in 'Appendix H' to see how parts of the 'Company Invoice' was done using blocks.

PHH - Sept. 2008


justifying paragraphs

Navigation: Section 2 - the 'BestDocument' class >

justifying paragraphs

 Return to chapter overview

Or Making Your Ends Tidy :

Before we get into any detail let use look at part of the patient letter we have seen previously.

Each of the two paragraphs shown below have lines of text which are fully justified. That is the start and finish of the ends of the lines in the paragraphs are the same for each of the lines.

It may not look like it to your eyes, however, our new BestDocument methods may make an improvement on this, we will have to see.

If we have a method which takes a whole paragraph or 'chunk' of text (for historic reasons I call it a memo), then if we split this into a number of lines, then we can place the lines on the paper using a 'block' method.

There are two method names of this form of placement, 'MemoPlace()', and 'MemoPlaceFullyJustify()'.

The first form has lines which have no added spaces between the words in the line, and the second has such 'empty padding', in order to make the right hand side move to the required position.

The nice thing about these is that if we can do the first, then the second just needs the lines found by the first, then adjusted with spaces to line up the right hand side with the required point.

Just before we go into the detail, please realize that if you just want to get nice results, all you need know is how to apply these two methods, nothing else below that in complexity. So if you can understand and copy the following few lines, you will know sufficient.

The following lines are the actual live code lines which product the final image at the bottom of this section :-

LOCAL oPPC AS PaperCoords

//-------------- the four code paragraphs ----------------

oPPC := PaperCoords{23,5}

oBD:MemoPlaceFullyJustify(oPPC, SELF:Para1,90)

oPPC := PaperCoords{30,5}

oBD:MemoPlaceFullyJustify(oPPC, SELF:Para2,90)

oPPC := PaperCoords{40,5}

oBD:MemoPlaceFullyJustify(oPPC, SELF:Para3,90)

oPPC := PaperCoords{47,5}

oBD:MemoPlaceFullyJustify(oPPC, SELF:Para4,90)

======================================================

Obviously, the text content of class variables Para1 through Para2 have been specified elsewhere. Also notice that I am using the 'TestBed' experimental tool to develop demo material for these notes. It is easy to use, and the one thing that will get you on quickly to making nice documents.

Okay then, let's see some code :-

METHOD MemoPlace(oPGC AS PageCoords, cText AS STRING, nWide AS Double) AS VOID

LOCAL aText AS STRING[]

aText := Memo2Lines(cText, nWide)

SELF:Block(oPGC, aText)

RETURN

Since we are just splitting the text into lines of a certain specified width (in percentage across value), the 'Page' and 'Paper' overload forms of the method are very similar.

See below for the 'Paper' and 'Graph' coordinate forms :-

METHOD MemoPlace(oPPC AS PaperCoords, cText AS STRING, nWide AS Double) AS VOID

LOCAL aText AS STRING[]

aText := Memo2Lines(cText, nWide)

SELF:Block(oPPC, aText)

RETURN

METHOD MemoPlace(oGC AS GraphCoords, cText AS STRING, nWide AS Double) AS VOID

LOCAL aText AS STRING[]

LOCAL oPPC AS PaperCoords

oPPC := ConvertGraphCoords2PaperCoords(oGC)

aText := Memo2Lines(cText, nWide)

SELF:Block(oPPC, aText)

RETURN

It would therefore seem that the 'crunchy' bit is going to be the method called 'Memo2Lines()' which does the making of the lines.

Remember each line must not be wider than the specified value 'nWide', and that 'cText' is the whole of the memo or paragraph.

Let us study this method below :-

METHOD Memo2Lines(cText AS STRING, nWide AS Double) AS STRING[]

LOCAL aLines, aWords AS STRING[]

LOCAL alLines, alWords AS ArrayList

LOCAL i, nLen AS INT

LOCAL qWords AS Queue

LOCAL cWord, cLine, cNewLine AS STRING

LOCAL nThisWide AS Double

alWords := SELF:WordsList(cText)

qWords := Queue{alWords}

nLen := qWords:Count

alLines := ArrayList{}

cLine := ""

DO WHILE .T.

IF qWords:Count < 1

IF cLine:Length > 0

alLines:Add(cLine)

cLine := ""

ENDIF

EXIT

ENDIF

//----------------------------

cWord := (STRING)qWords:Peek()

IF cLine:Length > 0

cNewLine := cLine + " "+ cWord

ELSE

cNewLine := cWord

ENDIF

nThisWide := SELF:PCTextLength(cNewLine)

IF nThisWide < nWide

cLine := cNewLine

qWords:Dequeue()

LOOP

ENDIF

IF nThisWide = nWide

cLine += cNewLine

qWords:Dequeue()

alLines:Add(cLine)

cLine := ""

LOOP

ENDIF

alLines:Add(cLine)

cLine := ""

ENDDO

aLines := (STRING[])alLines:ToArray(typeof(STRING))

RETURN aLines

This looks interesting, and I will take some time to explain in sufficient details. (You may also need to refer to 'ClickStartVDN' for information and examples of using Queue.)

My overall approach is to find a suitable routine which is clear to write, follow, and maintain - which lets the computer do the work. After all, that is why we have computers.

So step one is simple but very important, split the whole text into 'words' and put these in an array list object.

When I say 'words', I simply mean groups of characters which do not contain a space. So "right?" would be seen as a word under such circumstances.

You will see the point of using an array list is two fold - at the point of declaring it we do not know how many words we will generate, so we need the dynamic facility, and also it is then very easy to make a "Queue" object.

We use a "queue" as we wish to use the words from the front of the text, as we make up our individual lines.

Since the 'WordsList()' method will be reasonably straight forward we will look at that at the end. For now just follow through the Memo2Lines method code.

What are we doing :

To put it simply, we build up a line from nothing, adding a word and one space each time. We peek at the word at the head of the queue, and if this does not make the line too long we remove it from the queue. And then try the next word, and so on. As the queue gets shorter and the line gets longer, there will be a point at which when we peek, the line would exceed the required length.

When this happens, we do not remove the word, and revert back to the last line we used for successful comparison. Now we can save the line and start a new one.

This goes on till we usually finish with a part line, as the last of the 'set' or group.

Of course, each time we add a space and a word we need to find the new length of the text in percentage value, to compare with the width - which is also in percentage terms.

When everything is done, we convert the ArrayList object into a string array to be returned as the return value.

We have seen the 'PCTextLength()' method previously, but since I have just made an important change to code supporting it we ought to take a new look :-

METHOD PCTextLength(cText AS STRING) AS Double

LOCAL fResult, fWidth AS Double

fWidth := SELF:TextLength(cText)

fResult := 99.9999*fWidth/SELF:PageWidth

RETURN fResult

METHOD TextLength(cText AS STRING) AS Double

LOCAL oSizeF AS SizeF

LOCAL fResult, fWidthE, fWidthM AS Double

LOCAL nLen AS INT

nLen := cText:Length

fResult := 0.0

IF nLen < 1

RETURN fResult

ENDIF

LOCAL oDrawFormat AS StringFormat

LOCAL oPoint AS Point

oPoint := Point{100,100} //----- less risky than 0,0 !? -----

oDrawFormat := StringFormat{}

oDrawFormat:FormatFlags := StringFormatFlags.MeasureTrailingSpaces

oSizeF := SELF:g:MeasureString(cText, SELF:Font, oPoint, oDrawFormat)

fResult := oSizeF:Width

RETURN fResult

The 'TextLength()' method has been simplified as I have found a more suitable overload for the Graphics class method 'MeasureString()'. As you can see the clever trick I applied previously is not needed once you realize that the trailing spaces may be measured with cause for such tricks. This feels more like dotNet, thank goodness ;-)

So, it is just the 'WordsList()' method that we need to study :-

METHOD WordsList(cGivenString AS STRING) AS ArrayList

LOCAL iP1, iP2, i, nLen AS INT

LOCAL cLline, cRline, cRest, cText AS STRING

LOCAL aWords, aRest AS STRING[]

LOCAL alWords AS ArrayList

alWords := ArrayList{}

cGivenString := cGivenString:TRIM()

//---------------- check for length -----------------

IF cGivenString:Length < 1

RETURN alWords

ENDIF

//------------------------ check for no spaces ------------------------

IF ! cGivenString:Contains(" ")

alWords:Add(cGivenString)

RETURN alWords

ENDIF

iP1 := cGivenString:IndexOf(" ")

alWords:Add(cGivenString:Substring(0,iP1+1):TRIM())

cRline := cGivenString:Substring(iP1+1):TRIM()

cRest := ""

IF ! cRline:Contains(" ")

alWords:Add(cRline)

ELSE

aRest := SELF:Words(cRline) //--- recursive call to method ---

ENDIF

IF ! aRest = NULL_OBJECT

nLen := aRest:Length

FOR i := 1 UPTO nLen

cText := aRest[i]:TRIM()

alWords:Add(cText)

NEXT

ENDIF

RETURN alWords

You will notice that to fit our 'Memo2Lines' method we need to return an array list object. The rest is some slightly tedious code to go along a string and find the next space, then chop off a segment to trim into a word.

The only interesting bit is where we use recursion to process the rest of the text line after we have chopped off the word at the head of the line.

Justifying the Line Set :

There are a few ways you could go about this, and as long as the one we use works, it does not really matter.

We could have just based a new method 'Memo2LinesSameLength()' on processing the lines from the above methods. All we would require is a new method called 'Line2 SpecifiedLength()' which does "just what it says on the can"!

For some reason, which now I can't remember, I just repeated the above method and changed the appropriate code near the end :-

..................

..................

IF nThisWide = nWide

cLine += cNewLine

qWords:Dequeue()

alLines:Add(Line2SpecifiedLength(cLine, nWide))

cLine := ""

LOOP

ENDIF

alLines:Add(Line2SpecifiedLength(cLine, nWide))

cLine := ""

ENDDO

aLines := (STRING[])alLines:ToArray(typeof(STRING))

RETURN aLines

Notice that the very last line does not get adjusted, whatever.

Either way we do it we need the following method which pads the lines found with extr spaces to make them up to the required length or percentage page width.

Let us study the 'Line2SpecifiedLength()' method (and supporting method 'ConcatLine') as the final part of this section :-

==============================================================

METHOD Line2SpecifiedLength(cLine AS STRING, nWide AS Double) AS STRING

LOCAL cResult, cNewLine AS STRING

LOCAL aGaps AS STRING[]

LOCAL nLen, nPointer, i AS INT

LOCAL nThisWide AS Double

nThisWide := SELF:PCTextLength(cLine)

IF nThisWide >= nWide

cResult := cLine

RETURN cResult

ENDIF

IF ! cLine:Contains(" ")

cResult := cLine

RETURN cResult

ENDIF

LOCAL aWords AS STRING[]

aWords := SELF:Words(cLine)

nLen := aWords:Length

aGaps := STRING[]{nLen-1} //----- telegraph poles and gaps / spaces thing ! -----

FOR i := 1 UPTO nLen-1

aGaps[i] := " "

NEXT

nPointer := 1

DO WHILE .T.

cNewLine := SELF:ConcatLine(aWords, aGaps)

nThisWide := SELF:PCTextLength(cNewLine)

IF nThisWide = nWide

EXIT

ENDIF

IF nThisWide > nWide

EXIT

ENDIF

nPointer +=1

IF nPointer > nLen-1

nPointer := 1

ENDIF

aGaps[nPointer] += " "

ENDDO

cResult := SELF:ConcatLine(aWords, aGaps)

RETURN cResult

============================================================

METHOD ConcatLine(aWords AS STRING[], aGaps AS STRING[]) AS STRING

LOCAL cResult AS STRING

LOCAL i, nLen AS INT

cResult := ""

nLen := aWords:Length

FOR i := 1 UPTO nLen-1

cResult += aWords[i]+aGaps[i]

NEXT

cResult += aWords[nLen]

RETURN cResult

Basically, a simple idea which needs some rather tedious coding and also some care !

The idea is split the line into its words, then make the line again with extra spaces spread in turn along the line. When it is too long go back the the previous one for the solution.

I an sure some of you are itching to try and make the code more efficient - sorry, those days are over for me, clear code easy to understand and maintain, is the "order of the day"!

Remember, Leonardo would go for "beauty", and not efficient unreadable code :O) I side with Leonardo.

I think as a final test we ought to try these methods to see what we actually get - here goes :-

============================================================================================================

Above is the fruit of of labours ! Yes, it certainly looks acceptable to my eyes - AND - better than the earlier example of the patient letter I showed. This was because at the time of writing the earlier notes, that was all I had available to show you. Now we have our newly constructed BestDocument class, things have got a lot better, it would seem.

If you want a suggestion as to why this may be a better sample than earlier, it is because we are now using the 'MeasureString' method with the correct overload instead of fighting the code myself. We are now using the "include trailing spaces" version.

PHH - Sept. 2008


more advanced methods

Navigation: Section 2 - the 'BestDocument' class >

more advanced methods

 Return to chapter overview

Text Displayed at 90 Degrees :

We have already seen the 'TextUp()' method in action in a previous sub-section, where we go a nice blue "Upstanding Text" shown at the right hand side of the page.

Let us now focus on this method, and make a further method which will be useful for some printed formats, particularly graphical ones.

First, let us see some results :-

===========================

===========================

From the image it may be inferred that we now have two versions of the 'TextUp' method, one to print from the point supplied, and one to print up to it.

Believe it or not, it is quite straight forward to make this further method, as we will see below, but first we need to see just how the first form of works, the one we have called "TextUp()", and used previously.

Let us take a look at the code for this, realising that the text string will be placed down the paper from the point specified. We really ought to have named this method 'TextUpFrom' :-

METHOD TextUp(oPPC AS PaperCoords, cText AS STRING) AS VOID

LOCAL oPoint AS Point

oPoint := ConvertPaperCoords2Point(oPPC)

LOCAL oDrawFormat AS StringFormat

oDrawFormat := StringFormat{}

oDrawFormat:FormatFlags := StringFormatFlags.DirectionVertical

SELF:g:DrawString(cText , SELF:Font , SELF:Brush , oPoint, oDrawFormat )

RETURN

It may surprise you to see how easy this is to do. All that is different to what has been done before, is we have used an overload of the 'DrawString' method in the Graphics class, which accepts a 'StringFormatFlags' enumeration.

If you wish to know more, then look up in the SDK the information on StringFormat & StringFormatFlags.

So to summarize, the text string is placed vertically, and starts at the point supplied, and goes down the paper from that point. So in the above example, this is the "Upstanding Text - From" placement.

Let us now see the slightly more complex version which places text up to the point that is specified :-

METHOD TextUpTo(oPPC AS PaperCoords, cText AS STRING) AS VOID

LOCAL oPoint AS Point

LOCAL oSizeF AS SizeF

LOCAL nWidth, nHeight AS Double

LOCAL nX, nY AS INT

oPoint := ConvertPaperCoords2Point(oPPC)

nX := oPoint:X

nY := oPoint:Y

LOCAL oDrawFormat AS StringFormat

oDrawFormat := StringFormat{}

oDrawFormat:FormatFlags := StringFormatFlags.DirectionVertical

oSizeF := SELF:g:MeasureString(cText, SELF:Font, oPoint, oDrawFormat)

nY -= (INT)oSizeF:Height

oPoint := Point{nX, nY}

SELF:g:DrawString(cText , SELF:Font , SELF:Brush , oPoint, oDrawFormat )

RETURN

All we have had to do is call the 'MeasureString' method of the Graphics class, with the overload which accepts a StringFormatFlags enumeration.

The rectangle which is returned, as a SizeF object, still has width and height which applies to the page in the normal fashion. So it is the height that we need to subtract from the 'down position' of the originally specified point. We can then specify a new point using this readjusted position. Remember the Graphics methods work at a canvas coordinate level, so appropriate conversions are required when necessary.

PHH - Sept. 2008


methods for report columns

Navigation: Section 2 - the 'BestDocument' class >

methods for report columns

 Return to chapter overview

Before we get into detail on this interesting topic, let us take a moment to see what can be produced using the few report column methods :-

===================================================================================================================

If you study the image above closely, you will see that we have column areas which are left justified, right justified, and also centred. In fact there is another type which is to handle decimal values, and this aligns the decimal point for the required number of decimal places.

For you interest, and knowledge, the individual column detail for each line, is placed separately. So for a full line to be printed, nine part lines are placed on the paper, one for each column.

It is the 'ReportGenerator' class code which determines if the column content is to be printed at all, and if so what colour and style etc. etc. is to be used. The actual placement of the 'cell content' is determined by the 'BestDocument' (BD) method we use in our code.

So, which methods do we have, lets list them below :-

ColTextLeft()

ColTextCenter()

ColTextRight()

ColTextDecimal()

Not many, I think you will agree, but they do a nice job.

Because the work I did with ReportGenerator was done using the 'Page' metaphor, and the pages determined by numbers of lines on a page - and hence the pre-calculated pages number - I will use 'Page' coordinates for all of the column methods we follow through here. There will also be a complete set based on 'Paper' coordinates, but I will leave you to do the 'Graph' ones fro yourself, as I doubt they will be used very often, or even at all :-)

Just before we start to examine code, please do realise that column printing is all about handling the text within the left and right bounds of the column. We do not wish the contents to 'spill over' from one cell into the adjoining column. So we need to control this aspect of the text placing quite firmly.

Okay then, let's see the code for the 'ColTextLeft()' method :-

METHOD ColTextLeft(oPGC AS PageCoords, cText AS STRING, nWide AS Double) AS VOID

LOCAL nLine, nAcross AS Double

LOCAL oPoint AS Point

LOCAL nY, nX AS Double

LOCAL iY, iX AS INT

LOCAL cLine AS STRING

IF cText:TRIM():Length < 1

RETURN

ENDIF

//----- chop line if too long ! -----

cLine := SELF:TextRestrictRHS(cText, nWide)

nLine := oPGC:nLine

nAcross := oPGC:nAcross

IF nLine < 1 .or. nLine > SELF:LinesPerPage

//--------- warn box ---------

nLine := 1

ENDIF

IF nAcross < 0 .or. nAcross > 100

//---------- warn box ---------

nAcross := 0.0

ENDIF

oPoint := ConvertPageCoords2Point(oPGC)

SELF:g:DrawString(cLine , SELF:Font , SELF:Brush , oPoint)

RETURN

You will notice that just about the first thing we do is check to see if the supplied text string is too long for the column width specified in the third parameter. If it is, then the text string has sufficient removed (chopped) from its right hand side to allow it to fit in between the column bounds. We have seen the method 'TextRestrictRHS()' in an earlier section.

Once we have a string which will fit between the cell bounds, we need to make some checks that the point supplied (in 'Page' coordinates) is acceptable. Then the string is just drawn on the graphic surface at a point in canvas coordinates which is found by converting that supplied in 'Page' coordinates.

OK, that was easy, now how does the 'ColTextRight()' method differ from this ? Let's look at the code below :-

=======================================================================

METHOD ColTextRight(oPGC AS PageCoords, cText AS STRING, nWide AS Double) AS VOID

LOCAL r8Len, nAcross, nLine, r8Start AS Double

LOCAL cLine AS STRING

nAcross := oPGC:nAcross

nLine := oPGC:nLine

cText := cText:TRIM()

r8Len := SELF:PCTextLength(cText)

IF r8Len > nWide

//----- chop line if too long ! -----

cLine := SELF:Line2SpecifiedLength(cText, nWide)

r8Len := SELF:PCTextLength(cLine) //----- ought to be nWide ! -----

ELSE

cLine := cText

ENDIF

r8Start := nAcross + nWide - r8Len

oPGC := PageCoords{nLine, r8Start}

SELF:ColTextLeft(oPGC, cLine, nWide)

RETURN

==============================================

Firstly, we find the length of the supplied text string, and if it is longer than the width supplied as the third parameter, chop it down to size. So any string will end up as long as the width, or shorter.

From this string length we can then calculate where the starting position will be, for when we use the standard ColTextLeft() method to place our "possibly shortened" string.

Alright then, now let's look at the method to centre the text in the cell. View the code below :-

=================================================================================

METHOD ColTextCenter(oPGC AS PageCoords, cText AS STRING, nWide AS Double)AS VOID

LOCAL r8Len, nAcross, nLine, r8Half, r8Start, r8Remain AS Double

cText := cText:TRIM()

r8Len := SELF:PCTextLength(cText)

nAcross := oPGC:nAcross

nLine := oPGC:nLine

r8Remain := nWide - r8Len

IF r8Remain <= 0

SELF:ColTextLeft(oPGC, cText, nWide)

RETURN

ENDIF

//-----------------------------------------

r8Half := r8Remain*0.5

r8Start := nAcross+r8Half

oPGC := PageCoords{nLine, r8Start}

SELF:ColTextLeft(oPGC, cText, nWide)

RETURN

============================================

Put in words of plain English, if the text is too long (or exactly the width) we find simply pass it to the 'ColTextLeft()' method where it takes care of the 'chopping process', and what is left will fill the complete cell anyhow.

If the text is less than the cell width, we find half of the length of the extra space remaining in the cell - when the string takes up the rest of the room - and adjust the starting position, which we pass into the 'ColTextLeft()' method.

Okay, this is all a bit fiddly, but done once, and tested thoroughly, then all we need ever do is use the 'top level' methods in the appropriate manner.

Finally, if you are ready for it, lets brace ourselves and look at the code to handle aligning decimal values in columns. It is very similar to what we have seen for 'block' :-

====================================================================================

METHOD ColTextDecimal(oPGC AS PageCoords, cText AS STRING, nDec AS INT, nWide AS Double) AS VOID

LOCAL r8Len, nAcross, nLine, r4Half, r8Start AS Double

LOCAL i, iX, iY, INewX, nNewAcross AS INT

LOCAL cRight, cLeft AS STRING

LOCAL aParts AS STRING[]

IF SELF:DecimalAligns:Length < 4

SELF:FillDecimalAlign()

ENDIF

//------------------------------------

nAcross := oPGC:nAcross

nLine := oPGC:nLine

aParts := STRING[]{2}

cText := cText:TRIM()

r8Len := SELF:DecimalAligns[nDec]

r8Start := nAcross+(nWide-r8Len)

aParts := SELF:GetDecimalParts(cText, nDec)

cRight := "."+aParts[2]:TRIM()

oPGC := PageCoords{nLine, r8Start}

SELF:ColTextLeft(oPGC, cRight, nWide)

cLeft := aParts[1]:TRIM()

r8Len := SELF:PCTextLength(cLeft)

r8Start -= r8Len-0.5 //----- al dente -----

oPGC := PageCoords{nLine, r8Start}

SELF:ColTextLeft(oPGC, cLeft, nWide)

RETURN

=====================================================

As with the 'block' variety of this method, we need to know how many decimal places we are aligning to. Then find from using our little initialized string array, the width of the 'fattest' right hand side of the decimal point.

The character zero '0' is actually the widest of the digits, and this is why we use the required number to calculate the 'off-set' of the decimal point from the right hand side of the cell.

The short method below places the zeros in the string array for 'off-set' calculation.

METHOD FillDecimalAlign() AS VOID

LOCAL i AS INT

LOCAL cRight , cText AS STRING

LOCAL r8Len AS Double

SELF:DecimalAligns := Double[]{4}

cRight := ""

FOR i := 1 UPTO 4

cRight += "0"

cText := "."+cRight

r8Len := SELF:PCTextLength(cText)

SELF:DecimalAligns[i] := r8Len

NEXT

RETURN

Notice that a point is also part of the string with the following zeros.

The reason we do a check at the very start of the 'ColTextDecimal()' method, is so that we don't create this array each time the method is used - if it already is in existence. This saves time, but, we need to have the values available, so the check needs to be carried out.

So we find out the distance from the point to the end of the last of the zeros in the 'DecimalAligns' array and this then allows us to

PHH - Sept. 2008


aligning decimal numbers

Navigation: Section 2 - the 'BestDocument' class >

aligning decimal numbers

 Return to chapter overview

Have you noticed that this has been left till the last sub-section of this chapter ?

Can you guess why ??

Yep! You've got it - not easy, fiddly, lots of arithmetic - BUT - totally necessary, if we are to use proportionally spaced fonts and have columns of values.

So, are you ready for the challenge ?

Not as Bad as You May Think :

In fact, like all the rest of this 'stuff', it is not as bad as you think once you get your head into it. As long as we design our methods 'top down' and the keep our code clear, we will be able to do it fine.

If you remember, the 'block' placement below uses the methods designed initially for use with column placement with the ReportGenerator class. After checking the alignment of the decimal points in this image, we will step through what is required to achieve the results :-

Let us start at the top most method, that used by the developer / coder, and work our way down the chain of 'base' methods needed to support it.

Here is the calling code which started it all off - in the earlier sub-section :-

LOCAL cLineHF AS STRING

cLineHF := "======="

aN := <STRING>{"over","paid(£)",cLineHF,"0.6","13.11","200.88","47.8","10.00","2.1",cLineHF}

oPPC1 := PageCoords{7,44}

oBD:BlockDecimal(oPPC1, aN, 2, oBD:PCTextLength(cLineHF) )

.......................................

.......................................

So the method we as developers needs to know how to use, is the 'BlockDecimal()' method.

Simply, this specifies the point at which the block is to be placed, supplies the text array, the number of decimal places to be shown in each line, and the width of the block itself. In this example I have taken the easy way out and supplied a header/footer line which I know is going to be the longest in the list. You could always find the longest one yourself, with the PCTextLength() method.

Okay then, let's see the 'BlockDecimal' method again, and this time work our way through it.

METHOD BlockDecimal(oPGC AS PageCoords, aText AS STRING[], nDec AS INT, nWidth AS Double) AS VOID

LOCAL i, nLen AS INT

LOCAL cLine AS STRING

LOCAL nLine, nAcross AS Double

LOCAL oPGCthis AS PageCoords

oPGCthis := oPGC

nLine := oPGCthis:nLine

nAcross := oPGCthis:nAcross

nLen := aText:Length

FOR i := 1 UPTO nLen

cLine := aText[i]

SELF:BlockTextDecimal(oPGCthis, cLine, nDec, nWidth)

nLine +=1

oPGCthis := PageCoords{nLine,nAcross}

NEXT

SELF:NextLine := (INT)Math.ROUND(nLine + 0.5)

RETURN

Fine, nothing too clever here.

As each 'decimal' text section is taken from the array it is handed to the 'BlockTextDecimal()' method to be printed on its line after it has be suitably processed. Obviously, the process will involve finding the position of the decimal point and placing it appropriately.

Let us look at the 'BlockTextDecimal()' method now :-

======================================================================================

METHOD BlockTextDecimal(oPGC AS PageCoords, cText AS STRING, nDec AS INT, nWide AS Double) AS VOID

LOCAL r8Len, nAcross, nLine, r4Half, r8Start AS Double

LOCAL i, iX, iY, INewX, nNewAcross AS INT

LOCAL cRight, cLeft AS STRING

LOCAL aParts AS STRING[]

IF cText:TRIM():Length < 1

RETURN

ENDIF

IF ! (Conversion.VAL(cText) > 0 .or. Conversion.VAL(cText) < 0)

SELF:ColTextLeft(oPGC, cText, nWide)

RETURN

ENDIF

//=============================================================

IF SELF:DecimalAligns:Length < 4

SELF:FillDecimalAlign()

ENDIF

//------------------------------------

nAcross := oPGC:nAcross

nLine := oPGC:nLine

aParts := STRING[]{2}

cText := cText:TRIM()

r8Len := SELF:DecimalAligns[nDec]

r8Start := nAcross+(nWide-r8Len)

aParts := SELF:GetDecimalParts(cText, nDec)

cRight := "."+aParts[2]:TRIM()

oPGC := PageCoords{nLine, r8Start}

SELF:ColTextLeft(oPGC, cRight, nWide)

cLeft := aParts[1]:TRIM()

r8Len := SELF:PCTextLength(cLeft)

r8Start -= r8Len-0.5 //----- al dente -----

oPGC := PageCoords{nLine, r8Start}

SELF:ColTextLeft(oPGC, cLeft, nWide)

RETURN

=================================================================

Yep! you've got it - this is the bit we need to understand ;-)

Let me first outline the simple process, as the idea is not 'rocket science'.

The portion of text is taken and split into a left and a right part, one from either side of the decimal point.

PHH - Sept. 2008


Section 3 - Print Preview - 'tools'

Navigation: »No topics above this level«

Section 3 - Print Preview - 'tools'

 Return to chapter overview

Step Forward :

Above all else, this is the one 'thing' that has made printing with dotNet as good as it is.

I have known experienced developers which in the past world of WinAPI32, have dedicated a huge effort into trying to create a 'preview' screen of sorts. Their efforts, although successful, resulted in a screen viewer which was far from perfect, and seemed to always have some area of distortion.

In dotNet we have been provided with two 'tools' - which in one sense is just one !

What I mean by this is that there is a PrintPreviewControl (PPC) class, and this is what is used on the form which pops up on screen when a PrintPreviewDialog (PPD) screen is used.

The icons on the menu bar and a couple of other controls, give access to the property settings of the PP control. If we put a PPC on our own form the we have at least as much flexibility in how we change the settings as when we use the dialog screen.

The sort of settings are :- the start page, the degree of zoom, the number of pages displayed - both columns and rows. And obviously whether we make a physical printout.

At this point I have to be honest and tell you that I have much more experience using the dialog screen from my code than the fixed form control. However, this has been simply because I did not know how to get the control to work correctly. Now I know how to set it up correctly, and respond to other form controls and events, I am sure I will use it a lot more.

I will go away now and adapt the 'testbed' app tool of ours, so it now functions with a fixed PPC. So when we click on the test doc buttons we get the correct information displayed in the control itself. Maybe I will add a option to send output to one or the other (the other being the PPD), or even both !

So it would seem that the art of using, and getting the most from these 'tools', is to know how to handle the PrintPage event correctly. If you do this and structure your code right in the event handler method, so it handles more pages correctly, then you are onto something good.

Before we continue let us see the preview dialog 'tool' as it is with 'blank' output :-

My feelings are that the it is as complex as it needs to be, for the tasks required of it.

Above you can see the simple menu bar, with the zoom menu in focus. The preview screen shown is that of the 'floating' GUI - there is also a Forms 2.0 control which like all other such controls is part of a form, with a fixed position. This control will only be enlargeable if the form is allowed to expand, and if the anchor properties are set so that it will enlarge on form expansion.

We will see how to set all the relevant properties, for both formats of the preview 'tool'. See the later sub-section for setting properties.

The PrintPreview control :

Having seen a blank dialog screen you ought to now see the control itself, on a live form, but without any information being sent to it as yet :

The fixed control on the right is the PPC and I have commented out the code line which sets the event handler, in order to show you it blank.

It look much better with contents on display so maybe we should show that below :-

I reality, there are four pages on view is the scroll bars are used to good effect. But we will come to all that in good time.

So to summarize, the PPC is really the viewing tool, and the PPD is just a 'wrapper' form of this - which can be used when there is not a suitable GUI screen on which to place a control.

For most of us, most of the time, we will have a choice of both I suspect.

It would appear obvious to me now, that the algorithm which Microsoft have created, or acquired, to convert the printer's canvas coordinates into suitable screen coordinates (based on the screen resolution in use) is a very good one. To see this software at work, as the control is resized, is quite a marvel. Give it a go, do it slowly, and see all that is going on.

Before you go on to study the individual sub-sections please realize that this tool seems to have far fewer deficiencies associated with it than other preview systems I have used extensively.

I refer namely to PDF995 virtual printer driver (creating PDF files) working with Adobe Reader as the display tool. This was good, and a whole step up, BUT, I think the PPC /PPD is far better.

I will draw a comparison later in this section.

Good Previewing.

PHH - Sept. 2008.


general - about previewing

Navigation: Section 3 - Print Preview - 'tools' >

general - about previewing

 Return to chapter overview

Tips and Ideas :

Before we get into examining the details of the PrintPreviewDialog (PPD) class we need to talk a little on the general topic of previewing on screen, output which is to be printed.

Over the years there have been various attempts, and approaches, to 'seeing' your printed document without actually printing it.

Judging by the slow rate of development, and some of the results on the way, it would appear to be either a difficult, or a tricky task to master.

I even remember, the first add-on to an 'early' Word which gave you a simple start to previewing - we thought it marvelous. No doubt it had lots of shortcomings but it was MS's attempt to address the problem. Before then we did everything blind. Well, we did not do a great deal, so I suppose we coped.

I would simply say this, about where we are today with previewing :-

Take what tools are available and use them extensively, they do make life easier. However, be well aware of some pitfalls, and develop a strategy which covers all the possible problems.

Many of the problems that 'used to be' have gone away - BUT - be careful.

At the end of the day, what comes out of the printer is the most important thing. Therefore exact placement of printed elements on the paper have to be checked on the real output, at least from time to time. PPD viewing is just an aid. Luckily we can say (and I have checked just to be sure) that what is printed from the menu bar icon in the PPD tool, is exactly the same as what we print directly to the printer. This in itself is great news. One less thing to get wrong.

Secondly, as far as Microsoft is concerned, their PPD tool seems to do a pretty reasonable job of representing on the screen, what actually comes out of the printer.

I am talking here of alignment of one element with another, and representing lines and fonts closely to the physically printed form.

If you look at the invoices sample in 'Appendix H' you will see that there are a fair mix of types of font family, size and style etc. etc. Yet the only thing I noticed to be slightly amiss was that at one point in the development I had the grid a little too far to the right of the paper. It showed fine on the PPD, and was not that close to the paper edge, and yet on my quite new colour laser printer the final vertical grid line was missing.

So the message is, don't always believe what you see is what will come out on the paper, particularly if it towards the boundaries. Do a real test for these cases.

There seems to be a problem with getting an exact 100% correspondence in the pixel / coordinate system, when translating from printer canvas coordinates to screen coordinates.

I think modern screen resolutions, and the Vista operating system, have drastically improved things - I can't talk about XP any more as I do not now use it for printing.

Other Preview Systems :

Moving away from Microsoft directly, you need to be aware of other potential problems in previewing and printing. If you select a 'virtual' printer for use with the PrintDocument class (parent of BestDocument class) then the output is a PDF file. I still do this in my Vista 64 system quite a lot, and produce all my Pearls images that way. I use PDF995, but there are many others, 'Cute' being another.

However, when my 'IT Manager' prints documents directly from Adobe Reader with the resultant PDF file open, we get 'creep' at the page bottom. The physical placement of text is slightly too high. He needs to be well aware of this as he is often checking out my work with new letters and printing directly onto the pre printed stationery. Of course the footings on such stationery do not move, so it looks misaligned. When of course it is perfectly aligned.

Viewing the whole document under such circumstances is not a problem however, as all the elements move together it would appear - hence my own term 'creep'. So view to get a good idea / flavour of the output, but only believe the printed output. Be extra careful with pre printed stationery.

Bare in mind that things are improving all the time with these third party tools, so printer drivers (just complex software) and screen viewers, will keep getting better. So we need to keep revisiting the possible problems with the tools that we use, to see if some have 'disappeared'.

Fonts and Printer Drivers :

I think problems in the past with some font families, and even certain sizes of font families, showing is bizarre fashions, are largely a think of the past. But, I have seen this happen, some fonts display in incorrect positions relative to other elements.

Your first port of call if you get any bizarre output to screen or paper, is to change the printer driver. So may people think that because a printer driver comes from a well known company, that it works 100%. That can't be farther from the truth, BUT, things seem to be getting a lot better.

What I would say is - don't mess with old printers and their drivers, all you can get is grief, when in fact your code - written in the latest and best dotNet fashion - actually works. Printers are cheap enough and so get a modern one, and preferably one where you know the printer driver works for what you want to do.

Another related problem is where clients get hold of third party fonts (from somewhere) and then have grief getting letters and documents printed out in the said font - sometimes it may just be for a certain font size. The software to draw the glyphs is a complex mathematical equation based code, and can easily be deficient, particularly if from a less well known source. The equation used to calculate character output changes with the font size, so parts of the font software may be OK and others incorrect.

My thoughts are, make life easy for yourself, stick with the Windows font families supplied in Vista - there are plenty these days. Only go outside of these if you have a specialized need. These sorts of problem happened more in the past when Windows had fewer supplied font families, and it was common practice to 'collect' and use third party fonts.

Getting some Idea :

If you wish to get some idea of only some of the difficulties in producing a PPD tool, get such a tool on screen with suitable content, and gradually extend and enlarge the screen. Watch how the letters change size and form as the resizing takes place. Remember all the glyphs (letter outlines) are being calculated with equations based on series summation, and as the size changes the equations (approximations) need to change too.

I am no expert, by a country mile on such things, but knowing a little of what happens under the covers, means I am very surprised and impressed as to what can be achieved. And, that dotNet makes it so easy and accessible to the 'standard guy'.

Its just a pity the basics aren't easily available to follow.

Never, mind the problems, you have little excuse now that you have these notes, for not doing some great printing, to screen or paper.

PHH - Sept. 2008


what is this ?

Navigation: Section 3 - Print Preview - 'tools' >

what is this ?

 Return to chapter overview

The PrintPreviewDialog class allows the developer to very easily display on screen a resizeable form which can display the output from the developer's own event handler method(s).

When the 'Print()' method is called from the PrintDocument class (remember BestDocument inherits from PrintDocument), or the 'ShowDialog()' method of the instatiated PrintPreviewDialog class, the event handler is execited which is currently associated with the 'PrintPage' event of the PrintDocument class.

The line of code to 'set' the event handler is similar to :-

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentQ()}

This example shown above 'sets' the 'MyTestDocumentQ' method to be run when the PrintPage event triggers.

Do realise however that it is possible to have multiple event handlers associated with the event, as shown below :-

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentA()}

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentB()}

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentC()}

This means that for the one fresh page is put into focus on the graphics surface to be addressed by the event handlers.

When the handlers have finished their tasks, the system either stops printing, or moves onto the next page.

The control of whether there are any more pages is done by a property setting, as below :-

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

The 'HasMorePages' property has to be set once in one of the event handlers to allow the system after the end of the PrintPage event, to know whether to raise another such event or not.

The developer has to take firm control of the page printing process in his/her code, and the setting of this property in an event handler is part of the printing 'art'.

I believe that if you forget to set 'HasMorePages' to true, then you don't get any more. Likewise, if your code has set this property to true, but fails to reset it to false at some point, you empty the paper tray in the printer.

Let us look at the small section of code which handles the printing in our testbed application :-

===============================================

METHOD ShowDocument() AS VOID

//------ use selected printer ------

oBD := BestDocument{"TestShow",SELF:cPrinterName}

IF SELF:Landscape

oBD:Landscape := TRUE

oBD:ResetOrientation("L")

ELSE

oBD:Landscape := FALSE

oBD:ResetOrientation("P")

ENDIF

oBD:SetPageBoundaries()

SELF:SetAppropriateHandler()

SELF:lFirstPrint := TRUE

IF !SELF:Preview

oBD:Print()

ELSE

SELF:oPreviewDlg:=PrintPreviewDialog{}

SELF:oPreviewDlg:Document := SELF:oBD

SELF:oPreviewDlg:ShowDialog()

ENDIF

SELF:UnSetAppropriateHandler()

oBD:Dispose()

oBD := NULL_OBJECT

RETURN

==============================================

There is not a lot to this, thankfully.

Notice that you need to set the orientation on the BestDocument (inherits from PrintDocument) class, before you cause the PrintPage event to trigger.

You can't make the page outline on the dialog screen jump back and forth from portrait to landscape by changing this afterwards - so be warned.

On most of my forms, I like to have an logic option which controls whether to show a preview, or just go straight ahead with the physical printing. Here this is 'SELF:Preview' and I set it by default to true.

Now the good news and the bad news, all at once.

The good news is that the print icon in the menu bar of the tool 'triggers' the same code to print from the Preview screen, as we have supplied to print directly to the printer. This means the physical placing on the paper itself is identical, no matter which way you do it. (This is not the case when you print directly from Adobe Reader, we will return to this.)

So where is the bad news I hear you ask - well it is simply, that if you have had you event handling code run through once for the preview pages, it will be required to be run a second time when you click the printer icon.

This has caught me out a few times, I have to admit.

What happens, and what can be done to rectify it ?

Well, first of all lets make the problem occur :-

What upsets the developer is that all the pages have been shown correctly in the preview screen, and nothing seems to be wrong there. All pages come out as required, and the last page too is both there and complete.

So what can be wrong ?

Well, it is at the start of the second printing call to the handler(s) code where the problem is.

Let us take a look at the code at the end of my event handler to print the mail merging sheets from Section 15 later in the notes :-

//=============== END OF test code ==================

SELF:nRowPointer +=1

IF SELF:nRowPointer > SELF:NumberOfRows

e:HasMorePages := FALSE

//SELF:nRowPointer := 1

ELSE

e:HasMorePages := TRUE

ENDIF

RETURN

The code as shown, causes the error above - we are trying to access a row of data which does not exist. Quite simple to understand.

However, if the nRowPointer line is uncommented, the code works a second time without throwing an exception.

Put simply, the developer has to write his/her code such that the page control part of it resets any pointer type controlling variables, such that whole handler code would run a subsequent time, after the completion of all pages.

By resetting my page pointer to one, this solves the problem.

What throws you are first about this is that a direct print to the printer works - as this is also the first time the handlers run through the code.

Okay then having seen some live mail merge output on preview screen, as well as your first exception, lets move on to the next sub-section to see how to use the too.

PHH - Sept. 2008.


how is it best used ?

Navigation: Section 3 - Print Preview - 'tools' >

how is it best used ?

 Return to chapter overview

Let me first say that although the PrintPreviewDialog (PPD) class seems both easy to use and quite flexible too, it was not easy to find out the necessary details to make it work as shown in these notes.

I was appalled at just how little information there was in the numbers of the books I referred to, and then how unclear the information was. It was almost as if the writers had never actually done much printing.

Well these notes are different - most of the 'working' information has been gained by experience, and 'doing'. Learning the hard way is what I would call it. Although some of it was fun.

To be honest, I have found out even more by writing these notes.

Often we are doing a few new things at once when we attempt to print, and the edges get blurred as to what problem is coming from where ;-)

Let us look at page control - that is get one or more pages to print.

This aspect has nothing to do with the dialog screen, but it is inextricably linked with it.

If you print to a printer and never use a PPD screen, you still need to firmly control your page output. If you don't use the PPD you can get away without handling the re-setting of the page pointer variables, but I would not recommend this.

If you do use the PPD screen you don't need to display the whole document, you can provide only a selected range, or even a single page.

Remember, that when you show a PPD, each and every required page is printed, so the code for all such pages is run through completely.

If the task in hand is to see what your page layout looks like, then printing a single page is often sufficient. So why sit there while another twenty, or even two hundred are printed, when you really only wish to see one or two ?

Let us look at a good example of this, from the Report Generator tool described in 'Section 10' of these notes :-

The bottom left hand section of the Report Generator (RG) tool is shown above :-

To save me development time I included the facility to select one, a range, or all of the pages in the report set, and then launch the PPD. My default setting was a single page, and that was also page one.

You could actually allow individual page selection if that suited the task in hand, so pages 13,39,69 & 177 could be selected, and then the PPD would show just four, but their bottom right hand corner would show something like 'Page 177 of 256' or similar.

Do realize that when you use the printer icon in the menu bar you will physically print the same number of pages, so in this case, as well as showing four pages number 13,39,69 & 177, they will also be the ones printed.

How is it Used :

This comes from experience. If it is not required, then don't use it. Remember, everything get printed twice.

I like to use it to be a real part of the design process, on a 'try it and see' basis. This again is good when designing a columned report with the RG tool.

However, there is another and important role for the PPD and that is in the office, to save paper, and time.

Quite often users need to see some but not all of an internal report, in an extreme case just the final page !

The PPD can be very flexible for this sort of use, allowing the user to flick back and forth, read what they what, and then cancel. No paper or extra time is wasted.

In fact you could design some GUIs so that once the user sees the page numbers of the pages they require in the PPD, they exit and select these as a print 'set'. You could then allow PPD viewing once again, and print from there when the user is satisfied they have what they require.

As I said before, it is all very flexible - its just up to you.

Floating or Otherwise :

It is possible to have a fixed control on a form of the PPD type, see below for an early example I tried :-

The control on the left is the fixed form of the PPD.

We will look at how to place and use such a control later, but for now it is probably useful to you if you know I tend to use the 'floating' form of the PPD almost exclusively.

It is very flexible in its use as an enlargeable floating screen. Most times I need to enlarge to almost full screen to see what I wish to see. You may be different.

Lets see how this looks at design time :-

We will see how the properties of the control can allow for some flexibility in the use of this and the floating form, and to see these you need to the study sub-section two later in the chapter.

The form control is available from the toolbox in both VIDE and Visual Studio :-

Notice the last entry in the Extras tab above. And here are the related style settings from the properties box :-

Obviously when we want a floating version of the tool (PPD) we do everything in code. Again we will look at these details in the slightly later sub-section.

For now, it is sufficient to say that the PPD is your friend, it is there to make the sometimes difficult task of printing, just that bit easier.

I suggest you give it a go and get used to using it a lot - remember, it is what us printing pioneers have been waiting for for years :-)

PHH - Sept. 2008


multiple openings

Navigation: Section 3 - Print Preview - 'tools' >

multiple openings

 Return to chapter overview

Because the PrintPreviewDialog class when instantiated, produces a form on screen, it can be open the usual to ways using 'Show()' or 'ShowDialog()'

If we use 'Show()' we get a non modal form which allows us to continue to have access to the controls on the form from which it was launched.

Look at the output sample below, there may be times when you wish to have multiple dialog screens showing different pages, at the same moment in time.

In the example below I set things up so that the target page could first be selected with push button navigation (the crude form of output comes in the pale yellow rich text control (top left) and then this focus page can be should correctly in the PPD when the suitable push button is clicked. Sosimply I could see pages 17, 45 and 177 for example - side by side for comparison.

There are other ways to do this I accept, but the coding is simple and there may be benefits to three separate large screens, rather than three pages in one control screen.

You are very lucky to have such choices, so be glad

Obviously, for such an arrangement I have had to make the event handler just shown one page (no more) and this is the page which is brought into focus by the clicking of the navigation controls on th main form.

It is always a good idea to structure your information to be printed, such that the pagination has been done before the point at which you wish to print. This way it is easy for the event handler to just go straight to the required information and print it, then stop there. Much quicker this way.

PHH - Sept. 2008.


care with physical spacing !

Navigation: Section 3 - Print Preview - 'tools' >

care with physical spacing !

 Return to chapter overview

Using Screen Preview Facilities :

Before we go any further with our printing and document preparation, let me warn you of an effect caused by the use of the PrintPreview dialog which can easily rear up and bite you if you are not aware of it.

The above image shows the bottom of a letter which has been printed directly onto header note paper. Pre printed stationery may be it correct name.

What I found, while creating and editing the whole bunch of letters I was doing for my client at the time, was that the placement of my printed text 'moved'.

When I say 'moved' I mean that if I printed it directly to the stationery it was in the correct place, whereas if I printed from Adobe Reader, or PrintPreview, the lower page text seemed to 'creep' up the page.

As well as the 'Yours sincerely,' and the signature etc., we also print the opening hours detail, as well as the internal reference. These last two are the ones we see 'move'.

Note the very bottom line - which includes the VAT number - is part of the pre printed stationery.

I will see if I can reproduce the effect later for your benefit. For now just take on board that the PrintPreview dialog is great for many things, and makes our life a lot easier, but don't then make life more difficult again by forgetting this tip about bottom page creep. If you print directly from the Preview screen - onto pre printed stationery of this style - and do all your lining up this way - you will find that when you print directly to the printer (missing out the PrintPreview dialog) then the text will be too low.

This is all part of the fiddly side of printing which will always be with us I fear !

Real Spatial Problem - ClickStart :

PHH - Sept. 2008


setting properties by code

Navigation: Section 3 - Print Preview - 'tools' >

setting properties by code

 Return to chapter overview

Let me say from the outset that printing is difficult enough to try and explain to those new to it, without getting too deeply into more than the 'necessary' setting for PPD & PPC.

The way I see it is that there are a few settings we need to know how to make and change, in order to make the tools work for us. I have just spent quite a time looking through all the available properties and methods of both classes - it can get overwhelming. Lets just see what we need to do to make life easier for ourselves.

As you will see as things unfurl below, the necessary properties can be seen in the code which I have found works for me. All will be revealed in due course - keep reading.

PrintPreviewDialog class :

Remember, the dialog screen form of the PrintPreview tool, just has a PrintPreviewControl on it and has properties set and changes by the few simple form controls provided.

The PPD can't have more functionality than the PPC and doesn't function differently. The image representation must be the same, as it is the same view control being used.

However, you can use a PPD very conveniently when you do not have a suitabe GUI of your own on which to display the control itself.

If we study the properties and settings available to the PPC we will see what we can do.

We also need to see the difference in the code to successfully fill and display, using PPC as opposed to PPD. This will be shown later in this sub-section.

PrintPreviewControl class :

To start us off on our quest to find suitable, and 'simple to use' Vulcan code - to make PPC work for us - let us see what was used to make the output in introduction to this section.

EXPORT op4Complex AS System.Windows.Forms.PrintPreviewControl

..................................

..................................

SELF:op4Complex := System.Windows.Forms.PrintPreviewControl{}

SELF:op4Complex:Name := "p4Complex"

SELF:op4Complex:Location := System.Drawing.Point{ 408 , 0 }

SELF:op4Complex:Size := System.Drawing.Size{ 424 , 440 }

SELF:op4Complex:TabIndex := 9

SELF:op4Complex:Anchor := System.Windows.Forms.AnchorStyles.Top + System.Windows.Forms.AnchorStyles.Bottom +

System.Windows.Forms.AnchorStyles.LEFT + System.Windows.Forms.AnchorStyles.RIGHT

SELF:Controls:Add(SELF:op4Complex)

..................................

..................................

METHOD FormComplexLoad(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:InitializePrintPreviewControl()

RETURN

METHOD InitializePrintPreviewControl() AS VOID

SELF:nPageCounter := 3

SELF:op4Complex:Document := SELF:oBasicDoc

SELF:op4Complex:Document:DocumentName := "My Complex report :"

SELF:op4Complex:Zoom := 0.55

SELF:op4Complex:StartPage := 3

SELF:op4Complex:Columns := 2

SELF:op4Complex:Rows := 2

//SELF:op4Complex:AutoZoom := TRUE

SELF:op4Complex:UseAntiAlias := TRUE

SELF:oBasicDoc:PrintPage += PrintPageEventHandler{SELF , @PrintComplexPageHandler()}

RETURN

The form on which the PPC is placed is called 'FormComplex', and so the method to action methods required at the time the form is loaded is called "FormComplexLoad" and this is triggered by the form's load event.

Notice that all it does here is cause the 'InitializePrintPreviesControl' to be run.

This is old code I have got to work just yesterday, so I am not sure why I called the PPC the name 'p4Complex' but just realise that 'op4Complex' is the PPC object.

Now we can see that the PPC object has some properties set in order to generate the output as seen in the introduction. Lets go though these.

First and foremost, we have to supply a PrintDocument object, and that is done in the first line of the settings code section. The PPC object 'Document' property is set to the available PrintDocument object - in this case SEL:oBasicDoc.

The value shown for the zoom property was sufficient to be able to see enough detail of what was being displayed. The 'catch twenty two' here is the clearer you make it to see the detail, then the more of the four pages are hidden from immediate view, and need scroll bar action to bring them into view. This is always a dilemma for the developer when using preview screens - the thumb nail type images are great - but you can often get much information from viewing them. Make them clearer, and you then have large areas of screen output to handle. You can't have it both ways !

If I set the vales of both columns and rows to '1' then we get only one page in the display pane, and that is the start page, number three in this case.

However, if I set the value of both columns and rows to 3, and then the previously commented out line of 'AutoZoom := TRUE' we get the following display on form load :-

Very interesting, nine pages in all, but all we can detect is enough detail to see that each page is different and so I am not fiddling things by having the same page nine times over.

However, I must confess I have had such outputs in the past :-)

On the start page number issue, what the control calls the start page, and what we the user calls the start page will usually differ. The control just means that when it runs the event handler code, if you specify the start page to be 3, it will ignore the first two produced, and so the first one to be shown will be number 6. If I had set the class variable 'nPageCounter' to '1' then the correspondence will be 'one to one' - and the first page displayed would have been number one also.

Summary on the page number issue - the PPC pages are relative to production, the user code numbers are usually real page numbers which appear at the bottom right of all my pages I produce. This is one thing the coder / developer needs to take control over.

Lets now see what happens if comment out the 'AutoZoom' and bring back the Zoom factor line :-

SELF:op4Complex:Zoom := 0.55

SELF:op4Complex:StartPage := 3

SELF:op4Complex:Columns := 3

SELF:op4Complex:Rows := 3

//SELF:op4Complex:AutoZoom := TRUE

SELF:op4Complex:UseAntiAlias := TRUE

Notice the detail is readable and yet a factor of 0.55 is not so great are to make the hidden area 'huge', and hard work on the scrolling.

Also notice that the page is number 6, and the position of the scroll bars will confirm that this is the top left most page of a block of nine. So we started our pages at our number three, and the PPC ignored the first two, so we see six as the first.

Keeping everything else the same as above, and only changing the zoom factor from 0.55 to 1.3 produces the following :-

I think you will agree, it would be very hard work to move around the nine pages on offer in the pane, and get anything meaningful from the exercise. I will leave it to you to find out just how much zoom you can apply, by increasing the factor.

The last code setting we have used, but not so far explained is the 'UseAntiAlis' property. This is simply to do with how the characters are drawn by the operating system. The SDK does not have much more to say than this, so I am not sure as to what is going on. I can't tell on my Vista64 system if there is any difference in quality or speed for what I am displaying. Because of the difference in Vista with video output, and using the GPU, things may be different - you have been warned ;-)

The Dialog Tool :

Okay then, where does this leave us with the dialog screen form of this 'tool'? Well, if you look at the top of the dialog screen, you will see that there are some obvious ways of changing the properties that we have focused on so far. Maybe 5.00 was the maximum of the zoom ? However, notice there is no way of setting three columns and three rows using the icons available. No doubt you could add your own.

The start page is what is set by the combo box, and there is a print button too.

What this does suggest to me however, is that we have so far with our fixed PPC not looked into interactively changing the properties. By this I mean that with the 3 by 3 pages displaying, what if we had form controls of our own to change the row / column layout, and the start page. What would we need to do to get the control to redisplay - if anything ?

We would also need to provide a 'print button' for physical printing, if required.

I will see what I can add to the 'TestBed' app for you to see, you and copy.

One thing I thought I used to know, but can't rediscover, was how to disable the print option in the menu bar. I will need to talk to a 'man who can' - as they say in the adverts :-)

PHH - Sept. 2008


TestBed tool - display formats

Navigation: Section 3 - Print Preview - 'tools' >

TestBed tool - display formats

 Return to chapter overview

Okay then, some success at least, the TestBed app has been adapted to show output to both a fixed PPC object and also create floating PPD objects.

However, I have a problem I will have to ask Chris Pyrgas to solve for me, and that is I can't seem to get the events to trigger the fixed control to show anything but the first image which was loaded initially. Still, this sort of problem keeps you in your place, and makes you realize that this is an awful lot still to learn.

Wish me luck, as getting the PPC to work correctly has been a very longstanding problem for me.

There has been a time period between this image above and what I am writing now. In fact it is the day after :-)

The good news is I can get the tool do do what I want, the bad news is I had to fumble through the problem myself.

The code is probably getting a bit complex to expect a busy man to solve difficult problems in a few blinks of an eye. However, when you are made aware you are 'on your own' you have a greater resolve about the way you approach things. So I got a solution - BUT - it is not the prettiest thing I ever did see ! But, hey! ... it works !!

The new code, and interactive approach to handling the preview navigation in the fixed control, can be seen in 'Section - 6' on Experimentation.

PHH - Sept. 2008


Section 4 - Printing to a single page

Navigation: »No topics above this level«

Section 4 - Printing to a single page

 Return to chapter overview

It is one thing to assemble a page of graphical elements using the methods of the BD class, however it takes a little more work to code things correctly so that information either appears in the Print Preview screen, or on a piece of paper which 'emerges' from the printer itself.

Let us now focus on the part of the code which cause previewing, and hard copy to be produced.

Once we master how to get one page we can look at how to make many pages. The process is simple in essence, but can be tricky to get to work correctly.

What needs to be done is this :-

- Select a printer

- set the Orientation

- calculate the Boundaries

- choose an event Handler

- switch to Preview or Print mode

Let us look at the necessary code for this :-

============================================================================

This 'ShowDocument()' method is basically the one supplied with these notes - which allows you to display your own text pages and documents, after you have built the code as you think it ought to be !

Quite often for a "test rig" the developer uses a hard wired code line to use his/her local printer.

As you can see I had been using mu OKI, but have changed the BD code to accept an empty string. If an empty string is provided then the the default printer will be used.

Let us now look at the 'constructor' of the BD class to see how this is done :-

=============================================================

As you will notice, it is quite easy to find and apply the default printer. You may also like to check out 'Appendix A' to see how to easily get to any installed printer on your system.

Back to our ShowDocument() - for a test code module it is sufficient to be able to hard code the orientation. Here I use landscape as it fits better into being displayed in these notes - no other reason!

After the page boundaries have been found we have a line of code like :-

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocument()}

This is setting the event handler on the 'PrintPage' event which is in the PrintDocument class from which we inherited when we defined the BestDocument class.

The 'PrintPage' event seems to be triggered by a call to the 'Print()' method of the base class, or a call to the 'ShowDialog()' method of the PrintPreviewDialog class.

So simply, when we try tp print or preview (from our code) the PrintPage event will now run the method specified in this line of code - that is the 'MyTestDocument' method.

This is the method you will construct when you have test pages to view - we will see how to do that later.

Now, you may be wondering why I have the oBD:Print() line commented out. Simply, it was because I am in test code and did not wish to generate any hard copy from my printer.

When we move from test mode code, to real polished user code, we will have a GUI switch to allow direct printing, or previewing first. Then the code can be used more or less as it is here.

Notice that once the code section is past the point where printing is to occur, it is good practice to remove, or dissociate, the event handler from the PagePrint event. This means no matter what happens afterwards we will not get anything printed at the printer, or shown on the preview screen, which ought not to be there.

This may well be a good time to mention that the PagePrint event will keep triggering if :-

e:HasMorePages := TRUE

This is why we need to set this at the end of our method which is 'set' as the event handler. As long as this is true you will get the PagePrint event firing.

Yes, you can cause a lot of paper to come out of the printer VERY easily if you get you code logic wrong :0) A good reason to comment out the oBD:Print() call while you are testing.

Try to use the Preview mode while you test and perfect your printing method, it saves paper generally, as well as protecting you from possible disasters.

However, do remember what was said in "Section 3 - care with physical spacing ! " so switch back to physical printing for pre-printed stationery checking, as a final phase to you document creation.

Finally, you need to realise that the code section we have been studying is basically the same for whatever printing you wish to do in the future. All that will happen if you go to more complex page layout handling in your document, is that the event handler method (here called MyTestDocument), will get more complex, and may also switch to sub methods depending on what is being done.

When you think you have mastered single page printing and previewing move on to the next sub-section to see how to do multiple pages.

However, I suspect you need to go off and do a lot of work with single pages, as you learn how to use and apply the 'BestDocument' class methods for placing graphical elements on the page.

PHH - Sept. 2008.


Section 5 - Printing multiple pages

Navigation: »No topics above this level«

Section 5 - Printing multiple pages

 Return to chapter overview

The good news about printing multiple pages is that it is not difficult.

Yes, it took me a while to extract form some C# 'tomes' I was looking at - some 18 months ago - but that was because I now guess the authors had not actually done much printing. They seemed to make it seem obscure and difficult / impossible - and that is just not the case.

Actually, to be perfectly honest with you, the hardest part is in creating your own code to get the page layout of the elements correct.

Usually, if you want many pages, then your document is more complex. The more complex the document, the harder it is to write the page layout control code that you need. This is nothing to do with the 'multiple page' issue itself.

Lets late a look at a small section of some of my working code from the 'Report Generator' (RG) tool :-

METHOD ShowDocument() AS VOID

//------ find default printer ------

MessageBox.Show(SELF:oOwner:cFocusPrinter, "Focus Printer :")

oBD := BestDocument{"TestShow", SELF:oOwner:cFocusPrinter}

IF SELF:oOwner:Landscape

oBD:ResetOrientation("L")

ELSE

oBD:ResetOrientation("P")

ENDIF

oBD:SetPageBoundaries()

LOCAL nLPP AS INT

nLPP := SELF:oOwner:LinesOnPage+8

oBD:ResetLinesPerPage(nLPP)

oBD:PrintPage += PrintPageEventHandler{SELF , @PreviewRG()}

IF !SELF:Preview

SELF:nPageCount := 1

//oBD:Print()

ELSE

SELF:nPageCount := 1

SELF:oPreviewDlg:=PrintPreviewDialog{}

SELF:oPreviewDlg:Document := SELF:oBD

SELF:oPreviewDlg:ShowDialog()

ENDIF

oBD:PrintPage -= PrintPageEventHandler{SELF , @PreviewRG()}

oBD:Dispose()

oBD := NULL_OBJECT

RETURN

Lets us go through this a step at a time and see what needs to be done to get many pages, and how it is different from before - when we got just one.

The first half of this is very straight forward, it simply picks up the name of the printer which has been set or selected at the GUI - this is the 'focus' printer. Then the orientation is applied as set in the GUI, so the boundaries can be calculated.

After taking the page line GUI setting, and adding some extras for the headings and footings - which the class needs for its own use - the lines per page (LPP) is set for use.

Now we come to the printing and multiple page part of the code.

You ought to be able to predict that the method to be used, for the actual printing process, is called 'PreviewRG'. This is the vent handler which will be triggered by the 'PrintPage' event.

Remember we choose from our code to either use the physical 'Print()' method of the base class (PrintDocument) or the 'ShowDialog()' method of an instantiated PrintPreviewDialog object. The 'repetitive' part for the multiple pages is caused by the 'PrintPage' event being re-triggered due to the 'HasMorePages' property being set to true.

We need to have a controlling variable, which is scoped such that it is accessible to our code both inside our event handler method and also the class as a whole.

If we have a class variable to control whether we need more pages, then we can initialize this before we use the event handler the first time, and change its value with the event handler itself. In our example above I use 'SELF:nPageCount' to do this job.

Look at the code below to see how we handle the need for more pages - or not - depending on the data we wish to print :-

.....................................

.....................................

LOCAL nLen, nPageMax, nRowMax, nThisPage AS INT

.....................................

.....................................

nThisPage := SELF:aPages[SELF:nPageCount]

nPageMax := SELF:nPagesToDo

...........................................

...........................................

SELF:nPageCount +=1

IF SELF:nPageCount <= nPageMax

e:HasMorePages := TRUE

ELSE

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

SELF:nPageCount := 1

ENDIF

RETURN

So from within our event handling method we are accessing the class variable 'nPageCount', updating it as we finish each page. When we have run out of pages we set the event property 'HasMorePages' to false, and this then does not cause the PrintPage event to fire again.

Below is some other code which handles the printing of the footer region of the report pages, to produce such as :-

...........................

nLine := SELF:oOwner:LinesOnPage+6

oBrush := SolidBrush{SELF:oOwner:oColorFooter}

oBD:Brush := oBrush

oBD:Pen := Pen{oBrush, 1.0}

...........................

...........................

nLine +=1

oPGC1 := PageCoords{nLine, 98}

oPGC2 := PageCoords{nLine, 2}

cText := "Page : "+nThisPage:ToString()+" of "+SELF:oOwner:NumberOfPages:ToString()

oBD:TextRight(oPGC1, cText)

cText := SELF:oOwner:Footer1

oBD:Text(oPGC2, cText)

//=================== End of Footer =====================

...........................

Some of the other parts of the method (the full code is included in the RG tool) are related to finding and printing the actual page from the report 'set' - even if we are doing just a sub-range of what is available. If we are printing pages 13 to 15, of a whole report set of 26, then there are just three pages to print so nPageMax will be only 3, where as the number of pages will be 26.

Remember, it is totally up to you to control the multiple page throws. Printing reports with a fixed structure is relatively easy for page control, as we just need to count rows and pages. However, if we have a free text report, where text 'chunks' are being read from a text file by a Streamer Reader object, we will have to trigger the HasMorePages setting by whether there are any more lines to read.

It all depends on what you are doing - sounds as if I am trying not to be helpful by saying that, but it is true !

PHH - Sept. 2008


Section 6 - Experimentation, test app.

Navigation: »No topics above this level«

Section 6 - Experimentation, test app.

 Return to chapter overview

In order to make it easy for you to learn how to use the 'BestDocument' (BD) class quickly, I have provided a 'Test Class'.

This test class - called "TestBD" - simply acts as a wrapper so that all you need to is make methods called 'MyTestDocument()' and you will be previewing and printing in no time at all.

The 'TestBD' class is based on a simple GUI and allows for a few buttons and controls to be used to change a few settings and launch your documents.

I will set it up to look like this below :-

The grid and image testing code is in one method which is been shown when button A is clicked. All the user has to do is modify the code in the blank / stub method and then they can simply and easily test their efforts.

I will keep the form simple, but we better allow users to choose a printer :-)

The rest is up to you. But first, here are some printer, and it starts with the default printer - all code as in other sections - just follow it !

Okay then, we have a simple to use test 'wrapper' environment - how do we go about using this to learn how to create documents ?

Lets look at the overall class called 'NewTextDocument', which is used directly by the form above. Clicking on the push buttons A through E will instantiate an appropriate oNTD object.

==================================================================================================================================

MyTestDocumentE is the only one with any code between the marked user section. These few lines just draw a grid and that will work on any system the user happens to be running. The other stubs, A through D, are blank in this section - ready for you to put your test code there.

The output when push button 'E' is clicked, is shown below :-

Now lets us see the simple code to react to the push buttons :-

==================================================================================================

So really, the classes 'NewTestDocument' and 'fmTestBD' together, act as the testbed 'wrapper', and all the user needs to do is put his/her code in the marked sections - of methods A through E in the methods shown above in the NewTestDocument class.

I have actually used this testbed application to generate samples for some of the earlier sections. It does work OK :-)

Finally, you need to see the simple Start() function which pulls this altogether, and will allow you to have a good play around.

#USING System.Windows.Forms

#USING System.Drawing

[STAThreadAttribute];

FUNCTION Start( asCmdLine AS STRING[] ) AS INT

LOCAL nExitCode AS INT

LOCAL oForm AS fmReportsAndPrinting

LOCAL oTBD AS fmTestBD

Application.EnableVisualStyles()

Application.DoEvents()

oTBD := fmTestBD{}

Application.Run(oTBD)

RETURN nExitCode

Good luck with you trials and tests - remember to have fun with printing, now that the 'canvas' is well buried.

PHH - Sept. 2008.


TestBed Two - better !?

Navigation: Section 6 - Experimentation, test app. >

TestBed Two - better !?

 Return to chapter overview

Okay folks, since I wrote the previous section things have moved on a pace.

I have made some changes to the form so that we now have a fixed control which does Print Previewing.

At the click of a button the contents of the fixed control changes to whatever the button is 'wired' to and this is also placed in a floating dialog screen.

See the image below to find out what the new look is like :-

You may be interested to see that when the 'MM' button is pressed, we have the code setting the rows and columns to 2 and 3 for the fixed control, whereas the floating dialog screen has the defaults in use.

I think there is enough detail showing for you to see that we have page one (with Rev at the top) in the dialog screen and one through three showing on the fixed control.

See the image below for these details :-

Don't think that this is the final format of the tool for your experimental use, things are bound to change. It would appear appropriate to have a simple tool like that discussed earlier, and a more complex tool too.

I think it would be useful to try and place our own navigation controls on the TestBed form so we could change rows and columns, and much more. We shall see how this develops.

Progress, and Functionality :

Well, developed it has. It has been relatively easy to add the extra form controls to allow us as least as much flexibility in our settings as there is in the floating dialog screen. In fact we can set more rows and columns than it is possible to do with the PPD menu bar.

Also we can choose to supply or not supply a way to make a physical print out of the information - in other words we could code things such as this was just a viewing facility (i.e. print to screen).

As thing stand at the moment I can't figure how to disable the print option in PPD. We will however, there has to be an easy way :-)

Notice in the image above, our experimental tool has nice zoom controls (which are integrated, auto with value settings) - the image is with zoom at 1.4 applied already.

PHH - Sept. 2008


S6 Working Code

Navigation: Section 6 - Experimentation, test app. >

S6 Working Code

 Return to chapter overview

#USING System.Windows.Forms

#USING System.Drawing

[STAThreadAttribute];

FUNCTION Start( asCmdLine AS STRING[] ) AS INT

LOCAL nExitCode AS INT

LOCAL oForm AS fmReportsAndPrinting

LOCAL oTBD AS fmTestBD

Application.EnableVisualStyles()

Application.DoEvents()

oTBD := fmTestBD{}

Application.Run(oTBD)

RETURN nExitCode

//==========================================================

CLASS NewTestDocument

EXPORT oBD AS BestDocument

PROTECT Preview AS LOGIC

PROTECT oPreviewDlg AS PrintPreviewDialog

PROTECT oGraphics AS Graphics

PROTECT cDoc AS STRING

CONSTRUCTOR()

SUPER()

SELF:Initialize()

cDoc := "A"

RETURN

CONSTRUCTOR(cWhich AS STRING)

SUPER()

SELF:Initialize()

cDoc := cWhich

RETURN

METHOD Initialize() AS VOID

SELF:Preview := TRUE

RETURN

METHOD ShowDocument() AS VOID

//------ find default printer ------

oBD := BestDocument{"TestShow","OKI C5600"}

//oBD := BestDocument{"TestShow",""}

oBD:LinesPerPage := 66

oBD:SetPageBoundaries()

SELF:SetAppropriateHandler()

//oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentA()}

//Messagebox.Show(oBD:PrintableAreaDetails())

IF !SELF:Preview

oBD:Print()

ELSE

SELF:oPreviewDlg:=PrintPreviewDialog{}

SELF:oPreviewDlg:Document := SELF:oBD

SELF:oPreviewDlg:ShowDialog()

ENDIF

SELF:UnSetAppropriateHandler()

//oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentA()}

oBD:Dispose()

oBD := NULL_OBJECT

RETURN

METHOD SetAppropriateHandler() AS VOID

DO CASE

CASE SELF:cDoc = "A"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentA()}

CASE SELF:cDoc = "B"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentB()}

CASE SELF:cDoc = "C"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentC()}

CASE SELF:cDoc = "D"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentD()}

CASE SELF:cDoc = "E"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentE()}

END CASE

RETURN

METHOD UnSetAppropriateHandler() AS VOID

DO CASE

CASE SELF:cDoc = "A"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentA()}

CASE SELF:cDoc = "B"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentB()}

CASE SELF:cDoc = "C"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentC()}

CASE SELF:cDoc = "D"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentD()}

CASE SELF:cDoc = "E"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentE()}

END CASE

RETURN

METHOD MyTestDocumentA(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentB(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentC(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentD(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentE(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oBD:SetGraphics(e:Graphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//---------------------------------------------------

//============ place your test code here ============

LOCAL oPGC1, oPGC2 AS PageCoords

LOCAL N, M AS INT

N := 9

M := 11

oPGC1 := PageCoords{10,10}

oBD:GridComplex(oPGC1,<Double>{3,5,7,9,11,9,7,5,3},<Double>{1,3,5,7,5,3,2,3,5,7,5,3,1})

//=============== end of test code ==================

//------------ keep this for a single page -----------

e:HasMorePages := FALSE

RETURN

END CLASS

//==========================================================

CLASS fmTestBD INHERIT System.Windows.Forms.Form

PROTECT olbPrintersInstalled AS System.Windows.Forms.Label

PROTECT ocbPrinters AS System.Windows.Forms.ComboBox

PROTECT opbTestDocE AS System.Windows.Forms.Button

PROTECT opbTestDocD AS System.Windows.Forms.Button

PROTECT opbTestDocC AS System.Windows.Forms.Button

PROTECT opbTestDocB AS System.Windows.Forms.Button

PROTECT opbTestDocA AS System.Windows.Forms.Button

PROTECT ogbModeOfPrinting AS System.Windows.Forms.GroupBox

PROTECT orbDirectToPrinter AS System.Windows.Forms.RadioButton

PROTECT orbViewPreviewDialog AS System.Windows.Forms.RadioButton

PROTECT opbOK AS System.Windows.Forms.Button

// User code starts here (DO NOT remove this line) ##USER##

CONSTRUCTOR()

SUPER()

SELF:InitializeForm()

SELF:Initialize()

RETURN

METHOD Initialize() AS VOID

SELF:orbViewPreviewDialog:Checked := TRUE

SELF: FindPrinters()

RETURN

METHOD FindPrinters() AS VOID

//================ find and set printer(s) =================

LOCAL cDefaultPrinter AS STRING

LOCAL oPS AS PrinterSettings

oPS := PrinterSettings{}

cDefaultPrinter := oPS:PrinterName

LOCAL acPrinters AS STRING[]

LOCAL oPSSC AS PrinterSettings.StringCollection

oPSSC := PrinterSettings.InstalledPrinters

acPrinters := STRING[]{oPSSC:Count}

oPSSC:CopyTo(acPrinters,0)

SELF:ocbPrinters:Items:Clear()

SELF:ocbPrinters:Items:AddRange(acPrinters)

SELF:ocbPrinters:SelectedItem := cDefaultPrinter

// ==========================================================

RETURN

METHOD InitializeForm() AS VOID

// IDE generated code (please DO NOT modify)

SELF:Name := "fmTestBD"

SELF:SuspendLayout()

SELF:Location := System.Drawing.Point{100,100}

SELF:ClientSize := System.Drawing.Size{410,334}

SELF:Text := "Test Best Document class :"

SELF:olbPrintersInstalled := System.Windows.Forms.Label{}

SELF:olbPrintersInstalled:Name := "lbPrintersInstalled"

SELF:olbPrintersInstalled:Location := System.Drawing.Point{ 192 , 152 }

SELF:olbPrintersInstalled:Size := System.Drawing.Size{ 144 , 23 }

SELF:olbPrintersInstalled:Text := "Printers Installed :"

SELF:olbPrintersInstalled:TabIndex := 8

SELF:Controls:Add(SELF:olbPrintersInstalled)

SELF:ocbPrinters := System.Windows.Forms.ComboBox{}

SELF:ocbPrinters:Name := "cbPrinters"

SELF:ocbPrinters:Location := System.Drawing.Point{ 192 , 176 }

SELF:ocbPrinters:Size := System.Drawing.Size{ 200 , 21 }

SELF:ocbPrinters:TabIndex := 7

SELF:Controls:Add(SELF:ocbPrinters)

SELF:opbTestDocE := System.Windows.Forms.Button{}

SELF:opbTestDocE:Name := "pbTestDocE"

SELF:opbTestDocE:Location := System.Drawing.Point{ 16 , 176 }

SELF:opbTestDocE:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocE:Text := "Test Doc E"

SELF:opbTestDocE:Click += System.EventHandler{ SELF , @pbTestDocEClick() }

SELF:opbTestDocE:TabIndex := 6

SELF:Controls:Add(SELF:opbTestDocE)

SELF:opbTestDocD := System.Windows.Forms.Button{}

SELF:opbTestDocD:Name := "pbTestDocD"

SELF:opbTestDocD:Location := System.Drawing.Point{ 16 , 136 }

SELF:opbTestDocD:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocD:Text := "TestDoc D"

SELF:opbTestDocD:Click += System.EventHandler{ SELF , @pbTestDocDClick() }

SELF:opbTestDocD:TabIndex := 5

SELF:Controls:Add(SELF:opbTestDocD)

SELF:opbTestDocC := System.Windows.Forms.Button{}

SELF:opbTestDocC:Name := "pbTestDocC"

SELF:opbTestDocC:Location := System.Drawing.Point{ 16 , 96 }

SELF:opbTestDocC:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocC:Text := "Test Doc C"

SELF:opbTestDocC:Click += System.EventHandler{ SELF , @pbTestDocCClick() }

SELF:opbTestDocC:TabIndex := 4

SELF:Controls:Add(SELF:opbTestDocC)

SELF:opbTestDocB := System.Windows.Forms.Button{}

SELF:opbTestDocB:Name := "pbTestDocB"

SELF:opbTestDocB:Location := System.Drawing.Point{ 16 , 56 }

SELF:opbTestDocB:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocB:Text := "Test Doc B"

SELF:opbTestDocB:Click += System.EventHandler{ SELF , @pbTestDocBClick() }

SELF:opbTestDocB:TabIndex := 3

SELF:Controls:Add(SELF:opbTestDocB)

SELF:opbTestDocA := System.Windows.Forms.Button{}

SELF:opbTestDocA:Name := "pbTestDocA"

SELF:opbTestDocA:Location := System.Drawing.Point{ 16 , 16 }

SELF:opbTestDocA:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocA:Text := "Test Doc A"

SELF:opbTestDocA:Click += System.EventHandler{ SELF , @pbTestDocAClick() }

SELF:opbTestDocA:TabIndex := 2

SELF:Controls:Add(SELF:opbTestDocA)

SELF:ogbModeOfPrinting := System.Windows.Forms.GroupBox{}

SELF:ogbModeOfPrinting:Name := "gbModeOfPrinting"

SELF:ogbModeOfPrinting:SuspendLayout()

SELF:ogbModeOfPrinting:Location := System.Drawing.Point{ 192 , 16 }

SELF:ogbModeOfPrinting:Size := System.Drawing.Size{ 200 , 100 }

SELF:ogbModeOfPrinting:Text := "Mode of Printing :"

SELF:ogbModeOfPrinting:TabIndex := 1

SELF:Controls:Add(SELF:ogbModeOfPrinting)

SELF:orbDirectToPrinter := System.Windows.Forms.RadioButton{}

SELF:orbDirectToPrinter:Name := "rbDirectToPrinter"

SELF:orbDirectToPrinter:Location := System.Drawing.Point{ 32 , 24 }

SELF:orbDirectToPrinter:Size := System.Drawing.Size{ 136 , 24 }

SELF:orbDirectToPrinter:Text := "Direct to Printer :"

SELF:orbDirectToPrinter:TabIndex := 0

SELF:ogbModeOfPrinting:Controls:Add(SELF:orbDirectToPrinter)

SELF:orbViewPreviewDialog := System.Windows.Forms.RadioButton{}

SELF:orbViewPreviewDialog:Name := "rbViewPreviewDialog"

SELF:orbViewPreviewDialog:Location := System.Drawing.Point{ 32 , 56 }

SELF:orbViewPreviewDialog:Size := System.Drawing.Size{ 144 , 24 }

SELF:orbViewPreviewDialog:Text := "View Preview Dialog :"

SELF:orbViewPreviewDialog:TabIndex := 1

SELF:ogbModeOfPrinting:Controls:Add(SELF:orbViewPreviewDialog)

SELF:opbOK := System.Windows.Forms.Button{}

SELF:opbOK:Name := "pbOK"

SELF:opbOK:Location := System.Drawing.Point{ 328 , 304 }

SELF:opbOK:Size := System.Drawing.Size{ 75 , 23 }

SELF:opbOK:Text := "OK"

SELF:opbOK:Click += System.EventHandler{ SELF , @pbOKClick() }

SELF:opbOK:TabIndex := 0

SELF:Controls:Add(SELF:opbOK)

SELF:ogbModeOfPrinting:ResumeLayout()

SELF:ResumeLayout()

RETURN

METHOD pbTestDocAClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("A")

RETURN

METHOD DoDisplayDoc(cWhich AS STRING) AS VOID

LOCAL oNTDA AS NewTestDocument

oNTDA := NewTestDocument{cWhich}

oNTDA:ShowDocument()

oNTDA := NULL_OBJECT

RETURN

METHOD pbTestDocDClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("D")

RETURN

METHOD pbTestDocCClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("C")

RETURN

METHOD pbTestDocBClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("B")

RETURN

METHOD pbTestDocEClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("E")

RETURN

METHOD pbOKClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:Close()

RETURN

END CLASS


Section 7 - Lines and Grids

Navigation: »No topics above this level«

Section 7 - Lines and Grids

 Return to chapter overview

Let us have a break from text information, numbers and dates, and have an interesting look at lines and grids.

A per usual, I got my interest in these from AJB (Alwyn) when I followed his chapter 15 in the VO 'Black Bible' - see introduction for more.

This topic is also a good one for OOP as we seem to base all our more complex methods on simpler ones, which use even simpler ones and so one.

I also think it is good for top down design, as I believe in the approach a great deal. So when we come to see and use the complex grid methods, I want you to remember that these were designed and specified before the supporting methods, eventually getting down to the line drawing method itself.

Okay then, since the methods now exist, and are designed and tested, let us go the other way and see what we have, and how to use them, from the simplest line methods. Fits of all lets list the method names we will be looking at :-

DrawLine()

VLineSet()

HLineSet()

GridRegularNbyM()

GridComplex()

If you run your eye down the BestDocument class - the Vulcan code - you may notice a couple of other places the word 'line' appears in a method name. This is simply because it refers to a 'line of text', say when splitting up a memo or paragraph. There are only about three places this happens and they are pretty clear as to what the difference is.

We first saw the code for the four DrawLine() methods in Section 1 - on overlaoding.

Let us now examine one overload in particular, as we will use it to draw some interesting 'things'.

METHOD DrawLine(oGC1 AS GraphCoords, oGC2 AS GraphCoords, nPenWidth AS INT) AS VOID

LOCAL oPPC1, oPPC2 AS PaperCoords

oPPC1 := ConvertGraphCoords2PaperCoords(oGC1)

oPPC2 := ConvertGraphCoords2PaperCoords(oGC2)

LOCAL oPoint1, oPoint2 AS Point

oPoint1 := ConvertPaperCoords2Point(oPPC1)

oPoint2 := ConvertPaperCoords2Point(oPPC2)

SELF:DrawLine(oPoint1, oPoint2, nPenWidth)

RETURN

Notice that this overload takes two points and a pen width. The points are in the same coordinate system, which is the 'graph' type. That is 0,0 the origin is bottom left of the paper.

Probably the next thing you notice is that the method uses another 'DrawLine' overload itself. The signature of this overload is similar but the two points are GDI+ points, so are in 'canvas' coordinates. Look at this method below, as it uses yet another 'DrawLine' overload.

METHOD DrawLine(oPoint1 AS Point, oPoint2 AS Point, nPenWidth AS INT) AS VOID

LOCAL oThisPen AS Pen

oThisPen := SELF:Pen

oThisPen:Width := nPenWidth

SELF:g:DrawLine(oThisPen, oPoint1, oPoint2)

RETURN

This time the overload belongs to the parent, or base, class which is the Graphics class of dotNet. There is not a problem with all these as they have a different signature - one reason I put the two points first in the BD methods. All four of the BD methods have a similar parameter 'format' - point, point, pen.

The only thing that is different in each is the coordinate system of the points.

Fine, lets us now get something to show for our efforts, let us make a two axes X & Y as if for a graph drawing :-

I want you to realize that I have just made this using the same 'test app' as supplied for you to start creating. It works fine. I made this in about five to ten minutes.

Most of the time was taken adjusting the points so the result looked better to the eye.

Okay then, what does the code look like in the 'MyTestDocumentB' method. I chose to use 'B' for this.

======================================================================

METHOD MyTestDocumentB(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab, oFont16Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont16Ab := Font{"Arial",16,FontStyle.Bold}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

LOCAL oGC1, oGC2 AS GraphCoords

LOCAL cText AS STRING

oGC1 := GraphCoords{5,5}

oGC2 := GraphCoords{5,100}

oBD:DrawLine(oGC1, oGC2, 2)

oGC1 := GraphCoords{5,5}

oGC2 := GraphCoords{100,5}

oBD:DrawLine(oGC1, oGC2, 2)

oGC1 := GraphCoords{45,3}

cText := "X - axis"

oBD:Font := oFont22Ab

oBD:TEXT(oGC1, cText)

oGC1 := GraphCoords{1,55}

cText := "Y - axis"

oBD:TextUp(oGC1, cText)

oGC1 := GraphCoords{35,90}

cText := "Click Start - Test Graph One !"

oBD:TextUnder(oGC1, cText)

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

==================================================================

I had to set the orientation to Landscape outside of this methods - to get it to work :-) We will look at this later, for now lets get some experience placing elements.

Most of the code should be clear and obvious to you, but we have some BD methods which may be new - the TextUp() and the TextUder().

Using Grids :

Let us now 'push the boat out', as they say in the UK. Or, in a modern Americanism, lets 'go for it'.

Yes, what about our next attempt to draw something nice in 'GraphCoords', we try a ten by ten grid - as if it is half way on its way to becoming 'graph paper' as you get in mathematical work books. With a little touch of colour too make things 'nice' !

Now I think you agree, we are getting somewhere, and it looks good too. I made this in another ten minutes or so, and put it into 'NewTestDocumentC'.

Here is the relevant part of the code - notice I did not use the TextUnder() method as there was already a line close to the 'Click Start' text line.

//------------------------------------------------

LOCAL oGC1, oGC2 AS GraphCoords

LOCAL cText AS STRING

oGC1 := GraphCoords{5,95}

oGC2 := GraphCoords{95,5}

oBD:GridRegularNbyM(oGC1,oGC2,10,10)

oGC1 := GraphCoords{45,3}

oBD:Color := Color.Blue

oBD:ReSetBrushAndPen()

cText := "X - axis"

oBD:Font := oFont22Ab

oBD:TEXT(oGC1, cText)

oGC1 := GraphCoords{1,55}

oBD:Color := Color.Red

oBD:ReSetBrushAndPen()

cText := "Y - axis"

oBD:TextUp(oGC1, cText)

oBD:Color := Color.CadetBlue

oBD:ReSetBrushAndPen()

oGC1 := GraphCoords{35,100}

cText := "Click Start - Graph Paper ?"

oBD:TEXT(oGC1, cText)

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

===================================================

Most of the lines of code are messing about changing colour and placing text. The focal part to us is the one line to draw the grid, but since this is a 'higher level' method it does all the hard work and we just have to let it get on with things.

However, since we are here to learn, we had better look at the code which makes up the 'GridRegularNbyM()' method.

METHOD GridRegularNbyM(oGC1 AS GraphCoords, oGC2 AS GraphCoords, N AS INT, M AS INT) AS VOID

//----- top left to bottom right -----

LOCAL oPPC1, oPPC2 AS PaperCoords

oPPC1 := ConvertGraphCoords2PaperCoords(oGC1)

oPPC2 := ConvertGraphCoords2PaperCoords(oGC2)

SELF:GridRegularNbyM(oPPC1, oPPC2, N, M)

RETURN

As you can see this simply changes the coordinate type with a couple of calls to convert methods, and then calls another overload.

It is pointless writing the calculation code many times when we have already got the coordinate conversion methods - the process is not a 'real time' bottle-neck, so let the code do the work. Let us now view and study the code that does do some calculation :-

METHOD GridRegularNbyM(oPPC1 AS PaperCoords, oPPC2 AS PaperCoords, N AS INT, M AS INT) AS VOID

//----- top left to bottom right -----

LOCAL x1, x2, y1, y2 AS Double

LOCAL xDiff, yDiff, xInc, yInc AS Double

x1 := oPPC1:nAcross

y1 := oPPC1:nDown

x2 := oPPC2:nAcross

y2 := oPPC2:nDown

xDiff := x2-x1

yDiff := y2-y1

xInc := xDiff/M

yInc := yDiff/N

SELF:VLineSet(oPPC1, M+1, xInc, y2)

SELF:HLineSet(oPPC1, N+1, yInc, x2)

RETURN

Simply, you notice that the task of drawing a grid has been turned into drawing two sets of lines, one horizontal, the other vertical. So now we had better go and see what these methods look like. Because we have changed from 'graph' to 'paper' already, we need to see the 'paper' overload :-

METHOD VLineSet(oPPC AS PaperCoords, N AS INT, x AS Double, y2 AS Double) AS VOID

//---- oPPC (x1,y1) is top left (PAPER) coords, N number of lines, x spacing ----

//------------ y2 is bottom limit (so length is y2 - oPGC:nLine) ----

LOCAL i AS INT

LOCAL x1, y1 AS Double

LOCAL lError AS LOGIC

lError := FALSE

x1 := oPPC:nAcross

y1 := oPPC:nDown

IF y1 >= y2

lError := TRUE

ENDIF

IF N < 2

lError := TRUE

ENDIF

IF x <= 0

lError := TRUE

ENDIF

IF (N-1)*x+x1 > 100

lError := TRUE

ENDIF

IF lError

Messagebox.Show("error in VLineSet ???")

SELF:Error := TRUE

SELF:LastErrorMessage := "VLineSet not specified correctly."

RETURN

ENDIF

//------------------------------------------

LOCAL oPoint1, oPoint2 AS Point

LOCAL oPPCnew AS PaperCoords

FOR i := 0 UPTO N-1

oPpCnew := PaperCoords{y1, x1+i*x}

oPoint1 := ConvertPaperCoords2Point(oPPCnew)

oPPCnew := PaperCoords{y2, x1+i*x}

oPoint2 := ConvertPaperCoords2Point(oPPCnew)

SELF:g:DrawLine(SELF:Pen, oPoint1, oPoint2)

NEXT

lError := FALSE

RETURN

And now the similar task but in the horizontal direction :-

METHOD HLineSet(oPGC AS PageCoords, N AS INT, y AS Double, x2 AS Double) AS VOID

//----- oPGC (x1,y1) is top left (PAGE) coords, N number of lines, y spacing -----

//------------ x2 is right limit (so length is x2 - oPGC:nAcross) -----

LOCAL i AS INT

LOCAL x1, y1 AS Double

LOCAL lError AS LOGIC

lError := FALSE

x1 := oPGC:nAcross

y1 := oPGC:nLine

IF x1 >= x2

lError := TRUE

ENDIF

IF N < 2

lError := TRUE

ENDIF

IF y <= 0

lError := TRUE

ENDIF

IF (N-1)*y+y1 > 100

lError := TRUE

ENDIF

IF lError

//Messagebox.Show("error in HLineSet ???")

SELF:Error := TRUE

SELF:LastErrorMessage := "HLineSet not specified correctly."

RETURN

ENDIF

//-----------------------

LOCAL oPoint1, oPoint2 AS Point

LOCAL oPGCnew AS PageCoords

FOR i := 0 UPTO N-1

oPGCnew := PageCoords{y1+i*y, x1}

oPoint1 := ConvertPageCoords2Point(oPGCnew)

oPGCnew := PageCoords{y1+i*y, x2}

oPoint2 := ConvertPageCoords2Point(oPGCnew)

SELF:g:DrawLine(SELF:Pen, oPoint1, oPoint2)

NEXT

lError := FALSE

RETURN

As you can gather, nothing more than a bit of number crunching, and a couple of loops to draw single lines.

However, we can vastly improve our graph paper, simply by placing another 'gray' grid with thinner lines on the paper first, then placing the slightly thicker black lines.

See what you think of this :-

I would appear that we now know how to draw regular grids, so we better more on to complex ones without need to explain any more, but first check out the extra lines which did the grey and black, thin and thick lines above :-

LOCAL oGC1, oGC2 AS GraphCoords

LOCAL cText AS STRING

oBD:Color := Color.LightGray

oBD:ReSetBrushAndPen()

oBD:Pen := Pen{oBD:Brush, 0.5}

oGC1 := GraphCoords{5,95}

oGC2 := GraphCoords{95,5}

oBD:GridRegularNbyM(oGC1,oGC2,100,100)

//------------------------------------

oBD:Color := Color.Black

oBD:ReSetBrushAndPen()

oBD:Pen := Pen{oBD:Brush, 2.0}

oGC1 := GraphCoords{5,95}

oGC2 := GraphCoords{95,5}

oBD:GridRegularNbyM(oGC1,oGC2,10,10)

More Complex Grid :

PHH - Sept. 2008


Section 8 - Images and Bitmaps

Navigation: »No topics above this level«

Section 8 - Images and Bitmaps

 Return to chapter overview

Enter topic text here.


Section 9 - Reporting with data tables

Navigation: »No topics above this level«

Section 9 - Reporting with data tables

 Return to chapter overview

Enter topic text here.


why use data tables ?

Navigation: Section 9 - Reporting with data tables >

why use data tables ?

 Return to chapter overview

Enter topic text here.


breaking down the task

Navigation: Section 9 - Reporting with data tables >

breaking down the task

 Return to chapter overview

Enter topic text here.


Section 10 - a Reporting class or 'tool'

Navigation: »No topics above this level«

Section 10 - a Reporting class or 'tool'

 Return to chapter overview

Enter topic text here.


what does the 'tool' do ?

Navigation: Section 10 - a Reporting class or 'tool' >

what does the 'tool' do ?

 Return to chapter overview

Enter topic text here.


a fast way to customize

Navigation: Section 10 - a Reporting class or 'tool' >

a fast way to customize

 Return to chapter overview

Enter topic text here.


selecting columns

Navigation: Section 10 - a Reporting class or 'tool' >

selecting columns

 Return to chapter overview

Enter topic text here.


setting properties

Navigation: Section 10 - a Reporting class or 'tool' >

setting properties

 Return to chapter overview

Enter topic text here.


viewing report pages

Navigation: Section 10 - a Reporting class or 'tool' >

viewing report pages

 Return to chapter overview

Enter topic text here.


printing report pages

Navigation: Section 10 - a Reporting class or 'tool' >

printing report pages

 Return to chapter overview

Enter topic text here.


making defintion files

Navigation: Section 10 - a Reporting class or 'tool' >

making defintion files

 Return to chapter overview

Enter topic text here.


using without the GUI

Navigation: Section 10 - a Reporting class or 'tool' >

using without the GUI

 Return to chapter overview

Enter topic text here.


Section 11 - Letters and signatures

Navigation: »No topics above this level«

Section 11 - Letters and signatures

 Return to chapter overview

Enter topic text here.


Section 12 - Internal reports 'tool'

Navigation: »No topics above this level«

Section 12 - Internal reports 'tool'

 Return to chapter overview

Enter topic text here.


Section 13 - Laser Labels

Navigation: »No topics above this level«

Section 13 - Laser Labels

 Return to chapter overview

General Preparation :

Before we get into any detailed discuss about the nuts and bolts of printing labels on a laser printer, let us see what we are aiming at !

=======================================================================================================================

Here is the top part of a sheet of 3 by 7 laser labels.

Laser label production can be typically split into three parts. Preparing the data, setting up the template, and actually printing the labels.

The more effort that is put into the general preparation and setting up, then the more successful will be the physical act of printing.

Each of these phases needs care, and any lack of attention to detail will result in an unacceptable result. But it can be fun at the same time :-)

Requirements :

We need only the correct data, in exactly the right place on the page, and printed with the chosen colour and type face. Nothing else will do.

Oh! we could also have an image or two.

For mail merging purposes we will typically have a different set of information on each label, and this will be drawn from a data table.

Remember, we are to use the DataTable object as the source of our data for all our common tasks related to printing, because of its pivotal nature in the scheme of things in dotNet.

Before we use a DT object we make sure that the original data is suitably prepared by running it by the DTSST application first. So we should have the records (rows) in the order we require, as well as having no records which are not required for the label 'print run'.

While in the DTSST we will also make sure that the columns selected for the table to be used for the label printing, are only those either likely to be used on the label or put another way round, we do not carry over columns which are unlikely to be required.

This way we end up with a small number of fields, or columns, which will only need selecting and ordering on a 'small scale' operation.

Once we have the data in the form we require, we turn our attention to the template, or layout.

Depending on the number and size of the target labels on the 'sheet', we need to set up a starting position, and then two off-sets. One for along, and one for down.

Obviously, it will be necessary to see on screen the results of changing these settings, as well as a physical print out for absolute checking.

When testing template layout it is quite sufficient to have lines of asterisks (or similar) to mimic the data lines.

Finally, before we go to make this all happen, remember that there is no substitute for a real physical printout when it comes to getting things right.

OK, on to the next sub-section.

PHH - October 2008.


Preparing the Data

Navigation: Section 13 - Laser Labels >

Preparing the Data

 Return to chapter overview

Enter topic text here.


Setting up the Template

Navigation: Section 13 - Laser Labels >

Setting up the Template

 Return to chapter overview

Before we get sucked into the necessary details of planning a template user GUI, we ought to see what we will use to test out our proposed template settings.

Below is a 'Test Print' sample :-

=================================================================

It is only necessary to print the label areas shown, in order to see if both the positioning, and spacing, of the labels is correct.

Before we go any further, let us make it clear that we do npt test print with label sheets themselves - they are too expensive for that. Do all your test printing on paper, well before you ever try actual labels.

You will notice that the example shows five lines on each label, well we will see that this can be changed along with many other parameters for printing.

It would be ideal if the test print of the template printed the same number of lines as we expected there to be data fields on the final output.

Now let us see what a GUI may look like to allow input of such necessary detail :-

Although this is not in its final form it serves to give us at least a picture of what we are trying to make.

You may also realise from the description and designer text box controls, that the whole template setting will be stored on disk along with others of a similar nature.

If we are to be able to save a template, then we also ought to be able to view existing ones and call one back to re-apply to our test print.

Anyway, all in good time, for now we need to be able to input new settings, and have these saved to disk - using the serialization approach covered in 'Appendix D'.

As well as saving details, it would be desirable to be able to view the disk file in the rich text control.

For now, lets just see what our 'Printable Area' push button gives us :-

PHH - October 2008.


Printing the Labels

Navigation: Section 13 - Laser Labels >

Printing the Labels

 Return to chapter overview

Enter topic text here.


Section 14 - Data Preparation 'tool'

Navigation: »No topics above this level«

Section 14 - Data Preparation 'tool'

 Return to chapter overview

For many reasons it is beneficial to physically arrange the DataTable rows into the final required order, and not use an index system of any way.

If we have the rows of our DataTable object in the correct physical order, the we can easily dump the data and the schema to an XML file pair. These may be kept on disk until required for mail merge purpose, of printing a report. Laser label printing is another example 'consumer' of such an ordered DataTable.

Ideally, we would wish to have an easy way to sort our data rows into an order to suit the user. We need to be able to have a sort order which is often known as a 'multi key' sort. That means the final order is dependent on one or many columns in the table. A simple two key sort would be Lastname and Firstname - in a person type data set.

Once we have our sorted order, and have given it a visual inspection as to its 100% suitability, we would then wish to created a new table which is physically the order chosen.

As a bit of icing on the cake, it would also be nice to filter on certain column data conditions.

Well, the good news is that dotNet provides us with a more than suitable class which seems to do all this for us. Thanks to Paul Piko we will examine the suitability of the DataView class in the System.Data namespace. Thanks again Paul for your help and advice ;-)

Without wasting any more time lets move on to the next sub-section to discuss this class.

PHH - Sept. 2008


DataView class

Navigation: Section 14 - Data Preparation 'tool' >

DataView class

 Return to chapter overview

The beauty of this class is twofold. It is neither too complex, with oodles more methods and properties than we require, or is it a GUI control.

So we seem to not be carrying lots of 'baggage' we do not need, and can enjoy the relative simplicity of the class while doing the very tasks we wish to perform.

This means that when we get a feel of how to use this class, we can code it directly, or have some user interaction built into a GUI screen, so they can select appropriate options.

I have to confess that I was tempted (probably because of my lack of experience) to use the DataGridView - but this is probably way too complex, and the DataView (DV) class does look as if is was specifically designed for use similar to what we require.

At first glance there appears to be about ten methods and ten properties, which look as if they could be used frequently to our advantage. And although there are only three constructor overloads, there is one which seems most likely to be used a lot by us.

One method which immediately stands out is the "ToTable()" method. This looks as though it will create our new table with the row order, and selection, we choose to set up. All we need give for one of these overloads is the name of the table to create.

Two further methods which are of interest are Sort() and RowFilter().

OK then, before we get too carried away, let us have a quick look at the properties as listed in the SDK, and then write some code to instantiate a

==========================================================================================================================================================================

We have some code to examine !

From some of my test data for writing these notes, I have taken the four row table oDT and instantiated a DataView object.

I agree we have not don anything clever as yet, but we have easily created our oView and know it has four rows after using one of the properties, name 'Count'.

==============================================================================================

Simple beginnings but we are starting to get somewhere.

Let us now see if we can access a few of the cells using another of the properties, this time the 'Item' property.

Note we are taking the last of the four rows, that with index of three.

========================================================================================

As you can see, sweet Angelina is our girl !

However, with a surname like 'Cleftfork' we may be getting more than we bargained for :O)

After this brief success, I think we should move on to the next sub-section and see if we can alter the order in any way.

See you in "Sorting" after a refreshing cup of tea.

PHH - Sept. 2008


Sorting

Navigation: Section 14 - Data Preparation 'tool' >

Sorting

 Return to chapter overview

So we can instantiate a DataView (DV) object from an existing DataTable object.

Lets see if we can go further than counting the rows, and looking at a few data cells in a chosen row.

If we focus on the third field (or column), we see it is the 'Lastname' field. Lets see if we can sort these four fields in ascending order of their Lastname field :-

===================================================================================

Unless you are happy in believing me when I say that Swanson is the last name in an alphabetic order of the Lastname field, I will have to do something interesting to prove it !

Let us see if we can bind to a ComboBox control and display this column on its own.

Well, it certainly looks good enough for me. Let us see what the code looks like to do this :-

PROTECT olbSortedLastname AS Label

PROTECT ocbSortView AS ComboBox

.......................................

.......................................

SELF:ocbSortView:DataSource := oView

SELF:ocbSortView:DisplayMember := "Lastname"

Not a lot or work, I think you will agree.

Fine so far, now let us try and make the next step, sorting on two fields. Or making a "multi key" sort.

At the moment we have a Mick Masterson, lets us add a further record and give the person the first name of Mary. Mary will be record five, and so will follow Mick before we do a sort.

Fine, we have to be doing something right, at least the Mastersons are seen together.

In order to see if we have it right, we need to see the list of Firstnames.

This is still looking OK so now lets view the code to produce these results :-

=================================================================

As the code infers, the last record of the five to be added to the DataTable is Miss Mary.

The DataView object is then sorted by last and first names in ascending order.

You can see my 'commented out' code, which allowed me to switch the combo box contents from last to first names.

Finally, before we move on to other things, lets do a single sort on the age field to see who is youngest.

Yep, sexy Sally at a tender 21 years of age. You will also notice that I have changed the label so as to be correct about the contents of the combo box.

Now, just before we go, lets add a TextBox and bind the age field to it :-

PROTECT olbAge AS Label

PROTECT otbSortView AS TextBox

.......................................

.......................................

SELF:otbSortView:DataBindings:Add("Text", oView, "Age")

I think you will agree, we have not done a great deal to add a TextBox and have the age show for our lovely Sally ;-)

We are not here to examine data binding to controls in any detail, we will just notice in passing, how to bind to a TextBox and a ComboBox.

More of 'data binding' another day - now it is time to move on - we must see how we can do some selecting and filtering with the DataView class.

See you in the next sub-section.

PHH - Sept. 2008

Addendum :- after completing the section above I found out the following :-

oView:Sort := "Lastname ASC, Firstname DESC"

Each field in the sort definition can be either ASC or DESC. If one or the other is not specified, it appears as if ASC is taken as the default.

I will need to check out just what the behaviour is under these circumstances but at least realise that we can have a mixture of different sorting 'directions'.


Selecting - Filtering

Navigation: Section 14 - Data Preparation 'tool' >

Selecting - Filtering

 Return to chapter overview

Now that we have seen how easy it is to sort our data rows in the DataView object we have instantiated, let us move on to see just what we can do with filtering them.

Since we know that Sally is the youngest of our five people in the data record set, lets try and set a filter for all those over 21.

Well, if you trust me - and I say I have not fiddled the output, then Sally is not now to be seen. All 21 years of her are somewhere else.

Lets see what our code looks like which achieves this :-

oView:Sort := "Lastname ASC, Firstname ASC"

oView:RowFilter := "Age > 21"

In fact only the second line is new. We have set the 'RowFilter' property after the same line of sort code we used previously.

This now opens up the way for us to try other filter conditions, on other columns, with different data types.

Before we get too clever for ourselves, and get into multiple conditions of the logic, let us see a working example of a range of commonly used data types :-

oView:Sort := "Lastname ASC, Firstname ASC"

oView:RowFilter := "XmasList = TRUE" //----- works -----

I will leave it to your to check the data in the image below, to see who is currently checked as receiving a Christmas bonus.

Looks like no Christmas card for Mick and Mary, or whatever the significance of the 'XmasList' check ;-)

Now what about dates, these can often be tricky to get right ?

What has been done to remove Angelina from our list ?

oView:Sort := "DoB DESC"

oView:RowFilter := "DoB <> '1981/06/03'" //----- works -----

Notice, for a change, I have sorted the records (rows) into descending order of dates of birth.

So the filter applied only shows those rows which have a value of DoB not equal to 3rd. June 1981.

Let us now try a string field :-

oView:Sort := "Firstname"

oView:RowFilter := "Firstname <> 'Mary'" //----- works -----

Notice that we are sorting on the "Firstname" column and not specifying an order, so the default of ascending will apply.

Obviously, Mary has been filtered out - or should we say the 'Mary' row or record has been ignored.

Okay then so far so good. Lets now look at the "Accounts Owing" field, which is of data type 'Double'.

There are a couple of things to focus on with this column. Firstly, when I started these notes, I used a column heading of "Account Owing" with a space in the middle.

This proved fine till I started to do this section on filtering. As developers of 'yaw' may realise, spaces are not good news in certain places. Just having got myself out of the 'belt and braces' way of coding (never using spaces) I find that I have a problem once again. Funny old world !

I have now changed my code to be :-

oDC := DataColumn{"Account_Owing", typeof(Double)}

oDT:Columns:Add(oDC)

......................................................

oDR:Item["Account_Owing"] := 51.88

......................................................

The results are shown above for a filter condition where we ask for rows to be included which do not have an "Account_Owing" value equal to 7.89, which is our friend Cliff.

Note we have sorted the list so we have the biggest debtors at the top of the list.

oView:Sort := "Account_Owing DESC"

oView:RowFilter := "Account_Owing <> 7.89" //----- works ----- DON'T leave a space in column name !!! -----

So the point to learn here is don't put spaces in column headings if you ever wish to use DataView objects. If you want to prove this for yourself, then put back the space and trap the exception thrown. Then print out the exception message.

Having now covered a few common data types, you ought to be in a position to have a go at any others yourself, and make the sysntax work for you.

Let us now move on to compound filter statements, or those with multiple conditions.

Look at this simple result :-

We have removed, or filtered, everyone from the list except Mick. We did it by using the code below :-

oView:Sort := "Lastname ASC, Firstname ASC"

oView:RowFilter := "Lastname = 'Masterson' AND Firstname = 'Mick'"

Only our friend 'Mick' has data which fits the double condition.

Please notice that the 'AND' is not between points as in Vulcan syntax, like '.AND.' and this is because the string is used at runtime when Vulcan is not recognized. Vulcan syntax is something the compiler converts into dotNet 'runable' code. Obviously the acceptable syntax is more like C#, which is not really surprising.

Let's see if we can write a slightly more complex statement which would give us all the people with a first name of 'Mick' or 'Mary', and called Masterson.

Well we get Mick and Mary, what code did we get to work ?

oView:Sort := "Lastname ASC"

oView:RowFilter := "Lastname = 'Masterson' AND (Firstname = 'Mick' OR Firstname = 'Mary')"

Okay then, I hear you shout - you can do it a different way to that ! However, we have forced the issue, and used a three condition complex filter statement. So we are moving on.

This was good for developing our skills, but could we achieve, the same results with this data by looking for a first name beginning with 'M'. I am not sure I can get this to 'stick' but I will have a go :0)

I think I may need to talk with 'a man who knows'.

After a short trial break, I have one working solution, without going to "the man who knows", we can reserve him for later ;-)

Here is the code of what worked :-

oView:Sort := "Firstname ASC"

oView:RowFilter := "Lastname = 'Masterson' AND NOT(Firstname < 'M' AND Firstname > 'M')"

Notice we now have first name in ascending order, so Mary is before Mick.

Remember, the syntax in the quotes is not Vulcan, and so not surprisingly " ! " does not work in place of NOT.

I was hoping to use a sting method on the field content, but can't get that to work. This is the right time for our man !

From a personal point of view, and the purpose of these notes, I think we have enough flexibility and functionality to play with above.

I was planning that my data set returned from the SQL server would contain all my required records, and that I just may want to change the order of the records when it came to report printing, mail merging, and laser label printing.

Have a look in the next sub-section however to see how we can restrict the data rows in our newly created table, which we will pass on to our reporting, merging, and printing classes.

Addendum :

Since researching and writing the previous filter section above, I have found how to apply the more SQL type of field filtering :-

You may notice that the last nine entries shown above are new and have a different format to any of the previous ones.

This extra information is to be found in the SDK documentation for the Expression property of the DataColumn class :-

==================================================================================================================================================================================

It would appear that the developer can do just about anything, as long as he or she experiments and tests thoroughly first ;-)

PHH - Oct. 2008


Creating a new table

Navigation: Section 14 - Data Preparation 'tool' >

Creating a new table

 Return to chapter overview

Well readers, the job just gets easier and easier.

Look at the code below, to see how straight forward it is to make a new DataTable object which has the rows in the same physical order as the records appeared in our DataView object after applying our sort criteria.

Study the code below, and we will then discuss it afterwards :-

========================================================================================

Notice the order of our people is last name A-Z and then first name A-Z. Both in ascending order.

This should place Mary Masterson before Mick Masterson. The simple display loop to access the people first names, shows the actual new order of the DataTable records.

So for example, if we serialized this object we would get a pair of data files in XML format, which could be stored, and read in at a later stage for further use and processing.

We need to see just what the other overloads for 'ToTable()' will allow us to achieve - before that, let us dump the new table data to file.

Here is the code which is similar to that seen in a few places throughout these notes :-

================================================================================================

First lets see the schema which has been created :-

=================================================================================================================================

And now the data file in XML format.

Notice I have included the whole of the file, as part of the exercise was to demonstrate that we now have a file with physical order equivalent to the DataView sorted order.

Well, this looks just as we wanted it to :o)

Restricting the Output :

Just a little further back in the notes we said we would need to return to the overloads of the 'ToTable()' method, so see what extra flexibility and functionality was on offer.

Lets do that now.

=====================================================================================================================================================================

The first and the third in the list shown in the SDK documentation, have no specified name, the name is taken to be that of the original table.

So in essence, if we examine the fourth overload we will see the most flexibility and functionality available to us through this method.

Put simply, the Boolean parameter allows us to limit the rows, and the string array parameter to limit the columns.

This is what the SDK has to say about the Boolean parameter :-

If true, the returned DataTable contains rows that have distinct values for all its columns. The default value is false.

This sounds fine until we read what is to be said about the string array parameter :-

A string array that contains a list of the column names to be included in the returned DataTable. The DataTable contains the specified columns in the order they appear within this array.

This also is fine as long as we confine our attention to what it says. However, how does this impinge on the earlier statement ?

Does this mean if we restrict the columns, then the rows included are those where values exist in the columns to be included, or all the columns ?

We may have to do some tests to find out exactly.

To summarize what we may be able to do :-

- Include only fully completed rows

- Include only specified columns, and these will be in the physical order we specify.

This also is not clear on what will happen if we include a column name twice :O)

Let us now embark on some code writing and testing, as this seems to open up the door to some very useful 'power' as far as our control over the final DataTable format is concerned.

First lets look at our existing data records in a DataGrigView control :-

Notice that the whole 'dumped' file has been deserialized and placed in the DataTable with the order we sorted it to while the data records were in the DataView object.

Lets now make up a scenario on which to base some further testing.

Suppose we wish to have only the data required for mailing - creating both laser labels and possibly some sort of letter.

I suggest we don't need the account owing figures, or the clients age. Even if we are to send Christmas cards and / or birthday cards, we don't need the actual age. Yes, we will need the date of birth (DoB) so we can control when we mail a card, but not the age value itself.

So why don't we return to our code above, where we created a new table, and this time restrict the columns so as to miss out the two we have discussed.

On top of this, we shall also put the columns in a different order - just because we are allowed to do so ;-)

First of all, above is the results of our code changes. Notice we have two fewer columns (data fields) and also the column order has changed rather drastically.

Okay then, lets see what we needed to do to our code to make this happen :-

=====================================================================================================

The answer, as you can well see, is not a lot.

All I have done is list the five required columns by name, in the order required, and supply a FALSE for the Boolean input. We will look at that parameter more a little later in this sub-section.

For now, while we have the opportunity, lets see if we can learn some more by including a field twice in the required list :-

==============================================================================================================================================

Well, there's your answer to that one, a runtime error, or an exception thrown :-)

I must admit my money was on it working - just shows what I know !

So this is looking very much as though we need to be catching any exceptions when we try our "ToTable()" code lines.

Before we look at trapping the possible error, let us now try misspelling one of the fieldnames.

Here goes :-

============================================================================

And there's our result for changing 'Title' to 'Tittle' - but this is what I expected.

Right then, after a short break and a fresh cup of tea we will look at catching the exception so that the code will still run smoothly.

That's the theory anyway.

=========================================================================

Of course, we would need to enclose all the relevant code in the TRY / CATCH / END TRY structure.

Here I have just kept thing simple for demonstration purposes.

We would, in reality, need to enclose all the code which expected to use the new DataTable object. However, this is a topic which should be dealt with on its own. Alwyn Jennings-Bramly is writing a book on Vulcan.NET which has a nice section on Exception handling. Look out for this and get yourself a copy ASAP, there are lots of useful tip and tricks to be had - as well as a lot of knowledge !

Restricting the Rows :

Finally in this sub-section, lets create a new table with restricted rows, as well as columns.

I will now apply the filter condition below, which only gives us our Christmas card receivers :-

oView:Sort := "Lastname ASC, Firstname ASC"

oView:RowFilter := "XmasList = TRUE" //----- works ----

Now when we make our new table from the original one, which is now filtered as well as having the columns reduced in number, we get the following result :-

As the image indicates, we have only those people with a checked 'XmasList' field.

Let us take a peak at the actual data in the XML file pair :-

=============================================================================================

=================================================================================================================================

As you will notice from these files as displayed above, there is now both a reduction in the number of records (rows) as well as the number of columns.

So, before moving on, lets realise that we have managed to sort our data table, and then filter out selected rows, before making a new table which we then serialized to file on disk.

This new file on disk was read into a new table object and displayed using a grid view control.

Fine, now lets move on to the next sub-section where we make and use a GUI screen which allows the user to easily make some of the sort and filter settings necessary for us to prepare our DataTable information ready for processing by the reporting, merging and label printing classes. Well, any class that uses a DataTable object in fact.

PHH - Oct. 2008


GUI - for easy user input

Navigation: Section 14 - Data Preparation 'tool' >

GUI - for easy user input

 Return to chapter overview

Well then folks, now we have done the background work on the DataView class we can think of using it as a basis of a GUI tool.

The tool to be called 'DataTable Sorting and Selecting Tool' (DTSST), will allow any DataTable object to be sorted by rows on the contents of the specified fields, and to have a 'set' of user selected fields, or columns. The selected row and columns will be used to generate a new table with the physical structure of the specified 'format'.

Let us now do some top down design to say what our GUI tool will do.

The GUI tool will be able to be launched with a supplied DataTable object, or after instantiation, have the user select an XML file pair on disk.

Details of the schema will be readily available at a click, and column names will be seen in a list box control.

To select our columns for the target DataTable, we will be able to drag and drop from a source list to a target field list. If we are clever, we should be able to allow re-ordering of the fields by drag and drop too.

For the sorted order of the rows, we can select the field names in order of the 'multi key', and although we may not copy a field or column into the new DT object, we can still use it in the 'row sort' specification prior to that. So all fields are available for the sort criteria.

I am not sure whether we ought to include filtering - at least for version one of the tool. If we do it ought to be relatively simple, and probably not allow 'too complex' conditions to be set.

The reason I say this about the filtering, is that I see things such that my data sets returned from the SQL server, have exactly which data I require, even though we may sort the order, and restrict the fields for our reporting and the likes.

Finally, I think it would be good to see the original table, as well as the created table, and even be able to view XML table data directly from disk file.

And for those who like pictures - we need a few nice images on our GUI screens which we use to do all this.

Now we have a specification for our DTSST, lets get going with some code and testing - or maybe I will go and hunt a few nice images first :O)

The Tool (DTSST) :

Well, here I am three days later, and we have something nearly complete to examine. Below is how the tools looks now :-

The layout is pretty obvious, and the controls match very closely what our original specification laid out before us.

It will be very instructive to actually go right ahead and view some of the sub-forms - the ones used to actually do the tasks required.

First of all lets look at the column select screen :-

Quite straight forward really, the problem is that it is impossible to show the action, of the 'drag and drop' (DnD), from one list box to the other.

The code has been written to not allow 'copy' as a DnD mode, only 'Move' as a mode for this example.

We have one class called 'PearlDemoListBox' which handles each of the list boxes on this and subsequent GUI forms.

In the screen above the list box items drag from both list, and also can be dropped into the other list. We do not wish any column name to be in both list, so the 'Copy' has been disabled.

Fine, we are getting somewhere. Now we need to see the screen which allows us to change the column order, once we have our column selection.

You will notice that when we first open the screen we have a source list which is identical in every way to the one made when we closed the column selection screen.

I will now go ahead and DnD some field names into the target list, in the order of what is required :-

Allow our example is just show with a few field names, or columns, we have to design the tool so that it will handle quite large numbers of fields.

Eventually, each and every selected column needs to be dragged into the target list, as this is simply the order that the columns will be in the new table and new XML file pair which are created later in the sequence.

Okay folks, once we have finished with choosing both our column 'set' and its order, we can focus on the sorted order of the data rows to be copied over into the new table.

Let us now examine the screen to allow us to set the sort key order. I use the word 'key' as for many years going back 'when' this was referred to as a 'multi key sort'.

Lets see how this looks :-

You will notice that we have selected three fields, and that these too have been removed from the original list from where they were dragged. So the mode was set to 'Move'.

Fine, so we sort on the three fields show, the main 'division' will be bay manufacturer, then supplier, and then the items from each of those suppliers.

So far we have not said anything about the 'order' of the sorts. By 'order' I mean if they will be A to Z, or in fact Z to A in nature.

Lets see what we get when we click the push button called 'Set A-Z or Z-A Order'. View below for what happens :-

If we double click on the items in the Order list we cause a 'toggle change' from ASC to DESC, and back again. Just keep double clicking and the values go back and forth.

When we close the screen the values will be saved as they show at the time of closing.

Out of interest, the push button 'Set A-Z or Z-A Order' will toggle the viewing of the Order list, but each time the list is shown, it has all values set as the default ASC.

Since it only makes sense to sort on a small handful of fields at most, this is probably the easiest way to code it. It also works fine from my tests. However, one you have the code in your hands then you can do as you wish :-)

Lets just see what our trial view of the new table looks like for this data row sort order defined and set :-

I have tried to select a portion of the data to show that the triple ASC switch for the three fields, does as it is supposed to.

Three ASC values were used for this example, rather than the values shown in the last image of the 'Row Order' screen, as this is what the 'real life' situation requires at a business level - for this product data.

Once the new table has been created and saved in a pair of XML files (one data and one schema) the files can be read back in to see that the table is exactly as planned by the user :-

You can see that this table had a few different selections to those shown earlier, but there are correct, believe me.

Or if you don't then look at the schema file produced :-

===========================================================================================================================

The same eight fields in the same physical order - this is the .XSD file of the pair created.

Now have a look below, at a small portion of the data in the .XML file of the pair created.

This again reflects the same physical order - and the records are in order of manufacturer as a main sort key :-

Okay then, we are really getting somewhere with this DTSST application. Most of the rest of the stiff is pretty routine, for selecting focus folders of source and target, and seeing the settings which have been selected at any point in the use of the tool.

The only main aspect not so far covered in detail is the screen to allow users to provide or select a filter string.

The 'Filter Rows' push button brings up the following screen :-

This is how the screen launches if no filter string has been previously provided. In other words the focus filter setting is empty.

Notice a trial button is 'on hand' to allow the user to test their filter condition, and it launches a screen with the new table displayed, if the dotNet framework (DataView class) can make sense of the filter setting string.

This quite a handy feature, since it also helps if the user specifies something NOT acceptable. Let us see what we get if we type in something 'wrong' :-

You will notice that the error has been trapped - or as they say these days, the exception has been caught !

The exception message informs us that there is not a field or column, with the name 'testfield'. What else can go wrong ?

Notice that we have tried to apply a numeric filter condition to a string field.

I have also included a suitable heading in the message box, as the previous one looked a bit blank, I have to say.

Do be careful however, the exception catching is not foolproof apparently. If I mix a Boolean field and a decimal numeric condition I seem to get no exception thrown, and a blank new table results.

See below for the setting and the result :-

And yet we get an exception when we use a string comparison :-

Strangely, a string comparison with 'true' works fine. See below to get only the active records displayed :-

So it would look like the 'art' of setting filter conditions continues to be a difficult and mysterious one. I suppose in some deep seated inner feeling, I am quite pleased.

Can't make everything as easy as dotNet all the time :o)

However, to summarise this filter settings process so far, the exception trapping does mean we can try out our specified conditions without dropping out of the software, or getting a nasty FATAL error. This in itself is very useful when trying to use the tool seriously.

It is very quick to go back and forth from editing the filter condition, to doing a trial.

We will come to how we can save successful conditions, as well as retrieve ones saved earlier - when we have looked at the code which does the exception handling.

................................................................

................................................................

It is the 'ToTable()' method of the DataView class which makes the new DataTable object.

I have to confess I went no further than using the system exception, in this very simple way, initially just as a start. However, it seems to do the job OK, and so I am leaving it like this for now.

There is one other thing to be aware of when filtering, and that is an empty string seems to do just nothing - you still get a table with no filter condition applied - so I have added my own code to warn of this :-

Notice that the 'Do Trial' button just calls on the Create and View methods of the main screen class, for the new table.

Here is me trapping and warning of an empty filter string :-

Limitations in Conditions Set :

Until I can get help from others, I have to say that the syntax of the filter condition string is useful but somewhat limited.

In this I mean we have not seen how to 'get at' only part of the field as I did in DBF filtering. We would ideally like to be able to ask for a condition where the first three characters of a string field is exactly equal to "Jon" for example. I would like to use string methods if possible.

Saving and Retrieving Filter Condition Strings :

First of all, let us take a look at the screen which allows the user to enter and test filter conditions for the table in focus :-

As you can see, we have a text box at the bottom of the form for providing a filter condition - this is taken as the 'focus condition' and is the one applied when the 'Do Trial' button is pressed.

The working samples, shown in the rich text box, are only for general guidance. These are a set of filter conditions which I have had working, and which gave the desired results.

Also notice that the filter in focus may be saved into the drop down box at the top of the screen, and re-used sometime in the future.

The saved filter conditions are available during the whole of the working session in the DTSST. And, as an added bonus, they are also saved to an XML file when the app is closed.

In fact there are a few DTSST settings which are saved to XML file by serialization when the app is closed. On start-up of the app subsequently, the same settings are applied as existed at the point of the previous closure.

We will look at what these are in a little while. For now lets see what a 'basic' data dump to XML file looks like :-

=================================================================================================================

You will notice that the dump file is placed in the same folder as the application EXE file.

Although we only have a couple of other stored settings to the list of filter conditions, you will see clearly what is happening.

Only source and target folders have been stored along with the four filter strings shown in the drop down box in earlier image. However, at the moment these are re-applied at application start-up and the drop down list populated as if they had been selected using the folder dialog tool.

How to Use the Stored Filter Conditions :

Now that we can store our filter conditions, we better have a short discussion on how to best use the facility.

Only save conditions which have been tested and tried - and proved to work. Don't end up saving 'rubbish'.

Re-apply saved conditions by selecting them from the combo box - they will automatically be copied into the 'focus setting' text box, and become the focus setting.

There is to be a small 'edit facility' which will allow you to tidy the growing filter list - in case you get entries you no longer require.

Only unique conditions will be added to the list, no duplicates.

The edit facility will be a small GUI based on a couple of 'PearlDemoListBox' controls. We will need to drag any unwanted filter strings into the 'scrap these' list box. On screen closure only those remaining in the list will be saved, the others will be dropped.

I will also provide a push button facility (on the main screen), to allow the XML dump file to be scrapped and a new one started. However, you will lose all your previous filters when you use this facility, so be warned ;-)

Let us see the 'duplicate trap' in action. Note the image below is a modern technology 'fiddle' as it is showing two things at once - just to make things clear.

Look at the image and then we will discuss it afterwards :-

I have tried to save a filter string from the focus setting text box into the saved list - however, it is already present. The code simply uses the binary search method of the combo box, and as a result we can ignore the request to save it, as the returned index value from the search is zero or greater.

Let us now see what the new push button 'Edit Filter List' does for us. We will see the new GUI screen and what it can do.

Study this new screen below :-

Filter strings may be dragged and dropped (moved) from one list box to another. (Both list boxes inherit from the PearlDemoListBox class and have the 'Copy' function disabled.)

Using close will keep the changes, and quit lets them 'evaporate'.

The image below is after a live test from the screen above, and saving the changes by using the close button :-

Notice that the " prodsize >= '0' " string has disappeared.

And a further check of the XML dump file shows this entry below :-

=============================================================================================

All of this seems proof enough that the filter condition list can be edited and saved successfully using the tools provided.

A Helping Hand :

You may find it helpful when creating and testing filter conditions, to have a look at the schema of the data table in focus.

The following screen can be launched by clicking the 'Schema of Focus' button at the top right of the main form.

It has been made to display using the form 'show()' method, as opposed to my usual 'showdialog()' approach. This means it will stay on the screen as long as you wish, while going into other sub-screens to create and test filter conditions, for example. The screen closes when the main app is closed, or when you choose to do so with a mouse click in the control box, top right.

So, what left to do for this tool ?

Well, it would be a good idea to make things so it can be used and applied without need to show the GUI screen. All the tools are there basically, to have the tool used by code only. Then the developer could also use it under the covers - so to speak.

I will make this work without the GUI screen and include a sub-section called 'Using the DTSST from Code'.

PHH - October 2008.


How to Use - an Outline

Navigation: Section 14 - Data Preparation 'tool' >

How to Use - an Outline

 Return to chapter overview

Below is an outline of how to use the tool.

Firstly, lets say that the About and Help buttons give some useful detail - but not too much.

And now the Help information message box :-

In order to get data into a source data table object, the user needs to select a source folder.

Once a source folder is in focus the drop down list 'Source XML' is populated with names of 'pairs' of files. On name is associated with both an XML and an XSD file in the folder in focus. Only pairs are represented.

Okay then, we need to select a source file pair by choosing one from the drop down list. When this is done a couple of confirmatory message boxes appear, and then the table is shown in the grid control on the form. The name of the table is also shown just above the grid, in the centre. This table name comes from the schema file which is the XSD format file.

==========================================================================================

At this point the 'Schema of Focus' button gives some useful information on the field names and data types of the columns.

So far three pages of 118 column information has been printed out. A quick look at the message box produced when clicking the 'View Current Settings' button, shows that we have 49 columns in this example, and 699 rows of data. There are a lot of other settings still left blank however :-

I will now use the 'Column Selection' button and screen, as well as the 'Column Order' screen to do selection so that we flesh out the current settings.

The order of the data rows and the filter string setting will also be made :-

As a result of these settings, then using the new table name supplied by the user - in this case "ClickStartTest13" - we get a new table created as shown :-

[ Remember at this stage the table is still just in memory, there is no hard copy. ]

====================================================================================================================================

Using the last two buttons at bottom right of the form - just above close - we get a new file pair made of the recently created table.

We can now see details

Now the Schema of Focus shows the details of the new table which has been read back in from the hard copy file pair on disk :-

==================================================================

From a data type point of view, not very exciting, however we should make sure we include a 'DateTime' field next time we get a chance.

One final thing to stress, and that is the new file pair is created in the folder which has been selected as the target folder. So it is a good idea to follow the help instructions, and set this early in your sequence of events.

The table name is 'thrown back' at you for confirmation and so you do get a reminder about setting that correctly, but not the folder :-)

Good Luck with your sorting and selecting !

PHH - October 2008.

{ Oops! - just noticed we get an exception if the Browse Folder window is closed from the control box or the cancel button - will have to trap that one !}


Using the DTSST from Code

Navigation: Section 14 - Data Preparation 'tool' >

Using the DTSST from Code

 Return to chapter overview

PHH - October 2008.


Working Code(14)

Navigation: Section 14 - Data Preparation 'tool' >

Working Code(14)

 Return to chapter overview

METHOD Initialize() AS VOID

LOCAL oDT AS DataTable

LOCAL oDC AS DataColumn

oDT := DataTable{"BDtestTable"}

oDC := DataColumn{"Title", typeof(STRING)}

oDT:Columns:Add(oDC)

oDC := DataColumn{"Firstname", typeof(STRING)}

oDT:Columns:Add(oDC)

oDC := DataColumn{"Lastname", typeof(STRING)}

oDT:Columns:Add(oDC)

oDC := DataColumn{"Age", typeof(INT32)}

oDT:Columns:Add(oDC)

oDC := DataColumn{"Account_Owing", typeof(Double)}

oDT:Columns:Add(oDC)

oDC := DataColumn{"DoB", typeof(DateTime)}

oDT:Columns:Add(oDC)

oDC := DataColumn{"XmasList", typeof(Boolean)}

oDT:Columns:Add(oDC)

LOCAL oDCC AS DataColumnCollection

oDCC := oDT:Columns

LOCAL nNumberOfColumns AS INT

nNumberOfColumns := oDCC:Count

//Messagebox.Show("No. of columns :- "+nNumberOfColumns:ToString())

LOCAL oDR AS DataRow

oDR := oDT:NewRow()

oDR:Item[0] := "Miss"

oDR:Item[1] := "Sally"

oDR:Item[2] := "Swanson"

oDR:Item[3] := 21

oDR:Item[4] := 31.13

//oDR:Item[5] := DateTime{04,21,1979}

oDR:Item[5] := DateTime{1979,04,21}

oDR:Item[6] := TRUE

oDT:Rows:Add(oDR)

oDR := oDT:NewRow()

oDR:Item[0] := "Mr"

oDR:Item[1] := "Mick"

oDR:Item[2] := "Masterson"

oDR:Item[3] := 32

oDR:Item[4] := 47.56

oDR:Item[5] := DateTime{1989,07,11}

oDR:Item[6] := FALSE

oDT:Rows:Add(oDR)

oDR := oDT:NewRow()

oDR:Item[0] := "Rev"

oDR:Item[1] := "Clifford"

oDR:Item[2] := "Chonka"

oDR:Item[3] := 54

oDR:Item[4] := 7.89

oDR:Item[5] := DateTime{1959,04,21}

oDR:Item[6] := TRUE

oDT:Rows:Add(oDR)

oDR := oDT:NewRow()

oDR:Item["Title"] := "Ms"

oDR:Item["Firstname"] := "Angelina"

oDR:Item["Lastname"] := "Cleftfork"

oDR:Item["Age"] := 29

oDR:Item["Account_Owing"] := 51.88

oDR:Item["DoB"] := DateTime{1981,06,03}

oDR:Item["XmasList"] := TRUE

oDT:Rows:Add(oDR)

LOCAL oView AS DataView

oView := DataView{oDT}

//Messagebox.Show("Number of Records :- "+oView:Count:ToString())

LOCAL oDRV AS DataRowView

LOCAL cText AS STRING

oDRV := oView:Item[3]

cText := (STRING)oDRV:Item[0]+" "

cText += (STRING)oDRV:Item[1]+" "

cText += (STRING)oDRV:Item[2]+" "

//Messagebox.Show(cText,"Cell Test :")

oView:Sort := "Lastname ASC"

oDRV := oView:Item[3]

cText := (STRING)oDRV:Item[0]+" "

cText += (STRING)oDRV:Item[1]+" "

cText += (STRING)oDRV:Item[2]+" "

Messagebox.Show(cText,"ASC Order :")

//SELF:ocbSortView:DataSource := oView

//SELF:ocbSortView:DisplayMember := "Lastname"

oDR := oDT:NewRow()

oDR:Item[0] := "Miss"

oDR:Item[1] := "Mary"

oDR:Item[2] := "Masterson"

oDR:Item[3] := 53

oDR:Item[4] := 74.65

oDR:Item[5] := DateTime{1955,06,30}

oDR:Item[6] := FALSE

oDT:Rows:Add(oDR)

LOCAL cString AS STRING

/*oView:Sort := "Lastname ASC, Firstname ASC"

oView:RowFilter := "Age > 21" //----- works -----

SELF:otbSortView:DataBindings:Add("Text", oView, "Age") */

oView:Sort := "Lastname ASC, Firstname ASC"

oView:RowFilter := "XmasList = TRUE" //----- works ----

/*oView:Sort := "DoB DESC"

oView:RowFilter := "DoB <> '1981/06/03'" //----- works ----- */

/*oView:Sort := "Firstname"

oView:RowFilter := "Firstname <> 'Mary'" //----- works ----- */

/*oView:Sort := "Account_Owing DESC"

oView:RowFilter := "Account_Owing <> 7.89" //----- works ----- DON'T leave a space in column name !!! ----- */

/* oView:Sort := "Lastname ASC, Firstname ASC"

oView:RowFilter := "Lastname = 'Masterson' AND Firstname = 'Mick'" */

/*oView:Sort := "Lastname ASC"

oView:RowFilter := "Lastname = 'Masterson' AND (Firstname = 'Mick' OR Firstname = 'Mary')" */

/*oView:Sort := "Lastname ASC, Firstname ASC"

oView:RowFilter := "Lastname = 'Masterson' AND Firstname:Substring(0,1) = 'M'" */

/*oView:Sort := "Firstname ASC"

oView:RowFilter := "Lastname = 'Masterson' AND NOT(Firstname < 'M' AND Firstname > 'M')"

*/

//oView:RowFilter := "DoB = '1981/06/03'" //----- works -----

//oView:RowFilter := "Title = 'Rev'" //----- works -----

//oView:RowFilter := "Firstname = 'Mary'" //----- works -----

//oView:RowFilter := "Lastname = 'Masterson'" //----- works -----

//oView:RowFilter := "Account_Owing = 7.89" //----- works ----- DON'T leave a space in column name !!! -----

/*oView:RowFilter := "Title = 'Rev'"

oView:RowFilter := "Age = 53" */

//oView:RowFilter := "Age > 21 AND Account_Owing <> 7.89" //----- works -----

//===========================================================

Messagebox.Show(cText,"ASC & Filtered :")

SELF:ocbSortView:DataSource := oView

SELF:ocbSortView:DisplayMember := "Firstname"

//SELF:otbSortView:DataBindings:Add("Text", oView, "Age")

//SELF:otbSortView:DataBindings:Add("Text", oView, "DoB")

//RETURN

LOCAL oDTnew AS DataTable

LOCAL nLen, i AS INT

LOCAL oRow AS DataRow

LOCAL aColumns AS STRING[]

//oView:RowStateFilter := DataViewRowState.ModifiedCurrent

aColumns := <STRING>{"Xmaslist", "Firstname", "Title", "DoB", "Lastname"}

TRY

oDTnew := oView:ToTable("ClickTest04",FALSE,aColumns)

nLen := oDTnew:Rows:Count

cText := ""

FOR i := 1 UPTO nLen

oRow := oDTnew:Rows:Item[i-1]

cText += (STRING)oRow:Item[1]+CRLF

NEXT

Messagebox.Show(cText,"New Table :")

CATCH ex AS Exception

Messagebox.Show(ex:message)

END TRY

//------------------------------------------------------

LOCAL cFile, cData, cSchema AS STRING

cFile := "C:\VDNUK\viewDump_04"

cSchema := cFile+".XSD"

cData := cFile+".XML"

oDTnew:WriteXmlSchema(cSchema)

oDTnew:WriteXml(cData)

IF File.Exists(cSchema) .and. File.Exists(cData)

Messagebox.Show("Both files made successully !")

ELSE

Messagebox.Show("There was some sort of problem !?!")

ENDIF

//RETURN

//===============================================================

LOCAL nNumberOfRecords AS INT

nNumberOfRecords := oDT:Rows:Count

Messagebox.Show("No. of Records :- "+nNumberOfRecords:ToString())

//LOCAL cFile, cData, cSchema AS STRING

cFile := "c:\VDNUK\BestDocTableTest_01"

cSchema := cFile+".XSD"

cData := cFile+".XML"

/*oDT:WriteXmlSchema(cSchema)

oDT:WriteXml(cData) */

IF File.Exists(cSchema) .and. File.Exists(cData)

Messagebox.Show("Both files made successfully !")

ELSE

Messagebox.Show("There was some sort of problem !?!")

ENDIF

LOCAL oDT2 AS DataTable

oDT2 := DataTable{"BDtestTable"} //----- don't use a name ! get error -----

//---------- either leave blank or get the original from the schems file ------

TRY

oDT2:ReadXmlSchema(cSchema)

oDT2:ReadXml(cData)

CATCH e AS Exception

Messagebox.Show(e:tostring())

END TRY

//LOCAL cText AS STRING

cText := (STRING)oDT2:Rows[3]:Item["Firstname"]

Messagebox.Show(cText)

//===============================

//SELF:DoLoadXMLfiles()

SELF:Cols2Use := <INT>{0,1,2,5,7,8,9,10,11,114,115}

//SELF:Cols2Use := <INT>{0,1,2,5,8,9,10,114,115}

SELF:Rows2Use := <INT>{0,1,13,29,30,31,32,33,34,35,36,37,38,39,49,59,69,79,89,99,109,119}

SELF:FindDefaultPrinter()

SELF:FindXMLfiles()

//=================== My Testing =========================

/*SELF:cFocusXML := "C:\VDNUK\ReportPrinting\FMC31Dump.XML"

SELF:cFocusXSD := "C:\VDNUK\ReportPrinting\FMC31Dump.XSD"

*/

SELF:cFocusXML := "c:\VDNUK\viewDump_04.XML"

SELF:cFocusXSD := "c:\VDNUK\viewDump_04.XSD"

SELF:Text := "Viewing Test Data : Click Start RnP :"

SELF:AutoLoadXMLpair()

SELF:DoTryReportGenerator()

//========================================================

RETURN

PHH - October 2008.


Section 15 - Mail Merging

Navigation: »No topics above this level«

Section 15 - Mail Merging

 Return to chapter overview

Even after all these years, it is very satisfying to get a Mail Merge operation to run successfully. Especially when you have done all the work, and coding yourself.

If we can place elements on paper wherever we wish on the page, then we ought to be able to use data from a suitable data source, and place that in the relevant places on our page too. So the text, numbers and dates, come from data fields instead of being wired into the code - but we ought to be masters of such things from past experience.

As well as producing the letters to go out to our fictitious clients, we will also produce their laser labels for the envelopes too.

In order to do this we will create one small data source, in our case a DataTable object (DT), and use it for both operations.

We use a DataTable object as this is probably the most common, and flexible, object to hold data and allow us to carry out a wide variety of dotNet operations. Remember that SQL queries can produce a DT with so little effort to the developer, it is untrue. A couple of lines of code does it all.

Also, it is relatively easy to turn any other data format into a DataTable object. DBF, CSV, and especially XML and DTs go together like a dream.

To find out more about DTs and how they are setup and used, check out 'Appendix F'. In that section we will see how the very small mail merge data file is made ready for use when we need it.

If you wish to see more on file format conversions, look in ClickStartVDN to see the 'FileMultiConverter' tool.

For the purposes of our use in RnP ClickStart we will make an XML file pair for reading back into the DT.

I apologise to readers who may think that "three letters a mail merge does not make", but in my book, if it works with three - then it will work with one hundred and three, and so on. Also it is easier to show three side by side in a PrintPreview screen, and make an image to show here in these notes.

PHH - Sept. 2008


Direct from Code

Navigation: Section 15 - Mail Merging >

Direct from Code

 Return to chapter overview

Okay then, 'Mail Merging' (MM) here we come !

Let us start off this section with some immediate success. Always a good way to start.

Below is the output from a three record data file which has been used by one document to create three printed variations.

==========================================================================================================================================

If you check out the relevant part of 'Appendix F' you will see that this is displaying some of the field values from the data file made for the purpose of mail merging. As yet we have not used some of the date and numeric fields, but all the three records have been done.

Also you may be interested to know that I used the 'testbed' app. with an extra button added to it for MM purposes. If you are unfamiliar with the TestBed app then look into 'Section 6' on "Experimentaion, test app".

The extra method which has been added is called 'MyTestDocumentMM()' and is shown below :-

==============================================================

METHOD MyTestDocumentMM(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

IF SELF:oMMDT = NULL_OBJECT

//----- warning -----

MessageBox.Show("Data Table is not instantiated !")

e:HasMorePages := FALSE

RETURN

ENDIF

//==========================================================

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oBD:SetGraphics(e:Graphics)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//---------------------------------------------------

//============ place your MM test code here ============

oBD:ResetLinesPerPage(36)

SELF:GetMakeMMobject(SELF:nRowPointer) //----- make relevant oMM object -----

oBD:Font := oFont22Ab

LOCAL oPGC AS PageCoords

oPGC := PageCoords{5,10}

oBD:TEXT(oPGC,oMM:Title)

oPGC := PageCoords{6,10}

oBD:TEXT(oPGC,oMM:Firstname)

oPGC := PageCoords{7,10}

oBD:TEXT(oPGC,oMM:Lastname)

oPGC := PageCoords{8,10}

oBD:TEXT(oPGC,oMM:Add1)

oPGC := PageCoords{9,10}

oBD:TEXT(oPGC,oMM:Add2)

oPGC := PageCoords{10,10}

oBD:TEXT(oPGC,oMM:Add3)

oPGC := PageCoords{11,10}

oBD:TEXT(oPGC,oMM:PCode)

//=============== end of test code ==================

SELF:nRowPointer +=1

IF SELF:nRowPointer > SELF:NumberOfRows

e:HasMorePages := FALSE

ELSE

e:HasMorePages := TRUE

ENDIF

RETURN

================================================================

What you will notice is that in many ways the code is quite simple. The actual placing of each element is a bit tedious - but that's printing for you :0)

On the other hand the making of the MailMerge object, and the subsequent page control, is a bit more interesting.

Let us start to examine the details of what has been done, by looking at the MailMerge class, and how a oMM object is made at the start of the processing for each document.

Firstly, the MailMerge class :-

===============================

CLASS MailMerge

EXPORT Title AS STRING

EXPORT Firstname AS STRING

EXPORT Lastname AS STRING

EXPORT Add1 AS STRING

EXPORT Add2 AS STRING

EXPORT Add3 AS STRING

EXPORT PCode AS STRING

EXPORT Telephone AS STRING

EXPORT Email AS STRING

EXPORT DoB AS DateTime

EXPORT LastOrder AS DateTime

EXPORT XmasList AS Boolean

EXPORT Owing AS Double

CONSTRUCTOR()

SUPER()

SELF:Initialize()

RETURN

METHOD Initialize() AS VOID

Title := ""

Firstname := ""

Lastname := ""

Add1 := ""

Add2 := ""

Add3 := ""

PCode := ""

Telephone := ""

Email := ""

DoB := DateTime.MinValue

LastOrder := DateTime.MinValue

XmasList := FALSE

Owing := 0.0

RETURN

END CLASS

=================================

As you will notice, nothing clever here - we just need to have an object which corresponds 100% to the structure of the DataTable, and its rows.

In fact the 'GetMakeMMobject()' method is also simple and reflects the structure of the data. See below :-

=======================================

METHOD GetMakeMMobject(nRow AS INT) AS VOID

LOCAL oDR AS DataRow

oDR := SELF:oDRC:Item[nRow-1]

oMM := MailMerge{}

oMM:Title := (STRING)oDR:Item[0]

oMM:Firstname := (STRING)oDR:Item[1]

oMM:Lastname := (STRING)oDR:Item[2]

oMM:DoB := (DateTime)oDR:Item[3]

oMM:LastOrder := (DateTime)oDR:Item[4]

oMM:XmasList := (Boolean)oDR:Item[5]

oMM:Owing := (Double)oDR:Item[6]

oMM:Add1 := (STRING)oDR:Item[7]

oMM:Add2 := (STRING)oDR:Item[8]

oMM:Add3 := (STRING)oDR:Item[9]

oMM:PCode := (STRING)oDR:Item[10]

oMM:Telephone := (STRING)oDR:Item[11]

oMM:Email := (STRING)oDR:Item[12]

RETURN

=======================================

We use the current value of the class variable 'nRowPointer' to the 'GetMakeMMobject()' method.

The most important part of the method is at the beginning where the correct DataRow object is created from the data row collection which is a class property / field.

Both of the class 'collection' objects, associated with the provided data table, were setup after instantiation of the 'NewTestDocument' object, and just before it was displayed on screen with the 'ShowDocument()' method.

Now you have seen how the three documents have been created, lets us look at the other end of the process - that is what happens when we click the push button to start the mail merge.

Behind the button is this code :-

METHOD pbTestDocMMClick(o AS OBJECT , e AS System.EventArgs) AS VOID

//----- set up MM DataTable object -----

SELF:MakeMailMergeDataTable()

SELF:DoDisplayDoc("MM")

RETURN

So at the event of the button click we go off and find a suitable DataTable. This is then passed on for use when make a method call to display the document.

One thing at a time, and it will all be clear ;-)

METHOD MakeMailMergeDataTable() AS VOID

//=================== My Testing =========================

SELF:cFocusXML := "C:\VDNUK\FileMultiConverter\FMCmailmerge.XML"

SELF:cFocusXSD := "C:\VDNUK\FileMultiConverter\FMCmailmerge.XSD"

SELF:AutoLoadXMLpair()

RETURN

For my testing I have just hard coded in the source of the data we created in 'Appendix F'. Obviously, the auto load method then actions this as follows :-

==========================================================

METHOD AutoLoadXMLpair() AS VOID

LOCAL cFileD, cFileS, cFolder, cCaption AS STRING

LOCAL cTableName, cPathFileD, cPathFileS AS STRING

LOCAL oDCC AS DataColumnCollection

LOCAL oDRC AS DataRowCollection

LOCAL NumberOfColumns, NumberOfRows AS INT

cFileD := SELF:cFocusXML

cFileS := SELF:cFocusXSD

IF File.Exists(cFileD)

//MessageBox.Show("Data as XML exists!")

ELSE

MessageBox.Show("File NOT found :- "+cFileD)

ENDIF

IF File.Exists(cFileS)

//MessageBox.Show("Schema for XML exists!")

ELSE

MessageBox.Show("File NOT found :- "+cFileS)

ENDIF

SELF:oMMDT := DataTable{}

SELF:oMMDT:ReadXmlSchema(cFileS)

cTableName := SELF:oMMDT:TableName

//MessageBox.Show(cTableName:ToString())

cCaption := SELF:TEXT

SELF:TEXT := cCaption+" Reopting on : "+cTableName:ToString()

SELF:oMMDT:ReadXml(cFileD)

oDCC := SELF:oMMDT:Columns

NumberOfColumns := oDCC:Count

//MessageBox.Show("Number of Columns : "+NumberOfColumns:ToString())

oDRC := SELF:oMMDT:Rows

NumberOfRows := oDRC:Count

//MessageBox.Show("Number of Rows : "+NumberOfRows:ToString())

RETURN

===================================================================

I have left in the test print lines which I used in development and testing. They may be useful to you when you try to get MM to work for you for the first time.

You need to realize that this method could be useful to you in many places in the future of your coding. It is a general approach to making a DataTable object from a pair of XML / XSD files.

Normally, you may opt to miss out the last few lines where I have extracted 'Collections'. By that stage the data table has been created.

So all that seems left to be done is to pass this DataTable to be used in the 'NewTestDocument' object, from the 'DoDisplayDoc()' method of the 'fmTestBD' class :-

========================================

METHOD DoDisplayDoc(cWhich AS STRING) AS VOID

LOCAL oNTD AS NewTestDocument

oNTD := NewTestDocument{cWhich}

IF cWhich == "MM"

oNTD:SetUpDataTable(oMMDT)

ENDIF

oNTD:ShowDocument()

oNTD := NULL_OBJECT

RETURN

========================================

The data table is passed as a parameter in the 'SetUpDataTable()' method, this has been added to the class since we first made it in the earlier section number 6.

In fact a few little 'bits and bobs' have been added to the 'testbed' and 'test document' classes to allow the MM to work. Lets look at these to complete our first look at mail merging.

First, what is new to "NewTestDocumet" :-

====================================

CLASS NewTestDocument

.............................

.............................

//---------------------------

EXPORT oMM AS MailMerge

EXPORT oMMDT AS DataTable

EXPORT nRowPointer AS INT

EXPORT NumberOfRows AS INT

EXPORT NumberOfColumns AS INT

EXPORT oDCC AS DataColumnCollection

EXPORT oDRC AS DataRowCollection

...............................................

...............................................

METHOD SetUpDataTable(oDTin AS DataTable) AS VOID

oMMDT := oDTin

oDCC := SELF:oMMDT:Columns

SELF:NumberOfColumns := oDCC:Count

oDRC := SELF:oMMDT:Rows

SELF:NumberOfRows := oDRC:Count

SELF:nRowPointer := 1

RETURN

...................................

...................................

...................................

METHOD SetAppropriateHandler() AS VOID

DO CASE

................................................

................................................

CASE SELF:cDoc = "MM"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentMM()}

END CASE

RETURN

METHOD UnSetAppropriateHandler() AS VOID

DO CASE

................................................

................................................

CASE SELF:cDoc = "MM"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentMM()}

END CASE

RETURN

====================================================================

Simply we have had to add some class variables to take the data table and it associated 'bits'. The 'SetUpDataTable()' method then assigns to these, suitable values working back from the supplied DatTable object.

So by the time the MM documents are to be printed, there exists values for NumbersOfRows, nRowPoint, and the collections for DataRows and DataColumns.

These things are pretty obvious, and the compiler lets you know a lot about what you may not get 'quite right'. However, an easy thing to overlook is the associate a suiable event handler with the 'PrintPage' event.

Even more, likely is that you forget to include the couple of lines to 'dissociate' it again afterwards.

My philosophy is - if you switch them on, then switch them off too ! Although it would appear that the 'system' removes associations after the PrintPage event is over, I think the user ought to cause the dissociation too. I am pretty sure I followed this practice from reading good texts in the past.

Stop Here :

If you are a first time printer - and new to Vulcan and dotNet, you ought to stop at this point as we have covered all that is required to carry out a mail merge.

More Advanced Users :

People wishing to learn more about the deeper aspects of printing by controlling the event handlers for the PrintPage event - read on !

In fact I have just done some testing with handler settings - you may be interested to know what happened :-

=================================================================================================================

==================================================================================================================

Since the row 'three' referred to is actually the fourth row (zero based indexing for collections) then you may have a clue !?

Yes, we have the same event handler 'set' twice and so it tries to run twice. You need to realise that events can (and do) have many handlers associated with them, and they all get executed. So what if some are the same, they get run again. So, if we actually change our end of page logic to resetting the page pointer to 1 when it has got too big, we should get more pages. However, the results are likely to be unpredictable - lets see :-

==================================================================================================================================

Interesting but not very nice ! Its very much as I thought, not quite as expected, and certainly unpredictable.

SELF:nRowPointer +=1

IF SELF:nRowPointer > SELF:NumberOfRows

e:HasMorePages := FALSE

SELF:nRowPointer := 1

ELSE

e:HasMorePages := TRUE

ENDIF

We have stopped the exception happening, by not allowing the pointer to point to no existent rows, but why the results as we see them, I can't say. All I know from other Form 2.0 development is that you need to be very careful when you are taking change in your own code, of associating event handlers. Multiple triggering / execution will happen but the results aren't always what you expect.

Finally, it doesn't seem to matter if we disassociate, we still get the three pages we expected.

However, the strange results when we had two similar associations 'set' has given me an idea. I am going to do an experiment to see what is happening.

Before we go into any explanation, look at what comes out, then we will look at the code for testing :-

===============================================================================================================================

The middle page gives us a clue to what is going on. Since we know something about our data, we should recognise that Dr. Dilly Daydream is the last record and Rev Chonka the first one.

Leys see what code created this output, and then see if we can improve things :-

EXPORT lFirstPrint AS Boolean

.............................

.............................

oBD:Font := oFont22Ab

LOCAL oPGC AS PageCoords

LOCAL nDown AS INT

IF SELF:lFirstPrint

nDown := 0

ELSE

nDown := 13

ENDIF

oPGC := PageCoords{5+nDown,10}

oBD:TEXT(oPGC,oMM:Title)

oPGC := PageCoords{6+nDown,10}

oBD:TEXT(oPGC,oMM:Firstname)

oPGC := PageCoords{7+nDown,10}

oBD:TEXT(oPGC,oMM:Lastname)

oPGC := PageCoords{8+nDown,10}

oBD:TEXT(oPGC,oMM:Add1)

oPGC := PageCoords{9+nDown,10}

oBD:TEXT(oPGC,oMM:Add2)

oPGC := PageCoords{10+nDown,10}

oBD:TEXT(oPGC,oMM:Add3)

oPGC := PageCoords{11+nDown,10}

oBD:TEXT(oPGC,oMM:PCode)

//=============== end of test code ==================

SELF:nRowPointer +=1

IF SELF:nRowPointer > SELF:NumberOfRows

e:HasMorePages := FALSE

SELF:nRowPointer := 1

SELF:lFirstPrint := FALSE

ELSE

e:HasMorePages := TRUE

ENDIF

RETURN

=====================================================

Okay then, we have a class variable called 'lFisrtPrint' and this is initially set to true. As the 'MyTestDocument()' method closes the row pointer is incremented as previously. We no longer allow the row pointer to point at a row which does not exist, to remove problems with exception throwing.

However, we change the first print flag to false once we have been right through the data rows a first time. The reason I did this was because I thought the three pages were being printed on a 'second time'. I one sense they are , but not in order 1,2,3 then 1,2,3.

Looking at the results above, where the middle page is clear, it would look as though we are printing 1,1,2,2,3,3 as this would result in the third row from the first data set being on the same page as the first row from the second data set.

Look at the code above a little closer and you will see that when the second data set prints I use an off-set which moves everything to be printed down 13 lines. So again, this would suggest page one is all first data set, and page three all second data set. The middle is as we suspected, first the second.

What do we make of all of this then ?

Well, remember we have the same event handler 'set' twice, to be carried out when a PrintPage event happens. This now makes sense. A new page is started because the PrintPage event fires - then each associated event handler is executed in turn, before the event is taken to be completed.

I think I can change the code to remove the over-printing and to get one handler to do the name and address as initially tested, and another quite separate one to do some extra fields for the client in focus. Lets give it a go :-

Yes, we look as though we are understanding what is going on - great stuff.

I will show the code after a brief explanation. We now have two separate event handlers 'MyTestDocumentMM' and a 'MyDocumentMMpart2'. These are associated with the PrintPage event as shown below :-

............................

............................

CASE SELF:cDoc = "MM"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentMM()}

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentMMpart2()}

END CASE

Basically, the first handler is more or less as it was initially, it is the second one which we need to study :-

==================================================================

METHOD MyTestDocumentMMpart2(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

IF SELF:oMMDT = NULL_OBJECT

//----- warning -----

MessageBox.Show("Data Table is not instantiated !")

e:HasMorePages := FALSE

RETURN

ENDIF

//==========================================================

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oBD:SetGraphics(e:Graphics)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//---------------------------------------------------

//============ place your MM test code here ============

oBD:Font := oFont22Ab

LOCAL oPGC AS PageCoords

oPGC := PageCoords{25,10}

oBD:TEXT(oPGC,oMM:Telephone)

oPGC := PageCoords{26,10}

oBD:TEXT(oPGC,oMM:Email)

oPGC := PageCoords{27,10}

oBD:TEXT(oPGC,oMM:LastOrder:ToLongDateString())

oPGC := PageCoords{28,10}

oBD:TEXT(oPGC,oMM:DoB:ToLongDateString())

oPGC := PageCoords{29,10}

oBD:TEXT(oPGC,oMM:Owing:ToString())

oPGC := PageCoords{30,10}

oBD:TEXT(oPGC,oMM:Add3)

oPGC := PageCoords{31,10}

oBD:TEXT(oPGC,oMM:XmasList:ToString())

//=============== end of test code ==================

RETURN

=================================================================

As you can see, all we have done is place the extra fields of the data row, onto a low portion of the paper / page. We have not done any page throws as this has already been done in the first handler.

I would appear to me that this opens up the possibility of having different portions of the document handled separately by a specific event.

What I am thing of is the idea of printing some company logo or letter head detail, from a different method than that doing the letter content.

We will have to see how useful this turns out to be - however, it would appear that we can't print to pages which have already 'gone' ! By that I mean we can't chop back and forth between pages, adding bits here, and other bits there. We only get the one bite of the cherry at the page in focus - or the page whose PrintPage event is in progress. Any number of handlers will only place on the page in focus.

This is enough for one section, lets have a break and move on to seeing how we can improve our MM document, as well as use the data content to drive what actually gets printed.

PHH Sept. 2008


Data Driven Document Detail

Navigation: Section 15 - Mail Merging >

Data Driven Document Detail

 Return to chapter overview

Enter topic text here.


Using a Word 2007 document

Navigation: Section 15 - Mail Merging >

Using a Word 2007 document

 Return to chapter overview

Enter topic text here.


S15 Working Code

Navigation: Section 15 - Mail Merging >

S15 Working Code

 Return to chapter overview

//===========================================

#USING System.Windows.Forms

#USING System.Drawing

[STAThreadAttribute];

FUNCTION Start( asCmdLine AS STRING[] ) AS INT

LOCAL nExitCode AS INT

LOCAL oForm AS fmReportsAndPrinting

LOCAL oTBD AS fmTestBD

Application.EnableVisualStyles()

Application.DoEvents()

oTBD := fmTestBD{}

Application.Run(oTBD)

RETURN nExitCode

//===========================================

CLASS MailMerge

EXPORT Title AS STRING

EXPORT Firstname AS STRING

EXPORT Lastname AS STRING

EXPORT Add1 AS STRING

EXPORT Add2 AS STRING

EXPORT Add3 AS STRING

EXPORT PCode AS STRING

EXPORT Telephone AS STRING

EXPORT Email AS STRING

EXPORT DoB AS DateTime

EXPORT LastOrder AS DateTime

EXPORT XmasList AS Boolean

EXPORT Owing AS Double

CONSTRUCTOR()

SUPER()

SELF:Initialize()

RETURN

METHOD Initialize() AS VOID

Title := ""

Firstname := ""

Lastname := ""

Add1 := ""

Add2 := ""

Add3 := ""

PCode := ""

Telephone := ""

Email := ""

DoB := DateTime.MinValue

LastOrder := DateTime.MinValue

XmasList := FALSE

Owing := 0.0

RETURN

END CLASS

//===================================================

CLASS NewTestDocument

EXPORT oBD AS BestDocument

PROTECT Preview AS LOGIC

PROTECT oPreviewDlg AS PrintPreviewDialog

PROTECT oGraphics AS Graphics

PROTECT cDoc AS STRING

//---------------------------

EXPORT oMM AS MailMerge

EXPORT oMMDT AS DataTable

EXPORT nRowPointer AS INT

EXPORT NumberOfRows AS INT

EXPORT NumberOfColumns AS INT

EXPORT oDCC AS DataColumnCollection

EXPORT oDRC AS DataRowCollection

EXPORT lFirstPrint AS Boolean

EXPORT cPrinterName AS STRING

CONSTRUCTOR()

SUPER()

SELF:Initialize()

SELF:cDoc := "A"

RETURN

/*CONSTRUCTOR(cWhich AS STRING, oMMin AS MailMerge)

SUPER()

SELF:Initialize()

SELF:cDoc := cWhich

SELF:oMM := oMMin

RETURN */

CONSTRUCTOR(cWhich AS STRING)

SUPER()

SELF:Initialize()

SELF:cDoc := cWhich

RETURN

METHOD Initialize() AS VOID

SELF:Preview := TRUE

RETURN

METHOD SetUpDataTable(oDTin AS DataTable) AS VOID

oMMDT := oDTin

oDCC := SELF:oMMDT:Columns

SELF:NumberOfColumns := oDCC:Count

oDRC := SELF:oMMDT:Rows

SELF:NumberOfRows := oDRC:Count

SELF:nRowPointer := 1

RETURN

METHOD ShowDocument() AS VOID

//------ find default printer ------

//oBD := BestDocument{"TestShow","OKI C5600"}

oBD := BestDocument{"TestShow",SELF:cPrinterName}

oBD:LinesPerPage := 66

oBD:SetPageBoundaries()

SELF:SetAppropriateHandler()

SELF:lFirstPrint := TRUE

IF !SELF:Preview

oBD:Print()

ELSE

SELF:oPreviewDlg:=PrintPreviewDialog{}

SELF:oPreviewDlg:Document := SELF:oBD

SELF:oPreviewDlg:ShowDialog()

ENDIF

SELF:UnSetAppropriateHandler()

oBD:Dispose()

oBD := NULL_OBJECT

RETURN

METHOD SetAppropriateHandler() AS VOID

DO CASE

CASE SELF:cDoc = "A"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentA()}

CASE SELF:cDoc = "B"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentB()}

CASE SELF:cDoc = "C"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentC()}

CASE SELF:cDoc = "D"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentD()}

CASE SELF:cDoc = "E"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentE()}

CASE SELF:cDoc = "MM"

oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentMM()}

//oBD:PrintPage += PrintPageEventHandler{SELF , @MyTestDocumentMM()}

END CASE

RETURN

METHOD UnSetAppropriateHandler() AS VOID

DO CASE

CASE SELF:cDoc = "A"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentA()}

CASE SELF:cDoc = "B"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentB()}

CASE SELF:cDoc = "C"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentC()}

CASE SELF:cDoc = "D"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentD()}

CASE SELF:cDoc = "E"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentE()}

CASE SELF:cDoc = "MM"

oBD:PrintPage -= PrintPageEventHandler{SELF , @MyTestDocumentMM()}

END CASE

RETURN

METHOD MyTestDocument(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//oBD:Font := oFont16A

//oBD:GetLeftDecimal("12.34")

//oBD:GetDecimalParts("0.789")

//oBD:GetDecimalParts("-567.07890")

LOCAL oPGC AS PageCoords

LOCAL oGC AS GraphCoords

LOCAL oPPC AS PaperCoords

LOCAL cText AS STRING

LOCAL aWords AS STRING[]

LOCAL alWords AS ArrayList

cText := "tom, dick, and harry were here!"

alWords := oBD:WordsList(cText)

aWords := (STRING[])alWords:ToArray(typeof(STRING))

LOCAL i AS INT

cText := "Number of words :- "+CRLF+CRLF

FOR i := 1 UPTO aWords:Length

cText += aWords[i]+CRLF

NEXT

//Messagebox.Show(cText)

oPGC := PageCoords{0, 0}

cText := "First Under Line is here ...."

//oBD:ColTextLeft(oPGC, cText, 39)

//oBD:ColTextRight(oPGC, cText, 39)

//oBD:ColTextCenter(oPGC, cText, 39)

//cText := "-12.345"

//oBD:ColTextdecimal(oPGC, cText, 3, 39)

cText := "0.345"

cText := "-567.07890"

oBD:ColTextDecimal(oPGC, cText, 4, 39)

/*oBD:Font := oFont12A

oBD:Brush := SolidBrush{Color.Black}

oBD:Pen := Pen{oBD:Brush, 1.0}

oPGC := PageCoords{0, 0}

cText := "First Under Line is here ...."

//oBD:TextUnder(oPGC, cText)

oBD:TextCenter(oPGC, cText)

oBD:Font := oFont22Ab

oBD:Brush := SolidBrush{Color.Blue}

oBD:Pen := Pen{oBD:Brush, 2.0}

oPGC := PageCoords{7, 0}

cText := "HELLO Alwyn !"

oBD:Text(oPGC, cText)

oPGC := PageCoords{9, 54}

cText := "Hello Alwyn !"

oBD:TextUp(oPGC, cText)

oBD:Brush := SolidBrush{Color.Green}

oBD:Pen := Pen{oBD:Brush, 2.0} */

/*oBD:Font := oFont12A

oPGC := PageCoords{9, 30}

cText := "This is going to be a long bit of prose to check out my new memo place method. "

cText += "Second line of trial test text is now being typed at the keyboard just to see what is what!"

//oBD:MemoPlace(oPGC, cText, 42)

oBD:MemoPlaceFullyJustify(oPGC, cText, 42)

LOCAL aLines AS STRING[]

aLines := oBD:Memo2Lines(cText,42)

//Messagebox.Show(aLines:Length:ToString())

//Messagebox.Show(aLines[1]+CRLF+aLines[2]+CRLF+aLines[3])

//Messagebox.Show(aLines[1]+CRLF+aLines[2])

oPGC := PageCoords{17, 3}

cText := "We are on our way !!"

oBD:Text(oPGC, cText)

oBD:Brush := SolidBrush{Color.Red}

oBD:Pen := Pen{oBD:Brush, 2.0}

oPGC := PageCoords{21, 6}

cText := ">> I hope to have this ready for DevFest 2009 <<"

oBD:Text(oPGC, cText) */

/* oPGC := PageCoords{24, 75}

cText := ">> I hope to have PAGE right for DevFest 2009 <<"

oBD:TextRight(oPGC, cText)

oGC := GraphCoords{75, 43}

cText := ">> I hope to have GRAPH right for DevFest 2009 <<"

oBD:TextRight(oGC, cText)

oPPC := PaperCoords{44, 62}

cText := ">> I hope to have PAPER right for DevFest 2009 <<"

oBD:TextRight(oPPC, cText)

oBD:Brush := SolidBrush{Color.Black}

oBD:Pen := Pen{oBD:Brush, 2.0}

oPGC := PageCoords{52,28, 9}

//oPGC := PageCoords{28, 9}

cText := "Yipee - it works :O)..."

oBD:TextCenterUnder(oPGC, cText)

oPPC := PaperCoords{55,30}

cText := "Yipee 2 - it works again :0)"

oBD:TextCenterUnder(oPPC, cText)

oGC := GraphCoords{10,15}

cText := "Yipee 3 - it works trice :O)"

oBD:TextCenterUnder(oGC, cText)

*/

/* oGC := GraphCoords{5,10}

cText := "Yipee 3 - it works trice :O)"

oBD:TextUnder(oGC, cText)

oBD:DrawLine(PaperCoords{80,30}, PaperCoords{80,80}, 2)

oBD:DrawLine(GraphCoords{10,5}, GraphCoords{90,5}, 2)

LOCAL aText AS STRING[]

aText := <STRING>{"Thomas Gething","Dick Whittington","Harry Harrison"}

oBD:Font := oFont12A

oPGC := PageCoords{48, 9}

//oBD:Block(oPGC, aText)

oBD:BlockRestrictRHS(oPGC, aText, 32)

aText := <STRING>{"Sally","Jane","Freda"}

oBD:Font := oFont16A

oPGC := PageCoords{48, 42}

oBD:BlockUnder(oPGC, aText)

aText := <STRING>{"Thomas Jefferson","Dick Whittington","Harry Harrison","Magnus Magnusson"}

oBD:Font := oFont12A

oPPC := PaperCoords{50, 3}

//oBD:Block(oPPC, aText, 0.9)

//oBD:Block(oPPC, aText)

//oBD:BlockRestrictRHS(oPPC, aText, 16)

oBD:BlockRestrictRHS(oPPC, aText, 25, 0.85) */

/* LOCAL oPoint AS Point

oPoint := Point{13,13} */

//oPGC := PageCoords{13,13}

//oBD:VLineSet(oPGC, 5, 10, 39)

//oBD:HLineSet(oPGC, 5, 5, 59)

/*oPPC := PaperCoords{33,13}

oBD:VLineSet(oPPC, 5, 10, 59)

oBD:HLineSet(oPPC, 5, 5, 59) */

/*oGC := GraphCoords{13, 67}

oBD:VLineSet(oGC, 5, 10, 59)

oBD:HLineSet(oGC, 5, 5, 59) */

LOCAL oPointTL AS PointF

LOCAL oPointBR AS PointF

LOCAL N, M AS INT

oPointTL := PointF{20,40}

oPointBR := PointF{80,60}

LOCAL oPGC1, oPGC2 AS PageCoords

LOCAL oPPC1, oPPC2 AS PaperCoords

LOCAL oGC1, oGC2 AS GraphCoords

oPGC1 := PageCoords{10,10}

oPGC2 := PageCoords{50,90}

oPPC1 := PaperCoords{10,10}

oPPC2 := PaperCoords{90,90}

oGC1 := GraphCoords{10,90}

oGC2 := GraphCoords{90,10}

N := 9

M := 11

//oBD:GridRegularNbyM(oPGC1, oPGC2, N, M)

//oBD:GridRegularNbyM(oPPC1, oPPC2, N, M)

//oBD:GridRegularNbyM(oGC1, oGC2, N, M)

//oBD:VLineSetComplex(oPGC1, <Double>{9,11,13,15,17}, 60)

/*oPGC1 := PageCoords{20,10}

oBD:HLineSetComplex(oPGC1, <Double>{5,7,9,11,13}, 75) */

oPGC1 := PageCoords{10,10}

oBD:GridComplex(oPGC1,<Double>{3,5,7,9,11,9,7,5,3},<Double>{1,3,5,7,5,3,2,3,5,7,5,3,1})

//oGC1 := GraphCoords{10,86}

oPGC1 := PageCoords{10,50}

oBD:BitmapPlaceAtBottomLeft(oPGC1,"c:\vdnuk\fatherX\fatherX1.jpg")

//oBD:BitmapPlaceScaled(oGC1,"c:\vdnuk\fatherX\fatherX1.jpg", 0.85)

oPGC1 := PageCoords{30,10}

oBD:BitmapPlace(oPGC1,"c:\vdnuk\fatherX\fatherX2.jpg")

oPGC1 := PageCoords{50,10}

oBD:BitmapPlace(oPGC1,"c:\vdnuk\fatherX\fatherX3.jpg")

oPGC1 := PageCoords{10,30}

//oBD:BitmapPlaceScaled(oPGC1,"c:\vdnuk\fatherX\fatherX4.jpg",1.0)

//oBD:BitmapPlaceTransparent(oPGC1,"c:\vdnuk\fatherX\fatherX4.jpg")

//oBD:BitmapPlace(oPGC1,"c:\vdnuk\fatherX\fatherX4.jpg")

oBD:BitmapPlaceTransparent(oPGC1,"c:\vdnuk\fatherX\fatherX4.bmp", Color.White)

//oBD:BitmapPlaceTransparent(oPGC1,"c:\vdnuk\fatherX\fatherX4.bmp", Color.Red)

oPGC1 := PageCoords{30,30}

oBD:BitmapPlace(oPGC1,"c:\vdnuk\fatherX\fatherX5.jpg")

oPGC1 := PageCoords{50,30}

oBD:BitmapPlace(oPGC1,"c:\vdnuk\fatherX\fatherX6.jpg")

oPGC1 := PageCoords{10,50}

oBD:BitmapPlace(oPGC1,"c:\vdnuk\fatherX\fatherX7.jpg")

oPPC1 := PaperCoords{44,50}

oBD:BitmapPlaceScaled(oPPC1,"c:\vdnuk\fatherX\fatherX8.jpg",1.2)

oPGC1 := PageCoords{50,50}

oBD:BitmapPlaceAtBottomLeft(oPGC1,"c:\vdnuk\fatherX\fatherX9.jpg")

oPGC1 := PageCoords{40,30}

oBD:BitmapPlaceScaled(oPGC1,"c:\vdnuk\fatherX\fatherX10.jpg", 0.7)

oPGC1 := PageCoords{66,50}

oBD:BitmapPlaceAtBottomLeft(oPGC1,"c:\vdnuk\fatherX\fatherX1.jpg")

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

//oBD:ResetLinesPerPage(56)

RETURN

METHOD MyTestDocumentA(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentB(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentC(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentD(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oGraphics := e:Graphics

oBD:SetGraphics(oGraphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//------------------------------------------------

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentE(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oBD:SetGraphics(e:Graphics)

oBD:ResetLinesPerPage(67)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//---------------------------------------------------

//============ place your test code here ============

LOCAL oPGC1, oPGC2 AS PageCoords

LOCAL N, M AS INT

N := 9

M := 11

oPGC1 := PageCoords{10,10}

oBD:GridComplex(oPGC1,<Double>{3,5,7,9,11,9,7,5,3},<Double>{1,3,5,7,5,3,2,3,5,7,5,3,1})

//=============== end of test code ==================

//------------ keep this for a single page -----------

e:HasMorePages := FALSE

RETURN

METHOD MyTestDocumentMM2(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

IF SELF:oMMDT = NULL_OBJECT

//----- warning -----

MessageBox.Show("Data Table is not instantiated !")

e:HasMorePages := FALSE

RETURN

ENDIF

//==========================================================

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oBD:SetGraphics(e:Graphics)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//---------------------------------------------------

//============ place your MM test code here ============

oBD:ResetLinesPerPage(36)

SELF:GetMakeMMobject(SELF:nRowPointer) //----- make relevant oMM object -----

oBD:Font := oFont22Ab

LOCAL oPGC AS PageCoords

LOCAL nDown AS INT

IF SELF:lFirstPrint

nDown := 0

ELSE

nDown := 13

ENDIF

oPGC := PageCoords{5+nDown,10}

oBD:Text(oPGC,oMM:Title)

oPGC := PageCoords{6+nDown,10}

oBD:Text(oPGC,oMM:Firstname)

oPGC := PageCoords{7+nDown,10}

oBD:Text(oPGC,oMM:Lastname)

oPGC := PageCoords{8+nDown,10}

oBD:Text(oPGC,oMM:Add1)

oPGC := PageCoords{9+nDown,10}

oBD:Text(oPGC,oMM:Add2)

oPGC := PageCoords{10+nDown,10}

oBD:Text(oPGC,oMM:Add3)

oPGC := PageCoords{11+nDown,10}

oBD:Text(oPGC,oMM:PCode)

//=============== end of test code ==================

SELF:nRowPointer +=1

IF SELF:nRowPointer > SELF:NumberOfRows

e:HasMorePages := FALSE

SELF:nRowPointer := 1

SELF:lFirstPrint := FALSE

ELSE

e:HasMorePages := TRUE

ENDIF

RETURN

METHOD MyTestDocumentMM(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

IF SELF:oMMDT = NULL_OBJECT

//----- warning -----

MessageBox.Show("Data Table is not instantiated !")

e:HasMorePages := FALSE

RETURN

ENDIF

//==========================================================

LOCAL oFont, oFont9CS, oFont11CS, oFont10A, oFont12A AS Font

LOCAL oFont12Ab, oFont8A, oFont22CS, oFont16A, oFont22Ab AS Font

oBD:SetGraphics(e:Graphics)

oFont22Ab := Font{"Arial",22,FontStyle.Bold}

oFont16A := Font{"Arial",16}

oFont12A := Font{"Arial",12}

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

//---------------------------------------------------

//============ place your MM test code here ============

oBD:ResetLinesPerPage(36)

SELF:GetMakeMMobject(SELF:nRowPointer) //----- make relevant oMM object -----

oBD:Font := oFont22Ab

LOCAL oPGC AS PageCoords

oPGC := PageCoords{5,10}

oBD:Text(oPGC,oMM:Title)

oPGC := PageCoords{6,10}

oBD:Text(oPGC,oMM:Firstname)

oPGC := PageCoords{7,10}

oBD:Text(oPGC,oMM:Lastname)

oPGC := PageCoords{8,10}

oBD:Text(oPGC,oMM:Add1)

oPGC := PageCoords{9,10}

oBD:Text(oPGC,oMM:Add2)

oPGC := PageCoords{10,10}

oBD:Text(oPGC,oMM:Add3)

oPGC := PageCoords{11,10}

oBD:Text(oPGC,oMM:PCode)

//=============== end of test code ==================

SELF:nRowPointer +=1

IF SELF:nRowPointer > SELF:NumberOfRows

e:HasMorePages := FALSE

SELF:nRowPointer := 1

ELSE

e:HasMorePages := TRUE

ENDIF

RETURN

METHOD GetMakeMMobject(nRow AS INT) AS VOID

LOCAL oDR AS DataRow

oDR := SELF:oDRC:Item[nRow-1]

oMM := MailMerge{}

oMM:Title := (STRING)oDR:Item[0]

oMM:Firstname := (STRING)oDR:Item[1]

oMM:Lastname := (STRING)oDR:Item[2]

oMM:DoB := (DateTime)oDR:Item[3]

oMM:LastOrder := (DateTime)oDR:Item[4]

oMM:XmasList := (Boolean)oDR:Item[5]

oMM:Owing := (Double)oDR:Item[6]

oMM:Add1 := (STRING)oDR:Item[7]

oMM:Add2 := (STRING)oDR:Item[8]

oMM:Add3 := (STRING)oDR:Item[9]

oMM:PCode := (STRING)oDR:Item[10]

oMM:Telephone := (STRING)oDR:Item[11]

oMM:Email := (STRING)oDR:Item[12]

RETURN

END CLASS

//==============================================================

CLASS fmTestBD INHERIT System.Windows.Forms.Form

PROTECT ocbPrinters AS System.Windows.Forms.ComboBox

PROTECT opbTestDocMM AS System.Windows.Forms.Button

PROTECT olbPrintersInstalled AS System.Windows.Forms.Label

PROTECT opbTestDocE AS System.Windows.Forms.Button

PROTECT opbTestDocD AS System.Windows.Forms.Button

PROTECT opbTestDocC AS System.Windows.Forms.Button

PROTECT opbTestDocB AS System.Windows.Forms.Button

PROTECT opbTestDocA AS System.Windows.Forms.Button

PROTECT ogbModeOfPrinting AS System.Windows.Forms.GroupBox

PROTECT orbDirectToPrinter AS System.Windows.Forms.RadioButton

PROTECT orbViewPreviewDialog AS System.Windows.Forms.RadioButton

PROTECT opbOK AS System.Windows.Forms.Button

// User code starts here (DO NOT remove this line) ##USER##

EXPORT oMM AS MailMerge

EXPORT oMMDT AS DataTable

EXPORT cFocusXML AS STRING

EXPORT cFocusXSD AS STRING

EXPORT cPrinterName AS STRING

CONSTRUCTOR()

SUPER()

SELF:InitializeForm()

SELF:Initialize()

RETURN

METHOD Initialize() AS VOID

SELF:orbViewPreviewDialog:Checked := TRUE

SELF: FindPrinters()

RETURN

METHOD FindPrinters() AS VOID

//================ find and set printer(s) =================

LOCAL cDefaultPrinter AS STRING

LOCAL oPS AS PrinterSettings

oPS := PrinterSettings{}

cDefaultPrinter := oPS:PrinterName

SELF:cPrinterName := cDefaultPrinter

LOCAL acPrinters AS STRING[]

LOCAL oPSSC AS PrinterSettings.StringCollection

oPSSC := PrinterSettings.InstalledPrinters

acPrinters := STRING[]{oPSSC:Count}

oPSSC:CopyTo(acPrinters,0)

SELF:ocbPrinters:Items:Clear()

SELF:ocbPrinters:Items:AddRange(acPrinters)

SELF:ocbPrinters:SelectedItem := cDefaultPrinter

// ==========================================================

RETURN

METHOD InitializeForm() AS VOID

// IDE generated code (please DO NOT modify)

SELF:Name := "fmTestBD"

SELF:SuspendLayout()

SELF:Location := System.Drawing.Point{100,100}

SELF:ClientSize := System.Drawing.Size{410,334}

SELF:Text := "Test Best Document class :"

SELF:ocbPrinters := System.Windows.Forms.ComboBox{}

SELF:ocbPrinters:Name := "cbPrinters"

SELF:ocbPrinters:Location := System.Drawing.Point{ 192 , 176 }

SELF:ocbPrinters:Size := System.Drawing.Size{ 200 , 21 }

SELF:ocbPrinters:SelectionChangeCommitted += System.EventHandler{ SELF , @cbPrintersSelectionChangeCommitted() }

SELF:ocbPrinters:TabIndex := 7

SELF:Controls:Add(SELF:ocbPrinters)

SELF:opbTestDocMM := System.Windows.Forms.Button{}

SELF:opbTestDocMM:Name := "pbTestDocMM"

SELF:opbTestDocMM:Location := System.Drawing.Point{ 16 , 296 }

SELF:opbTestDocMM:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocMM:Text := "Test Doc MM"

SELF:opbTestDocMM:Click += System.EventHandler{ SELF , @pbTestDocMMClick() }

SELF:opbTestDocMM:TabIndex := 9

SELF:Controls:Add(SELF:opbTestDocMM)

SELF:olbPrintersInstalled := System.Windows.Forms.Label{}

SELF:olbPrintersInstalled:Name := "lbPrintersInstalled"

SELF:olbPrintersInstalled:Location := System.Drawing.Point{ 192 , 152 }

SELF:olbPrintersInstalled:Size := System.Drawing.Size{ 144 , 23 }

SELF:olbPrintersInstalled:Text := "Printers Installed :"

SELF:olbPrintersInstalled:TabIndex := 8

SELF:Controls:Add(SELF:olbPrintersInstalled)

SELF:opbTestDocE := System.Windows.Forms.Button{}

SELF:opbTestDocE:Name := "pbTestDocE"

SELF:opbTestDocE:Location := System.Drawing.Point{ 16 , 176 }

SELF:opbTestDocE:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocE:Text := "Test Doc E"

SELF:opbTestDocE:Click += System.EventHandler{ SELF , @pbTestDocEClick() }

SELF:opbTestDocE:TabIndex := 6

SELF:Controls:Add(SELF:opbTestDocE)

SELF:opbTestDocD := System.Windows.Forms.Button{}

SELF:opbTestDocD:Name := "pbTestDocD"

SELF:opbTestDocD:Location := System.Drawing.Point{ 16 , 136 }

SELF:opbTestDocD:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocD:Text := "TestDoc D"

SELF:opbTestDocD:Click += System.EventHandler{ SELF , @pbTestDocDClick() }

SELF:opbTestDocD:TabIndex := 5

SELF:Controls:Add(SELF:opbTestDocD)

SELF:opbTestDocC := System.Windows.Forms.Button{}

SELF:opbTestDocC:Name := "pbTestDocC"

SELF:opbTestDocC:Location := System.Drawing.Point{ 16 , 96 }

SELF:opbTestDocC:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocC:Text := "Test Doc C"

SELF:opbTestDocC:Click += System.EventHandler{ SELF , @pbTestDocCClick() }

SELF:opbTestDocC:TabIndex := 4

SELF:Controls:Add(SELF:opbTestDocC)

SELF:opbTestDocB := System.Windows.Forms.Button{}

SELF:opbTestDocB:Name := "pbTestDocB"

SELF:opbTestDocB:Location := System.Drawing.Point{ 16 , 56 }

SELF:opbTestDocB:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocB:Text := "Test Doc B"

SELF:opbTestDocB:Click += System.EventHandler{ SELF , @pbTestDocBClick() }

SELF:opbTestDocB:TabIndex := 3

SELF:Controls:Add(SELF:opbTestDocB)

SELF:opbTestDocA := System.Windows.Forms.Button{}

SELF:opbTestDocA:Name := "pbTestDocA"

SELF:opbTestDocA:Location := System.Drawing.Point{ 16 , 16 }

SELF:opbTestDocA:Size := System.Drawing.Size{ 136 , 23 }

SELF:opbTestDocA:Text := "Test Doc A"

SELF:opbTestDocA:Click += System.EventHandler{ SELF , @pbTestDocAClick() }

SELF:opbTestDocA:TabIndex := 2

SELF:Controls:Add(SELF:opbTestDocA)

SELF:ogbModeOfPrinting := System.Windows.Forms.GroupBox{}

SELF:ogbModeOfPrinting:Name := "gbModeOfPrinting"

SELF:ogbModeOfPrinting:SuspendLayout()

SELF:ogbModeOfPrinting:Location := System.Drawing.Point{ 192 , 16 }

SELF:ogbModeOfPrinting:Size := System.Drawing.Size{ 200 , 100 }

SELF:ogbModeOfPrinting:Text := "Mode of Printing :"

SELF:ogbModeOfPrinting:TabIndex := 1

SELF:Controls:Add(SELF:ogbModeOfPrinting)

SELF:orbDirectToPrinter := System.Windows.Forms.RadioButton{}

SELF:orbDirectToPrinter:Name := "rbDirectToPrinter"

SELF:orbDirectToPrinter:Location := System.Drawing.Point{ 32 , 24 }

SELF:orbDirectToPrinter:Size := System.Drawing.Size{ 136 , 24 }

SELF:orbDirectToPrinter:Text := "Direct to Printer :"

SELF:orbDirectToPrinter:TabIndex := 0

SELF:ogbModeOfPrinting:Controls:Add(SELF:orbDirectToPrinter)

SELF:orbViewPreviewDialog := System.Windows.Forms.RadioButton{}

SELF:orbViewPreviewDialog:Name := "rbViewPreviewDialog"

SELF:orbViewPreviewDialog:Location := System.Drawing.Point{ 32 , 56 }

SELF:orbViewPreviewDialog:Size := System.Drawing.Size{ 144 , 24 }

SELF:orbViewPreviewDialog:Text := "View Preview Dialog :"

SELF:orbViewPreviewDialog:TabIndex := 1

SELF:ogbModeOfPrinting:Controls:Add(SELF:orbViewPreviewDialog)

SELF:opbOK := System.Windows.Forms.Button{}

SELF:opbOK:Name := "pbOK"

SELF:opbOK:Location := System.Drawing.Point{ 328 , 304 }

SELF:opbOK:Size := System.Drawing.Size{ 75 , 23 }

SELF:opbOK:Text := "OK"

SELF:opbOK:Click += System.EventHandler{ SELF , @pbOKClick() }

SELF:opbOK:TabIndex := 0

SELF:Controls:Add(SELF:opbOK)

SELF:ogbModeOfPrinting:ResumeLayout()

SELF:ResumeLayout()

RETURN

METHOD cbPrintersSelectionChangeCommitted(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:cPrinterName := SELF:ocbPrinters:SelectedItem:ToString()

RETURN

METHOD pbTestDocMMClick(o AS OBJECT , e AS System.EventArgs) AS VOID

//----- set up MM DataTable object -----

SELF:MakeMailMergeDataTable()

SELF:DoDisplayDoc("MM")

RETURN

METHOD MakeMailMergeDataTable() AS VOID

//=================== My Testing =========================

SELF:cFocusXML := "C:\VDNUK\FileMultiConverter\FMCmailmerge.XML"

SELF:cFocusXSD := "C:\VDNUK\FileMultiConverter\FMCmailmerge.XSD"

SELF:AutoLoadXMLpair()

RETURN

METHOD AutoLoadXMLpair() AS VOID

LOCAL cFileD, cFileS, cFolder, cCaption AS STRING

LOCAL cTableName, cPathFileD, cPathFileS AS STRING

LOCAL oDCC AS DataColumnCollection

LOCAL oDRC AS DataRowCollection

LOCAL NumberOfColumns, NumberOfRows AS INT

cFileD := SELF:cFocusXML

cFileS := SELF:cFocusXSD

IF File.Exists(cFileD)

//MessageBox.Show("Data as XML exists!")

ELSE

MessageBox.Show("File NOT found :- "+cFileD)

ENDIF

IF File.Exists(cFileS)

//MessageBox.Show("Schema for XML exists!")

ELSE

MessageBox.Show("File NOT found :- "+cFileS)

ENDIF

SELF:oMMDT := DataTable{}

SELF:oMMDT:ReadXmlSchema(cFileS)

cTableName := SELF:oMMDT:TableName

//MessageBox.Show(cTableName:ToString())

cCaption := SELF:Text

SELF:Text := cCaption+" Reopting on : "+cTableName:ToString()

SELF:oMMDT:ReadXml(cFileD)

oDCC := SELF:oMMDT:Columns

NumberOfColumns := oDCC:Count

//MessageBox.Show("Number of Columns : "+NumberOfColumns:ToString())

oDRC := SELF:oMMDT:Rows

NumberOfRows := oDRC:Count

//MessageBox.Show("Number of Rows : "+NumberOfRows:ToString())

RETURN

METHOD pbTestDocAClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("A")

RETURN

METHOD DoDisplayDoc(cWhich AS STRING) AS VOID

LOCAL oNTD AS NewTestDocument

oNTD := NewTestDocument{cWhich}

IF cWhich == "MM"

oNTD:SetUpDataTable(oMMDT)

ENDIF

oNTD:cPrinterName := SELF:cPrinterName

oNTD:ShowDocument()

oNTD := NULL_OBJECT

RETURN

METHOD pbTestDocDClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("D")

RETURN

METHOD pbTestDocCClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("C")

RETURN

METHOD pbTestDocBClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("B")

RETURN

METHOD pbTestDocEClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoDisplayDoc("E")

RETURN

METHOD pbOKClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:Close()

RETURN

END CLASS

//==================================================================

PHH - Sept. 2008.


Section 16 - printing 'outside of the box' !

Navigation: »No topics above this level«

Section 16 - printing 'outside of the box' !

 Return to chapter overview

What is this all about ?

Well, put quite simply, we say today "thinking outside of the box" as an expression which is difficult to put into other words.

If we are talking about printing, we may say "there is more than one way to skin a cat" ....

If you like, here is a way to make a quick cheat, when it comes to printing.

Below is a real live example of something I needed to do in Visual Objects. We needed a "Goods Return Note" document which was to be only used for items returned for some reason. Needless to say this does not happen very often, and the need for such a document is infrequent, and just a tiny fraction of all the other documents printed automatically by the network system.

For this reason, I realised I could take quite a different approach to solve the problem, do it quickly, and keep the IT manager happy as the look of the document was good. Remember this is an internal document anyway.

Look at the GRN below, this is actually a bitmap :-

The idea is that since in VO I can print directly from bitmap file on disk (courtesy of IPicture library from Michael Rubinstein) then we could us another software tool (in this case Excel) to prepare the blank form, or template. At the time we wish to print the document the bitmap is printed first, and then any text is over-printed.

Look at the results below :-

Obviously, it is helpful to get a good image, and to make sure that it is square on the scanner (if indeed you need to scan), and remember that JPegs are only made after scanning in at least grey scales. Nor black and white.

The last task you need to do is line up the text to the bitmap grid, which is not difficult if you have suitable tools available like those in the BestDocument class.

It would be easier to make a better job of this in Vulcan than the one we see from VO. The printing class have improved. I have better control over the coordinates used etc. etc.

Just don't forget this as an option, the file size is not now as important as it was, as networks are much better in performance, and all the rest of the hardware specification has shot up too.

Also, if it is to only have limited use, then quickness of design and implementation is important - saves money :-)

PHH - Sept. 2008


Microsoft - Reporting Services

Navigation: »No topics above this level«

Microsoft - Reporting Services

 Return to chapter overview

Enter topic text here.


Report Viewer control Forms 2.0

Navigation: Microsoft - Reporting Services >

Report Viewer control Forms 2.0

 Return to chapter overview

Enter topic text here.


Appendix A - Printers

Navigation: »No topics above this level«

Appendix A - Printers

 Return to chapter overview

Put simply, you can't print without a printer.

Even if you wish to show output on the screen in the Print Preview dialog, you first need to specify an installed printer.

It is the printer driver software which tells the dotNet classes, related to the 'PrintDocument' class, what the canvas dimensions are for the GDI+ classes to use for composing each page.

So if we want to control our printing, we need to know a few important basics about printers, and how the dotNet system deals with printers.

This is one area in which dotNet has made life much, much, easier for the standard guy wishing to handle his/her own printing.

When I did my Visual Objects printing I needed to use some code for first finding, then handling the printers, which was API in style - and emailed to me from another country. To this day I do not know what it is doing, even though it has been used possibly millions of times, and keeps on going.

I hear you say, what is wrong with that ? There is loads of code I use and don't understand.

Well, the answer is simple - when you hit a problem, and need to solve it, it helps a great deal to be able to adapt the code in front of you. You can't change code if you don't first fully understand it. Also, now that I have Vulcan and dotNet, I wish to leave API stuff where it belongs :-)

Now that we are going to multiple bins (or trays), duplexing, all colour printing etc. etc. we need as much control of our printers as we can muster.

The first thing that 'sold' dotNet to me happened some 18 months back, when I knew nothing, and found that in a couple of days effort I could find out for myself, all the Windows printers available, to and put these in a list box / combo box. Okay, Chris Prygas did help push me in the right direction, but it was a great step forward even so.

From then on I never looked back.

Let us now look at some code to find our Windows printers and make them available to a user in a GUI control :-

EXPORT ocbPrinters AS System.Windows.Forms.ComboBox

...................................................

...................................................

//================ find and set printer(s) =================

LOCAL acPrinters AS STRING[]

LOCAL oPSSC AS PrinterSettings.StringCollection

oPSSC := PrinterSettings.InstalledPrinters

acPrinters := STRING[]{oPSSC:Count}

oPSSC:CopyTo(acPrinters,0)

SELF:ocbPrinters:Items:Clear()

SELF:ocbPrinters:Items:AddRange(acPrinters)

SELF:ocbPrinters:SelectedItem := SELF:oOwner:cDefaultPrinter

//==========================================================

Here in just a few lines, we have found all installed Windows printers, and after clearing out our combo box, added them all to the drop down list control.

VERY nice indeed !

============================================================

If you have fought with printers in the API and VO days you will now appreciate that an installed printer is simply referred to by its name. A single string of characters. This string is simply the name as seen in the Printers GUI from the Control Panel as shown below :-

You will notice the 'OKI C5600' that I have been using in my code and writings in previous sections.

So in true scientific tradition, let us look at this screen and predict what else would work.

"Epson Stylus Photo RX500 (M)" is the string as got from the properties box below, this should plug straight into our code and work. Do take care that you get the string exactly as it is, don't miss off any brackets, or other characters. Gone are those days of needing three different parameters - just the name is sufficient.

PHH - Sept. 2008.


Appendix B - Colors, Pens and Brushes

Navigation: »No topics above this level«

Appendix B - Colors, Pens and Brushes

 Return to chapter overview

For your information, Color is a structure, and Font, Pen and Brush a class.

I suggest you read the following sub-section to know what the details are as regards 'Class V Structure'.

Let me say that in these notes we will do sufficient with all of these for good printing, but that if you wish to take things much further then you will need to acquire and read the ClickStart notes on viewing images, and doing much more with imagery generally.

Colors :

There are sufficient defined colors for most of us to manage quite well without thinking of making any more.

Lets us look at the ways we can create a color for general use :-

LOCAL oColor AS Color

oColor := Color.FromName("SlateBlue")

The SDK uses SlateBlue in one example I read, but there are a few in the list as displayed above.

Notice how the static method 'FromName()' allows easy use to make a colour structure.

Another common code snippet you will see is of the form :-

LOCAL oColor AS Color

oColor := Color.Red

And of course this does not stop you having colors in an array, as shown below (also from ClickStartVDN) :-

=========================================================================================================

For more detail on the use of colour look up ClickStartGDI+ and how colours are used with imagery as well as more complex printing and painting.

Pens :

Pen objects are initiated from a visual effect and a width. So in its simplest, and possibly purest form, the pen object can be created like this :-

LOCAL oPen AS Pen

oPen := Pen{Color.Blue, 3}

That is a color structure and a 'nib' width. This can also be done by supplying a user created color structure, as below :-

LOCAL oPen AS Pen

LOCAL oColor AS Color

oColor := Color.Red

oPen := Pen{oColor, 3}

However, it is also possible to create a pen object from a brush, as follows :-

LOCAL oPen AS Pen

LOCAL oColor AS Color

LOCAL oBrush AS SolidBrush

oColor := Color.Red

oBrush := SolidBrush{oColor}

oPen := Pen{oBrush, 3}

[ *note* the nib width is real and so can be 0.33 if you require it ]

Think of the brush as the colour, pattern and effect - all rolled into one - the all you also need is the width of the pen nib.

Before we move on to look at brushes, let us see that the pen can have a 'DashStyle' property, and that this may be changed.

As well as a DashStyle property there is also an EndCap and StartCap property to investigate.

====================================================================================================

A word of WARNING - don't by mistake look at the 'Pens' class in the SDK, as it has this to say :-

>> The Pen objects returned by this class are immutable, meaning their properties cannot be changed. <<

This is not true for the 'Pen' class (without the 's') - so be warned. As you can see above we certainly can change the property, but not all properties as we would think of them. I think width and brush / color will be immutable - as they put it.

We better move on to look at brushes in a little more detail, but do remember to read the ClickStartGDI+ notes for much more detail.

Brushes :

Okay then, lets try to sort out a few more wrinkles ;-)

A brush object can be made from one of a number of brushes - or types of brush. The simplest is the 'SolidBrush', and this has a color.

In the examples above we did not specify a solid brush for the purple and red lines shown in the examples, but could have written code such as :-

LOCAL oPen AS Pen

LOCAL oBrush AS SolidBrush

oBrush := SolidBrush{Color.Purple}

oPen := Pen{oBrush}

If we then want a pattern of some description we can choose from the set patterns in the 'HatchBrush' class :-

Since our pattern, or effect, is much more complex than a solid colour, we need to specify two color structures and a hatch style.

The hatch style comes from the HatchStyle enumeration :-

[ *** not all possibilities are shown ]

Since it is not the brief of these RnP notes to cover in depth the Brush and Brushes class I refer you to both the ClickStartVDN notes, as well as the ClickStartGDI+ notes.

What you need to be looking for are the Texture and Gradient brushes. This last class covers both the linear gradient brush and the path gradient brush.

These can really impress - go have a look !

PHH - October 2008.


Class 'v' Structure - Team Tips

Navigation: Appendix B - Colors, Pens and Brushes >

Class 'v' Structure - Team Tips

 Return to chapter overview

Here are some very instructive comments / notes from Paul Piko on the matter :-

A .NET structure is like a class on a diet. Normally an instance of a structure is created on the stack, it is a "value type", whereas an instance of a class is on the heap, a "reference type".

Creating a structure instance doesn't trigger the garbage collector but creating a class instance can. Structures are normally used for representing basic, light-weight objects, and cannot be inherited from. A structure can't have protected/abstract/virtual/sealed members.

Classes then have a performance overhead relating to the heap allocation, object access and garbage collection which structures do not share.

So, I assume, the framework designers chose to make Color a structure, trading off the flexibility of a class against the potential memory/speed improvements.

A summary of Microsoft's recommendations are:

A structure can be preferable when:

* You have a small amount of data

* You perform a large number of operations on each instance and would incur performance degradation with heap management

* You have no need to inherit the structure or to specialize functionality among its instances

A class is preferable when:

* You need to use inheritance and polymorphism

* You need to initialize one or more members at creation time

* You need to supply an unparameterized constructor

* You need unlimited event handling support

If your type does not fit clearly into either of these categories, define a Class. Classes are more flexible than structures, and the storage and performance differences are often negligible. Structures are intended primarily for types that behave like built-in types, not necessarily for general-purpose use.

http://msdn2.microsoft.com/en-us/library/aa289521(VS.71).aspx

I hope that throws some light on why Color is a structure and not a class.

Many thanks to Paul for this information.

Now a contribution from Don - this is also worth getting your head round :-

In addition to the things Paul mentioned, one of the key differences between classes and structures is their 'pass-by' semantics, and this more than anything else may decide which one is appropriate for any given task.

Classes are always passed by reference. If you pass a class to a function, you are really passing a managed pointer to the class instance. There is still only one copy of the class instance (managed by the GC) and any changes to the instance in the called function will also be reflected in the caller, since both are referencing the same instance.

Structures are always passed by value. If you pass a structure to a function, the system makes a copy of all the data in the structure and passes the copy. Therefore, any changes to the structure in the called function will NOT be reflected in the structure that the callee has.

You can override this if necessary. To pass a class by value, you would pass 'o:Clone()' instead of 'o'. To pass a structure by reference, type the parameter using 'REF' instead of 'AS'.

Most POD types in .NET (int, date, logic, single, double, etc.) are structures. This gives them the efficiency you would expect for such data types, but still gives them many of the characteristics of full blown classes (i.e. methods, properties, inheritance). Classes such as point, rect, dimension, etc. in VO would be much more efficient if they were structures, but VO doesn't have any equivalent (and we really can't change them without breaking compatibility).

PHH - October 2008


Appendix C - Fonts

Navigation: »No topics above this level«

Appendix C - Fonts

 Return to chapter overview

Font Families :

In order to discuss fonts we have to get some terminology correct !

What most users term as a 'font', is actually a 'font family'. So 'Arial' is a font family, and so is 'Times New Roman'.

The font is what we have when we specify a font family and a size. So 'Arial 12' is a font. This is mirrored in the way we code things :-

oBrushTrans := SolidBrush{Color.FromArgb(75,Color.Red)}

e:Graphics:DrawString("FRONT" , Font{"Arial" , 49.0} , oBrushTrans , 150 , 400)

e:Graphics:DrawString("FRONT" , Font{"Arial" , 49.0} , oBrushTrans , 200 , 500)

This (above) is taken straight from the ClickStartVDN notes from March 2008.

LOCAL oFont AS Font

oFont := Font{"Century", 12}

So one overload of the font

When we use fonts to print, the colour and 'effect' is from the pen and brush object used for the printing.

The style is determined by a FontStyle object class. See below for examples :-

=========================================================================================================================================================

Okay then don't get confused, remember that we need to get right into overloading, and what this can do to help us.

The code above shows that there is another way to create a font object, and that is from another font object, along with a FontStyle object.

So the above image shows font styles 'Italic', 'Underline' and 'Strikeout' in action to create the printed output on the form.

The SDK documentation shows :-

================================================================================================================

I suggest you look at the ClickStartVDN notes, to save copying all the details to this section. But just as another 'taster' to what lies there, here is a little something to throw some light !

Some Helpful Fonts - Reports :

===========================

It may help you to know that I spent some time sorting through the available fonts in Vista, to find this selection which are mainly 'Sans Serif' and therefore suitable for report printing - where columns and other issues mean that clear and clean text is rather essential.

However, when it comes to font styles we need some more explanation, as at first sight some seem to be missing !

Study the image below of part of the Report Generator main screen :-

Some Helpful Styles :-

===================

You should be able to spot the 'odd one out' as it were. The selected item in the drop down list shows 'Bold Italic' with which we are all familiar.

However, this is not shown in the enumeration list. And simply this is because the various settings can be combined, in the same way as flags are combined in other places.

Look at the working code below - which comes from the Report Generator application :-

METHOD StyleName2FontStyle(cName AS STRING) AS FontStyle

LOCAL oFS AS FontStyle

IF !cName == "Bold Italic"

DO CASE

CASE cName == "Regular"

oFS := FontStyle.Regular

CASE cName == "Bold"

oFS := FontStyle.Bold

CASE cName == "Italic"

oFS := FontStyle.Italic

CASE cName == "Strikeout"

oFS := FontStyle.Strikeout

CASE cName == "Underline"

oFS := FontStyle.Underline

OTHERWISE

oFS := FontStyle.Regular

END CASE

ELSE

oFS := FontStyle.Italic | FontStyle.Bold

ENDIF

RETURN oFS

It does not take a lot of imagination to see that we ought to be able to make some other combinations work.

Whether we would want to is another matter :O)

Try combining some for yourself, like "Underline, Bold" - or even "Strikeout, Italic".

I think we ought to be moving on, so see you in the next Appendix.

PHH - October 2008.


Font Families

Navigation: Appendix C - Fonts >

Font Families

 Return to chapter overview

Here is some information on Font Families which will allow you to take things further if you require it.

However, there is not so much that this volume of ClickStart will get too large ;-)

Finding the Windows Font Families :-

On most Windows platforms today there are literally hundreds of fonts installed, and these are grouped into font families.

Although we can do a lot with printing and fonts (without going into much depth, and by using just the standards like Arial and 'Times New Roman'), it is instructive to apply some of our newly acquired dotNet skills to finding and selecting available fonts.

Below is a basic approach to finding the full list available to us, and printing out a few of the names. I have selected numbers 129 through 134 on my Vista platform. Yours may well be different.

================================================================================================================================

We can combine some of our learned skills with colors and other graphics elements, and display a sample of the range of font families on a blank form.

See below for the live output and the code for the first part - or blue part - of the output.

I will leave it to you to run this code on your system, from the code included in the "Full Working Code" sub-section.

You will need to experiment to see just what can be done with font families.

You may also like to visit the sections on printing to see how to get the list of font families which are available to the graphics surface for a printer, or printing device.

Are there any font families which are available to one of these but not the other ?

Remember to be colourful and also have font fun :O)

PHH - October 2008


Appendix D - Serialization, a God-send !

Navigation: »No topics above this level«

Appendix D - Serialization, a God-send !

 Return to chapter overview

Coming to Vulcan and dotNet as a standard user, there are two ways I have used "serialization" to solve my own immediate coding needs.

One is for reading and writing data held in DataTable objects - to disk file - the other is for 'preserving' the state of a customized object, from one use of the application to the next.

What I mean by this last statement, is that the data held in the properties of one of my own classes, is "persisted" while the application is not running. In the same way we used DBFs and Ini files in the past to keep our data and settings. Obviously there needs to be a pair of processes similar to reading and writing, and these are called deserialization and serialization. Normally, we say these the other way round, but since writing is serialization we had better get things the right way round as to associating the meanings.

Serialization places data in XML formatted files, and these look similar in nature to HTML that now most people are familiar with - a tagged mark up language.

XML files are good for transporting over the net.

Although such XML files may be used to keep a few simple configuration settings, they are just as capable of holding huge amounts of data too. Obviously, the serialization of a DataTable object often creates larger files, as commonly the amount of data is much greater than for a configuration file.

XML files seem to zip very well also - they seem to shrink a great deal indeed.

When DataTables are serialized there is a second, and often much smaller file made, this is the schema file. The schema file has a secondary file name of " .XSD " as opposed to that of " .XML " for the file holding the data.

Personally, I find it very convenient to serialize a class of my own creation, and this produces just an XML file. Rightly or wrongly, I use this as a simple and effective way to do my own configuration saving and setting.

Enough for talking, lets look at some real examples, and ones we can follow in future to code our own solutions.

Let us look at a real example form my recently created "DataTable Sorting and Selecting Tool" (DTSST).

Look at the general layout of the code below, and then we will discuss it in detail :-

METHOD DumpData() AS VOID

SELF:Form2DumpObject()

LOCAL serial AS Serialization.XmlSerializer

LOCAL oSW AS StreamWriter

LOCAL cFile, cText, cHead AS STRING

cFile := Application.StartupPath+"\DTSSTdetails.xml"

IF File.Exists(cFile)

cText := "Dump file exists already!"

cHead := "XML file :"

MessageBox.Show(SELF,cText,cHead)

ENDIF

//----- save object to XML file -----

serial := Serialization.XmlSerializer{typeof(DTSSTdetails)}

oSW := StreamWriter{cFile}

serial:Serialize(oSW, SELF:oDetails)

oSW:Close()

oSW := NULL_OBJECT //----- not needed in dotNet -----

serial := NULL_OBJECT //----- not needed in dotNet -----

RETURN

You will notice that it is the serial object which is based on a class called 'DTSSTdetails', and it is this object which is having it export properties serialized to disk.

The code line at the top of the method 'SELF:Form2DumpObject()' is what is converting GUI screen information into the properties of this object designed for serialization.

Let us look at the class called DTSSTdetails :-

[Serializable]CLASS DTSSTdetails

EXPORT cSourceFolder AS STRING

EXPORT cTargetFolder AS STRING

EXPORT aFilters AS STRING[]

EXPORT cSourceFile AS STRING

EXPORT cFocusXML, cFocusXSD AS STRING

CONSTRUCTOR()

SUPER()

SELF:cSourceFolder := ""

SELF:cTargetFolder := ""

SELF:aFilters := STRING[]{0}

SELF:cSourceFile := ""

RETURN

END CLASS

A few things to notice, first that there is a constructor which takes no parameters. This is a requirement for serializable classes - one overload of the constructor must be like this.

Also, we have the properties for 'dumping to disk' (makes a nice change from using and typing 'serialization') set to EXPORT. Only export properties will be used, and if by any chance your class has an export property you wish NOT to be dumped to disk, then you do as such :-

[NonSerialized]EXPORT cSourceFile AS STRING

Both these could be written with empty braces, the code is syntactically identical :-

[Serializable()]CLASS DTSSTdetails

............................................................

[NonSerialized()]EXPORT cSourceFile AS STRING

The braces are only really required when there are parameters to pass.

Obviously, in the case of the last line of code shown above, the data for the 'cSourceFile' property will not be present in the dumped (serialized) file.

Let us now see the results of this code in action :-

==============================================================================================================

As you can clearly see, we have values for two folders, a list of filter strings, and then a primary file name.

Okay then, al we need do to finish the 'chain' of events to serialize, is see what the 'Form2DumpObject()' method does for us.

METHOD Form2DumpObject() AS VOID

SELF:oDetails:cSourceFolder := SELF:cSourceFolder

SELF:oDetails:cTargetFolder := SELF:cTargetFolder

SELF:oDetails:aFilters := (STRING[])SELF:alFilterConditions:ToArray(typeof(STRING))

SELF:oDetails:cSourceFile := SELF:cSourceFile

RETURN

Simply put, it takes values associated with form controls, and places them into the dump object property fields. In this case the 'DTSSTdetails' object.

Fine, now we can serialize, or dump data, what about the reverse ? Lets now look at the deserializing or 'scooping' process.

METHOD ScoopData() AS Boolean

LOCAL serial AS Serialization.XmlSerializer

LOCAL oSR AS StreamReader

LOCAL cFile, cText, cHead AS STRING

LOCAL lResult AS Boolean

lResult := FALSE

cFile := Application.StartupPath+"\DTSSTdetails.xml"

IF File.Exists(cFile)

cText := "Existing DUMP file will be read!"

cHead := "XML file :"

MessageBox.Show(SELF,cText,cHead)

//----- read object from XML file -----

serial := Serialization.XmlSerializer{typeof(DTSSTdetails)}

oSR := StreamReader{cFile}

SELF:oDetails := (DTSSTdetails)serial:Deserialize(oSR)

oSR:Close()

oSR := NULL_OBJECT //----- not needed in dotNet -----

serial := NULL_OBJECT //----- not needed in dotNet -----

lResult := TRUE

ELSE

cHead := "XML Dump file :"

cText := "File can't be found !?"+CRLF

cText += cFile+CRLF

MessageBox.Show(SELF,cText,cHead)

ENDIF

RETURN lResult

Pretty much as you would expect, the class property oDetails is instantiated with the zero parameter constructor of that class, and the streaming used to read the data. The serializer finishes the job by populating the fields for which there is stored data.

However, this process is not complete as far as our application is concerned, as we need to have these data values 'applied' to our GUI form :-

METHOD DumpObject2Form() AS VOID

SELF:cSourceFolder := SELF:oDetails:cSourceFolder

SELF:olbSourceFolderDetails:TEXT := SELF:cSourceFolder

SELF:FillDropDownSourceXML()

SELF:cTargetFolder := SELF:oDetails:cTargetFolder

SELF:olbTargetFolderDetails:TEXT := SELF:cTargetFolder

SELF:FillDropDownTargetXML()

SELF:cSourceFile := SELF:oDetails:cSourceFile

SELF:olbSourceFilePair:TEXT := SELF:cSourceFile

SELF:alFilterConditions:Clear()

SELF:alFilterConditions:AddRange(SELF:oDetails:aFilters)

RETURN

As you can see, this process is a little more complex that the one to dump the form details to disk via the DTSSTdetails object. The reason is simple, we have to find the currently available XML and XSD files in the source and target folders, then place them into the drop down lists. We will not complicate this section by showing the detailed code for this, as you can look that out in the working code section for yourself.

In case you are thinking that this is a complex and long winded affair, be assured, it is actually easy to do, put into practise, and get to work correctly. Particularly if you have some working code to copy and paste - then adapt.

The biggest bit for you to do is design the special class to be serialized. And, I think it best to have a class especially for this rather than mess about trying to use only parts of an existing one.

So plan and think the serialization through, don't do it as an after thought.

Getting Round any Problems :

So far, with all I have done in my applications, I only 'hit a wall' when I had a class with fonts, pens and brushes.

However, after a little testing and trial, I found that I could save font families (as a string), and font styles as an actual 'FontStyle' object.

It was relatively easy to create the pens and brushes after deserialization, and something similar before dumping to XML file.

So think about what you wish to save, and do some tests if you think the objects may cause some problems. There is usually a way around many such related problems.

To see what I did for saving the configuration for report settings, see the working code for the ReportGenerator application.

Serializing DataTable objects :

Okay folks, this is no big deal, as it is really just an 'under the cover' version of what we have seen above. Single method calls on the DataTable (DT) object allow for the 'dumping' and 'scooping' as I often call it.

Lets see the code to save a newly made DT object, in the DTSST application, to disk :-

METHOD DoMakeXMLfilesFromNewTable() AS VOID

IF SELF:oDTtarget = NULL_OBJECT

MessageBox.Show("No 'NEW' data table in focus !","WARNING :")

RETURN

ENDIF

//-------------------------------------

LOCAL cFile, cData, cSchema AS STRING

cFile := SELF:cTargetFolder+SELF:cXMLtableName

cSchema := cFile+".XSD"

cData := cFile+".XML"

SELF:oDTtarget:WriteXmlSchema(cSchema)

SELF:oDTtarget:WriteXml(cData)

IF File.Exists(cSchema) .and. File.Exists(cData)

Messagebox.Show("Both files made successully !")

ELSE

Messagebox.Show("There was some sort of problem !?!")

ENDIF

RETURN

The only difference here, apart from it being a bit simpler, is that we have methods to create two forms of XML file code. One the data and one the schema.

Remember, the schema (.XSD) describes the data structure, and the .XML file is the data to fit this schema.

It is just as easy to read back the stored details from disk, to populate a new DT object :-

[ so here is how to scoop ! ]

METHOD MakeTableFromXMLpair(cPathFile AS STRING) AS DataTable

LOCAL cSchema, cData AS STRING

LOCAL oDT AS DataTable

oDT := DataTable{} //----- don't use a name ! get error -----

//---------- either leave blank or get the original from the schema file ------

cSchema := cPathFile:TRIM()+".XSD"

cData := cPathFile:TRIM()+".XML"

IF File.Exists(cSchema) .and. File.Exists(cData)

Messagebox.Show("Both files located !")

ELSE

Messagebox.Show("Can't find file - problem !?!")

RETURN oDT

ENDIF

//================================

TRY

oDT:ReadXmlSchema(cSchema)

oDT:ReadXml(cData)

CATCH e AS Exception

Messagebox.Show(e:tostring())

RETURN oDT

END TRY

SELF:cXMLtableName := oDT:TableName

Messagebox.Show(SELF:cXMLtableName, "Table Name :")

SELF:FindSchemaDetails(oDT)

RETURN oDT

Again, this and all related code can be found in the application working code for the DTSST application.

One useful thing to point out is do not give you table a name before using the 'Read' DT methods. If you choose a name for the table which conflicts with that in the schema, then you will have problems :0(

Finally, before we leave this topic, here is a small sample of the XML file made in a recent dumping of a newly made DT to disk file :-

=====================================================================================================================================================

Notice that the name of this table, in both files, can be seen as "goodtest26".

PHH - October 2008.


Appendix E - managing signatures

Navigation: »No topics above this level«

Appendix E - managing signatures

 Return to chapter overview

Enter topic text here.


Appendix F - DataTable objects

Navigation: »No topics above this level«

Appendix F - DataTable objects

 Return to chapter overview

The dotNet DataTable class is found in namespace "System.Data", and the assembly of the same name.

The DataTable (DT) class is very flexible and yet powerful. It seems to be a central object in many dotNet operations. Also, many varied data file formats may be quite easily converted into a DT for easy use. XML and DTs go together like 'love and marriage'.

So put simply, if we make classes to work directly with DT objects they will have a very wide application across all of dotNet. Whatever your starting point, get to DTs as soon as possible, then use DT related tools.

For the above reasons I decided to make the ReportGenerator tool work solely with DTs. It was a great relieve when I realized how this straight forward decision made life so easy. Once I had seen the light this first time, it was not difficult to make one more step, and base my Mail Merge code on data tables too.

I hope you see the light as clearly, keep looking :O)

The SDK documentation say that ... "The DataTable object is a central object in the ADO.NET library. Other objects that use the DataTable include the DataSet and the DataGridView."

From personal experience I found DTs a dream to work with while learning SQL with Vulcan and ADO.NET, and I for one am a definite convert.

If you look out the ClickStartVDN help notes you will find the section on the "FileMultiConverter" tool, and this uses DT extensively with the DataGridView control to display results. I actually use the FMC tool in the next-but-one sub-section, to prepare the mail merge data in XML format.

If DTs are new to you, I suggest you take some time to familiarize yourself with them, and write some small test applications so you get a good working 'feel' for them.

Okay then, lets get to grips with using data tables by moving on to the next sub-section, and delving right into some useful coding.

Good Luck.

PHH - Sept. 2008


DataTables - the fundamentals

Navigation: Appendix F - DataTable objects >

DataTables - the fundamentals

 Return to chapter overview

Introduction :

Coming to the DataTable class as a beginner, you need to appreciate that we can approach this from 'two ends' as it were. One where we have data which is easily read into a DT and the other where we wish to create a DT from scratch, before we have any data as such.

Get it into your thinking, right away, that a schema is needed to describe the data which is to be held in the table.

If we have data in a file, usually a schema is associated with this. If we are constructing from scratch (programmatically), we need to define our schema. This is done by making DataColumn objects and adding them to the DataColumnCollection - which is accessed through the 'Columns' property of the DT object.

When we have defined our schema - basically saying what data types are to be held in which columns, and giving them names - we are ready to add data rows to the table.

A data row is a line of data cells, and many of us are used to calling these data 'records'. In this case, a row is made up from individual data cells, and the content of each data cell must be the same data type as that specified in the relevant DataColumn property.

As an aside - to those who may be coming from the world of VO, and missing badly their dynamic array, this is a possible answer to your prayers - once you get familiar with the syntax and general use.

Not only can you add and remove rows 'on the fly' but you can also do the same for columns. Each column may be of any data type, and strongly typed columns are better than the VO weak typing. Don't forget you can obviously put complex objects in a column and its related cells. However, I have not tried a column of DTs as yet :O)

One thing you can't do with a VO dynamic array however, is throw it at a DataGridView and have it display on your GUI form. It is just about as easy as that with DTs.

Enough of the chat and philosophy, lets get going, and 'sink some syntax' !!!.

Making a Start :

We can't start to make a schema unless we first dream up some suitable details. Then we need some simple data to match our schema, so here goes :-

Title : STRING

Firstname : STRING

Lastname : STRING

Age : INT32

Account Owing : Double

DoB : DateTime

XmasList : Boolean

Below is both the compiled code for the data columns, as well as it running :-

======================================================================================================

This looks fine, we have seven columns associated with our table, and these have a variety of data types, which will be useful for testing and demonstration.

Lets us move on and now create some rows (one at a time), and fill these with suitable test data :-

==========================================================================================

Notice that when we wish to make a new row, we take a DataRow variable and insatiate it, using the 'NewRow()' method of the DataTable.

This presumably copies over the necessary details about column data types, ready for the cell contents to be added.

Okay, lets move on, and learn some more.

Well now, this is where we see just how easy using DataTables really is. We will try to dump the data, and schema, from the newly made table into XML format on disk. And guess what ! It takes only a couple of lines. Have a look at the image of the required code lines, and live response.

===========================================================================================================================

Notice that you need to supply the secondary file names yourself.

If this has successfully dumped the data to the two files shown, we ought to be able to double click them in the Windows Explorer pane and see the details.

=======================================================================================================================================

Lets give this a go to see what we get.

First lets try the schema file :-

=================================================================================================================================

That seems to have done as we hoped, and saved the column names and their correct data types.

Now let us see what the data looks like :-

=============================================================================================

I think you will agree it was easy, and it seemed to work 100%.

Now we will see just how easy it is to read this back in from file - to make a DataTable object - and then pass this for display, to a DatatGrid object.

Once we have done that, we are then free to do all sorts of lovely thing with DataTable objects as our sources of information - for printing and reporting in all sorts of ways.

Field / Column Names for Ordinals :

It will please a lot of readers to know that instead of the ordinal values to refer to the columns, or fields in the table, we can actually use string values of their names. Here is a fourth record input in the (to my eye) more friendly way :-

oDR := oDT:NewRow()

oDR:Item["Title"] := "Ms"

oDR:Item["Firstname"] := "Angelina"

oDR:Item["Lastname"] := "Cleftfork"

oDR:Item["Age"] := 29

oDR:Item["Account Owing"] := 51.88

oDR:Item["DoB"] := DateTime{1981,06,03}

oDR:Item["XmasList"] := TRUE

oDT:Rows:Add(oDR)

You are probably by now starting to see why I suggested it was a good replacement for the VO dynamic array. I think it is actually better as I always wished to use field names instead of ordinal values with my VO arrays.

Now I can with data tables.

Reading XML Files to DataTable :

Study the code below to see how easy it is to read from XML files on disk, and make a new DataTable and populate it with cell contents :-

LOCAL oDT2 AS DataTable

oDT2 := DataTable{} //----- don't use a name ! get error -----

TRY

oDT2:ReadXmlSchema(cSchema)

oDT2:ReadXml(cData)

CATCH e AS Exception

Messagebox.Show(e:tostring())

END TRY

LOCAL cText AS STRING

cText := (STRING)oDT2:Rows[3]:Item["Firstname"]

Messagebox.Show(cText)

Please note the comment I made about NOT giving the new DataTable a name. Look what happens below when I call it a name which is different to that in the existing schema :-

======================================================================================================================================================

However, as you can see in the code above the last, when we left the name empty, it did work.

Having looked inside the schema file I see we gave the table name as "BDtestTable" so presumably if I use that name it will work - let's give it a go :-

Yep, nothing much to show you, just take it from me that when I typed the name exactly as it should be, it did work. I suggest leaving it blank unless you have a real good reason for not doing so.

What is Missing :

If we are to use DataTable objects as central to our approach to report generation, laser labels, mail merging, and much, much more, then it would be more than useful to 'prepare' the table itself before we use the data for the task in focus. What I mean is that we can order the records to suit our needs and even add control information in either one, or a few, extra 'control' columns.

By 'control' I mean that the extra columns added by the developer's code would contain information about page throws, keeping lines together, and more - depending on what task is to be done. It may even, for example, contain information on which bitmap is to be included - when the task is laser label printing. For the Christmas card mailing list female customers may get an angel whereas male customers a Father Christmas. Well, you get the idea !?

In the 'Data Preparation Tool' (DPT) sub-section, we will see a general purpose tool which will allow us to prepare any data table data so that it is ready for final processing. While studying this tool we will see how to make a sorted table, with records in the order we require for the final process - whatever it may be.

We will look at using built in features of the DataGridView control, and also doing things ourselves by Vulcan code. There are benefits to both approaches, so we will try to spot the best from each - just to make our lives even more easy.

Before moving on to another section, just lets us focus on the fact that DataTable objects seem VERY important to many aspects of dotNet programming, and therefore it would seem eminently sensible to learn as much as possible about all the other related classes and coding approaches.

So for example checking out primary keys, and the DataTableReader class, would be just a start.

PHH - Sept. 2008


Preparing Mail Merge Files

Navigation: Appendix F - DataTable objects >

Preparing Mail Merge Files

 Return to chapter overview

Below is an image of the FileMultiConverter at work. With other images of the data files created initially, and the resulting XML pair.

This tool was developed as a learning exercise and part of the ClickStartVDN note set. Although not totally finished or polished, it does work. Well, should I say well enough to do the job we require here :-)

First I made a small three record file in DBF format in dBAdministrator. This is shown below :-

Then the 'FileMultiConverter' tool was run and the DBF loaded in from the selected folder ( I used C:\VDNUK\FileMultiConverter\ ).

I then change the GUI screen selections to dump a file pair in XML / XSD format to the same folder.

Below, I used the same Vulcan 'tool' to read back in the XML pair, after selecting both a schema file and a data file using the appropriate drop down list boxes.

You can see the MAILMERGE.DBF file is available in the focus directory.

This shows that if the data can be read back into the DataTable object being displayed in the DataGridView control above, then it should be OK for reading into a data table for our mail merging procedure.

We will also use the same data for printing laser labels to go on the envelopes.

Before we go any further, let us few the data in the newly made XML file :-

==================================================================================

===============================================================================================================================

Both the XML and the XSD file are shown above - well two records of the XML file, as this is sufficient to show that we have succeeded.

PHH - Sept. 2008.


Appendix G - Examine Printers 'tool'

Navigation: »No topics above this level«

Appendix G - Examine Printers 'tool'

 Return to chapter overview

Enter topic text here.


Appendix H - Sample Output

Navigation: »No topics above this level«

Appendix H - Sample Output

 Return to chapter overview

Company Invoice :

Below is a sample invoice which has been prepared using the 'BestDocument' class.

There are three text regions which were placed using 'blocks' - out of interest, see if you can predict which these are, and then compare with the code solution shown below.

This is a real example, and I use this in my invoicing package. Initially this was written in VO and used PDF995 as the preview facility. As you can see the preview facility used for this is the dotNet PrintPreview dialog.

You may be interested to know that this prints out physically to the printer as nicely as it looks here.

When the live invoicing application is run on live data, this 'template' is printed by code first, and then the relevant data is placed in the appropriate places to complete the job.

We will look at the 'template' part first, before looking at the sample data 'filling' code section.

As a further challenge, see if you can figure out the four regular grids which were placed in making this.

Once you have done that, check out the code below :-

The complete code for the 'DocumentQ' method is included in the working code sub-section of this appendix.

When you get the 'testbed' demo app up and running for yourself, it will print as shown just be you clicking the 'Q' button.

However, it will be instructive to examine some sections of this code to reinforce our learning from earlier sections.

Because the details in the template are not going to change very often, I have simply hard coded them - it was simply a personal choice to do so.

Let us see how the name and address section where placed on the paper :-

LOCAL ............. , oLucidaFont AS Font

.....................................

.....................................

oLucidaFont := Font{"Lucida Sans",14}

.....................................

.....................................

//-------------------------------------

oBD:Font := oLucidaFont

LOCAL aNA AS STRING[]

LOCAL nM AS Double

aNA := STRING[]{5}

aNA[1] := "Dr. Philip H. Hepburn,"

aNA[2] := "BEST house,"

aNA[3] := "19, Firbank Crescent,"

aNA[4] := "Newport, S. Wales."

aNA[5] := "NP19 7QR"

//nM := 22

nM := 20

oPPC := PaperCoords{nM,61}

oBD:Block(oPPC, aNA)

//--------------------------------

As you can see, the code simply makes a string array of five elements, and fills it with the necessary characters.

After the point has been defined in 'Paper' coordinates, it is passed with the string array as parameters into the BD class 'Block()' method.

When we wish to place the customer name and address, we do a similar thing, except we get the data from a file before filling the string array. We will see this later in the section.

The code to place the company logo is as follows :-

//------------------- Logo ------------------------

LOCAL cText AS STRING

LOCAL oPPC AS PaperCoords

LOCAL nQd, nQa AS Double

nQd := 0.5

nQa := 75.0

oPPC := PaperCoords{nQd,nQa}

oBD:Font := oCenturyFontVL

oBD:TEXT(oPPC, "Q")

nQd += 3.5

nQa += 5.3

oPPC := PaperCoords{nQd,nQa}

oBD:Font := oCenturyFontM

oBD:TEXT(oPPC, "e")

nQd += 8.2

nQa -= 7.5

oPPC := PaperCoords{nQd,nQa}

oBD:Font := oScriptFont

oBD:TEXT(oPPC, e"\"Eminence thro' Quality\"")

There are a couple of things to note here. One is the apparent over-printing of the 'e' and the 'Q'. Note that text printed on the paper does not have 'white' space, you you don't have to be as careful with the what is placed first as you do with images. A signature bitmap image placed after the text can cause parts of the text to disappear. In reality there has been 'white' over-printed and the text is still there underneath - you just can't see it.

To summarize this small section, if you place the 'e' and the 'Q' in ether order (one before the other) they will both be still visible - the spaces, or gaps, in letters are actually 'nothing'.

Forming the Grids :

I have to be honest, the reason we have the lined, or grid, section made up by four regular grid operations, is because the 'complex' method seems to need some editing / adjusting. It did not do as it ought. So being resourceful I have done it with four regular grids instead. I know the regular grid method works as we used it successfully to do the graph paper in an earlier section.

Let us examine the code to place the grids, notice I have left in place the code for the first attempt - to use a complex grid. I will use this later to check things out.

LOCAL aX, aY AS Double[]

LOCAL nE, d1, d2, d3 AS Double

nE := 3.0

d1 := 13.5

d2 := 67

d3 := 11.5

aX := <Double>{d1,d2,d3}

aY := <Double>{nE,nE,nE,nE,nE,nE,nE,nE,nE}

//oBD:GridComplex(oPPC, aX , aY)

LOCAL nXg, nYg AS Double

LOCAL oPPC3 AS PaperCoords

//----------- left column -----------------

oPPC := PaperCoords{nD,nA}

nXg := nA+d1

nYg := nD+9*nE

oPPC3 := PaperCoords{nYg, nXg}

oBD:GridRegularNbyM(oPPC,oPPC3,9,1)

//---------- middle column ----------------

oPPC := PaperCoords{nD,nA+d1}

nXg := nA+d1+d2

oPPC3 := PaperCoords{nYg, nXg}

oBD:GridRegularNbyM(oPPC,oPPC3,9,1)

//------------ right column ---------------

oPPC := PaperCoords{nD,nA+d1+d2}

nXg := nA+d1+d2+d3

oPPC3 := PaperCoords{nYg, nXg}

oBD:GridRegularNbyM(oPPC,oPPC3,9,1)

//---------------------------------

Nothing startling, remember the N by M is down then along - so we have 9 equal vertical partitions, and one horizontal one. Placing the edge of one grid over the dge on the next does not cause any visible problems.

Now see how we place the fourth grid, the one for the totals section :-

//-------------- totals ---------------

nXg := nA+d1+d2

nYg := nD+9*nE

oPPC := PaperCoords{nYg, nXg}

nXg += d3

nYg += 3*nE

oPPC3 := PaperCoords{nYg, nXg}

oBD:GridRegularNbyM(oPPC,oPPC3,3,1)

//-------------------------------------

You will notice that I have bothered to code things so that most coordinates are 'relative' to a starting position. This makes it easier to move, or 'ease', a whole sub-section so that it looks right. If not you just adjust one place and try again. Don't be tempted to cut corners in your own coding, it will come back and 'bite you' !

Before we move on, there are a couple of small points to do with general coding. The first related to the 'escape' sequence and strings, the second to do with multiple font styles. Let us take a moment to see the code and learn from it :-

oBD:Text(oPPC, e"\"Eminence thro' Quality\"")

In order to get the quote marks " at the start and end of the text string, we need to use \" and the compiler then knows we are not trying to finish the string of characters before we start it - if you catch my drift. Make an effort to look up the others which make up the set of special combinations - another is \n for new line.

Finally for the template part of the company invoice section, notice how we set two font styles to the same font :-

LOCAL oFSbi AS FontStyle

oFSbi := FontStyle.Bold | FontStyle.Italic

oTimes12BI := Font{"Times New Roman",12,oFSbi}

oBD:Font := oTimes12bi

oPPC := PaperCoords{73,2}

cText := e"Cheques should be made payable to \"BEST consultancy\""

oBD:TEXT(oPPC, cText)

//-----------------------

Yes, you could do all the work in the Font constructor, but it can get less clear as to what you are doing. I like to make my code as clear as possible when there is not a real time issue to worry about.

I have not tried it but you ought to be able to combine all of the FontStyle values into one, and apply them successfully. Obviously you need to pick a font family which supports them all, so I will leave it as an extension exercise for you to try. Not sure when you will ever use it for real however :-)

Customer Order Details :

Right, now for some order details to be added to the invoice template, the sort of completed paper work which would be sent out to a customer.

Look at the extra information which as been added since the above image was generated. After a scan of the image we will go through the various regions and look at which block method has been used to fill the grid and relevant spaces :-

Notice the fruits of our hard work on block methods is now paying off. We have some nice looking decimal aligned information as well as a right aligned small set in the date column.

The particulars column is just a standard left justified block method, with one exception, there is a scaling factor which allows the line spacing to be 'eased' to fit the existing template grid.

First of all, let us look at the whole of the customer information code :-

=====================================================================================

METHOD DisplayOrderDetails(oBD AS BestDocument, nC AS Double, nI AS Double, nG AS Double) AS VOID

LOCAL aCompany AS STRING[]

aCompany := STRING[]{8}

aCompany[1] := "Mr. Sven Gonaway,"

aCompany[2] := "Storkland Healthcare Ld.,"

aCompany[3] := "Pentagle Way,"

aCompany[4] := "Cardiff Business Park,"

aCompany[5] := "Llanishen,"

aCompany[6] := "CF13 4ER"

aCompany[7] := "(012920) 472200"

aCompany[8] := "sven.gona@wasthere.net"

LOCAL oMSSS12 AS Font

oMSSS12 := Font{"Microsoft Sans Serif",12}

oBD:Font := oMSSS12

LOCAL oPPC AS PaperCoords

oPPC := PaperCoords{nC,8}

oBD:Block(oPPC,aCompany)

//----------------------------------------

oPPC := PaperCoords{nI,23}

oBD:Text(oPPC,"13/03/2005")

oPPC := PaperCoords{nI,71}

oBD:Text(oPPC,"2526")

//----------------------------------------

LOCAL aParts AS STRING[]

aParts := STRING[]{8}

aParts[1] := "Supply and deliver computer & IT parts as listed below :-"

aParts[2] := "replacement keyboard and laser mouse with fancy mat"

aParts[3] := "new HDD and IDE cable, DVD/CD drive"

aParts[4] := "refurbished flat screen 23 inch monitor and P/S"

aParts[5] := "install, test, & integration of all hardware provided"

aParts[6] := "software services for Vulcan beginners classes"

aParts[7] := "dotNet master class and room sweep for management"

aParts[8] := "empty the trash cans in main offices on the way out"

oPPC := PaperCoords{nG-0.5,18}

oBD:BlockRestrictRHS(oPPC,aParts,83,1.63)

//--------------------------------------------------------------------

LOCAL aDates AS STRING[]

aDates := STRING[]{3}

aDates[1] := "September"

aDates[2] := "22nd."

aDates[3] := "2008"

oPPC := PaperCoords{nG-0.5,3}

oBD:BlockRightRestrictRHS(oPPC,aDates,15,1.63)

//----------------------------------------

LOCAL aCosts AS STRING[]

aCosts := STRING[]{8}

aCosts[1] := ""

aCosts[2] := "27.13"

aCosts[3] := "92.67"

aCosts[4] := "87.34"

aCosts[5] := "25.00"

aCosts[6] := "177.00"

aCosts[7] := "0.02"

aCosts[8] := "10.10"

oPPC := PaperCoords{nG-0.5,84}

oBD:BlockDecimal(oPPC,aCosts,2,9,1.63)

//----------------------------------------

LOCAL aTotals AS STRING[]

aTotals := STRING[]{3}

aTotals[1] := "419.26"

aTotals[2] := "73.37"

aTotals[3] := "492.63"

oPPC := PaperCoords{nG+26.5,84}

oBD:BlockDecimal(oPPC,aTotals,2,9,1.63)

//----------------------------------------

RETURN

=================================================

We have actually done all we needed with just five calls to BD placing methods. And all were of a 'block' variety.

The range of 'block' methods was :-

Block()

BlockRestrictRHS()

BlockRightRestrictRHS()

BlockDecimal()

I think you will agree they are very useful, and extremely flexible in application.

For this test document 'Q' and the testbed app, I just called this above customer 'order detail' method from the end of the 'MyTestDocumentQ()' method. In a real live system you would do something very similar, but also go off and find the data from the customer and order files.

Without doing too much block code viewing, let us look at one from above, the 'BlockRighRestrictRHS()' method - used to display the small amount of date information on the left of the grid.

============================================================================================================

METHOD BlockRightRestrictRHS(oPPC AS PaperCoords, aText AS STRING[], nAcrossRHS AS Double, nHeightFactor AS Double) AS VOID

LOCAL i, nLen AS INT

LOCAL cLine AS STRING

LOCAL nDown, nAcross, nAdTextHeight, nWidth AS Double

LOCAL oPPCthis AS PaperCoords

LOCAL nLine, nAdjust AS Double

oPPCthis := oPPC

nAdjust := 1.0

nDown := oPPCthis:nDown

nAcross := oPPCthis:nAcross

nWidth := nAcrossRHS-nAcross

nLen := aText:Length

nAdTextHeight := SELF:TextHeight("PLQjy")

FOR i := 1 UPTO nLen

cLine := aText[i]

nLine := PCTextLength(cLine)

IF !nLine > nWidth

oPPCthis := PaperCoords{nDown,nAcross+(nWidth-nLine)}

SELF:TEXT(oPPCthis, cLine)

ENDIF

//----- move down for next line of block text -----

nDown += nAdTextHeight*nHeightFactor

NEXT

RETURN

========================================================

Here we are both adjusting the line height to fit the grid - using the nHeightFactor input parameter, and also using the limit supplied for the percentage distance across the page - beyond which we must not go. Because this is a right justified method, this limit is also used to calculate back to find where the string must be placed. Since the strings are of different length this position will be different each time.

OK then, not rocket science, just some tedious arithmetic - which must be spot on or nothing will be seen (usually).

Remember, once you get the 'knack' of applying the methods you do very little arithmetic, just design nice documents.

Anyone having tried to make nice posters and booklets in a DTP package will realize there is no 'fast' way to get results. There is always a certain amount of preparation involved. And, if you are in a rush, it shows in the finished output :0)

Just before we move on, let us make a point of expressing just how suitable the 'Paper' coordinate system was for this task. I chose 'Paper', rather than 'Page' or 'Graph', and I think it proved to be the correct choice.

Simply, line counting in the page metaphor was unsuitable here - but on its day, with the right type of document, it really pays dividends.

PHH - Sept. 2008


a patient enclosed note

Navigation: Appendix H - Sample Output >

a patient enclosed note

 Return to chapter overview

PHH - Sept. 2008


AH - Working Code

Navigation: Appendix H - Sample Output >

AH - Working Code

 Return to chapter overview

=================== Company Invoice =====================

METHOD MyTestDocumentQ(o AS OBJECT , e AS PrintPageEventArgs) AS VOID

LOCAL oCenturyFontVL, oCenturyFontM, oScriptFont, oTimes12 AS Font

LOCAL oLucidaMediumFont, oLucidaLargeFont, oLucidaFont AS Font

LOCAL oTimesFont12b, oArialBold13, oMSSS10, oTimes12BI AS Font

oCenturyFontVL := Font{"Century Schoolbook",80}

oCenturyFontM := Font{"Century Schoolbook",30}

oScriptFont := Font{"Script MT Bold",12}

oLucidaFont := Font{"Lucida Sans",14}

oLucidaMediumFont := Font{"Lucida Sans",20}

oLucidaLargeFont := Font{"Lucida Sans",36}

oTimesFont12b := Font{"Times New Roman",12,FontStyle.Bold}

oTimes12 := Font{"Times New Roman",12}

oArialBold13 := Font{"Arial",13, FontStyle.Bold}

oMSSS10 := Font{"Microsoft Sans Serif",10}

oBD:SetGraphics( e:Graphics)

oBD:JobName := "TestInvoice"

oBD:ResetLinesPerPage(67)

/*

oFont12Ab := Font{"Arial",12,FontStyle.Bold}

*/

//------------------- Logo ------------------------

LOCAL cText AS STRING

LOCAL oPPC AS PaperCoords

LOCAL nQd, nQa AS Double

nQd := 0.5

nQa := 75.0

oPPC := PaperCoords{nQd,nQa}

oBD:Font := oCenturyFontVL

oBD:Text(oPPC, "Q")

nQd += 3.5

nQa += 5.3

oPPC := PaperCoords{nQd,nQa}

oBD:Font := oCenturyFontM

oBD:Text(oPPC, "e")

nQd += 8.2

nQa -= 7.5

oPPC := PaperCoords{nQd,nQa}

oBD:Font := oScriptFont

oBD:Text(oPPC, e"\"Eminence thro' Quality\"")

//------------------ Company ------------------------

LOCAL nBd, nBa AS Double

nBd := 1.0

nBa := 1.0

oPPC := PaperCoords{nBd,nBa}

oBD:Font := oLucidaMediumFont

oBD:Text(oPPC, "INVOICE")

LOCAL nD, nA AS Double

nD := nBd+2.5

nA := nBa-1.0

oPPC := PaperCoords{nD,nA}

oBD:Font := oLucidaLargeFont

oBD:Text(oPPC, "BEST consultancy")

nD := nBd+7.9

nA := nBa-0.0

oPPC := PaperCoords{nD,nA}

oBD:Font := oTimesFont12b

cText := "Specialists in all aspects of Business & Personal Computing"

oBD:Text(oPPC, cText)

nD := nBd+10.5

nA := nBa+0.0

oPPC := PaperCoords{nD,nA}

oBD:Font := oLucidaFont

oBD:Text(oPPC, "V.A.T. registration no. :- GB 648 4766 89")

//---------------------------- Do Grid ----------------------------

nD := nBd+41.5

nA := nBa+1.0

oPPC := PaperCoords{nD,nA}

LOCAL aX, aY AS Double[]

LOCAL nE, d1, d2, d3 AS Double

nE := 3.0

d1 := 13.5

d2 := 67

d3 := 11.5

aX := <Double>{d1,d2,d3}

aY := <Double>{nE,nE,nE,nE,nE,nE,nE,nE,nE}

//oBD:GridComplex(oPPC, aX , aY)

LOCAL nXg, nYg AS Double

LOCAL oPPC3 AS PaperCoords

//----------- left column -----------------

oPPC := PaperCoords{nD,nA}

nXg := nA+d1

nYg := nD+9*nE

oPPC3 := PaperCoords{nYg, nXg}

oBD:GridRegularNbyM(oPPC,oPPC3,9,1)

//---------------------------------

oPPC := PaperCoords{nD,nA+d1}

nXg := nA+d1+d2

//nYg := nD+9*nE

oPPC3 := PaperCoords{nYg, nXg}

oBD:GridRegularNbyM(oPPC,oPPC3,9,1)

//---------------------------------

oPPC := PaperCoords{nD,nA+d1+d2}

nXg := nA+d1+d2+d3

//nYg := nD+9*nE

oPPC3 := PaperCoords{nYg, nXg}

oBD:GridRegularNbyM(oPPC,oPPC3,9,1)

//---------------------------------

//-------------- totals ---------------

nXg := nA+d1+d2

nYg := nD+9*nE

oPPC := PaperCoords{nYg, nXg}

nXg += d3

nYg += 3*nE

oPPC3 := PaperCoords{nYg, nXg}

oBD:GridRegularNbyM(oPPC,oPPC3,3,1)

//-------------------------------------

oBD:Font := oLucidaFont

LOCAL aNA AS STRING[]

LOCAL nM AS Double

aNA := STRING[]{5}

aNA[1] := "Dr. Philip H. Hepburn,"

aNA[2] := "BEST house,"

aNA[3] := "19, Firbank Crescent,"

aNA[4] := "Newport, S. Wales."

aNA[5] := "NP19 7QR"

//nM := 22

nM := 20

oPPC := PaperCoords{nM,61}

oBD:Block(oPPC, aNA)

//--------------------------------

oBD:Font := oMSSS10

LOCAL aNC AS STRING[]

aNC := STRING[]{5}

aNC[1] := "tele : (01633) 243057"

aNC[2] := "email : phil@phhepburn.f2s.com"

aNC[3] := "web1 : www.BESTconsultancy.co.uk"

aNC[4] := "web2 : www.BestPrimaryLibrary.com"

aNC[5] := "web3 : www.ClickStartVulcan.net"

oPPC := PaperCoords{nM+11,61.2}

oBD:Block(oPPC, aNC)

//--------------- Col Headers -------------------

oBD:Font := oArialBold13

oPPC := PaperCoords{40,4}

oBD:Text(oPPC,"Date")

oPPC := PaperCoords{40,43}

oBD:Text(oPPC,"Particulars")

oPPC := PaperCoords{40,82}

oBD:Text(oPPC,"Amount £")

//-------------------------

LOCAL oFSbi AS FontStyle

oFSbi := FontStyle.Bold | FontStyle.Italic

oTimes12BI := Font{"Times New Roman",12,oFSbi}

oBD:Font := oTimes12bi

oPPC := PaperCoords{73,2}

cText := e"Cheques should be made payable to \"BEST consultancy\""

oBD:Text(oPPC, cText)

//-----------------------

//-------- totals -------

oBD:Font := oArialBold13

LOCAL nT AS Double

nT := 70.5

oPPC := PaperCoords{nT,75}

oBD:Text(oPPC,"Cost")

oPPC := PaperCoords{nT+nE,75}

oBD:Text(oPPC,"V.A.T.")

oPPC := PaperCoords{nT+2*nE,75}

oBD:Text(oPPC,"Total")

//------- bottom left -------

nT := 82

oPPC := PaperCoords{nT,2}

oBD:Text(oPPC,"Conditions of Trade :-")

oPPC := PaperCoords{nT+11,2}

oBD:Text(oPPC,"V.A.T. Registration No. GB 648 4766 89")

oBD:Font := oTimes12

LOCAL aC AS STRING[]

oPPC := PaperCoords{nT+3,4}

aC := STRING[]{3}

aC[1] := "Please note that payment is due within 14 days of the invoice date."

aC[2] := "The surcharge of 5% may be charged for any overdue account."

aC[3] := e"All goods remain the property of \"BEST consultancy\" until paid for in full."

oBD:Block(oPPC,aC)

//-------------------------------------------

oBD:Font := oLucidaFont

nD := nBd+15.5

nA := nBa

oPPC := PaperCoords{nD,nA}

oBD:Text(oPPC,"INVOICE date :-")

nA := nBa+48.5

oPPC := PaperCoords{nD,nA}

oBD:Text(oPPC,"INVOICE no. :-")

//-------------------------------

oBD:Font := oArialBold13

oPPC := PaperCoords{nM,nA}

oBD:Text(oPPC,"From :")

oPPC := PaperCoords{nM,nBd}

oBD:Text(oPPC,"To :")

//---------- keep this for a single page ---------

e:HasMorePages := FALSE

RETURN

==========================================================================


Appendix I - Microsoft RS downloads

Navigation: »No topics above this level«

Appendix I - Microsoft RS downloads

 Return to chapter overview

Enter topic text here.


Appendix J - converting coordinates

Navigation: »No topics above this level«

Appendix J - converting coordinates

 Return to chapter overview

Enter topic text here.


AJ - Working Code

Navigation: Appendix J - converting coordinates >

AJ - Working Code

 Return to chapter overview

METHOD ConvertPageCoords2Point(oPGC AS PageCoords) AS Point

LOCAL nLine, nAcross AS Double

LOCAL nLPP AS INT

LOCAL oPoint AS Point

LOCAL nY, nX AS Double

LOCAL iY, iX AS INT

LOCAL r8Line, r8LineHeight, r8Across, r8LM, r8PCPW AS Double

LOCAL r8TM AS Double

nLPP := SELF:LinesPerPage

r8LineHeight := SELF:LineHeight

nLine := oPGC:nLine

nAcross := oPGC:nAcross

IF nLine < 0 .or. nLine > nLPP

//--------- warn box ---------

RETURN Point{0,0}

ENDIF

IF nAcross < 0 .or. nAcross > 100

//---------- warn box ---------

RETURN Point{0,0}

ENDIF

r8TM := SELF:TM

r8Line := nLine

nY := r8TM+(r8Line)*r8LineHeight

r8LM := SELF:LM

r8Across := nAcross

r8PCPW := SELF:PCPW

nX := r8LM+r8Across*r8PCPW

iY := (INT)Math.Floor(nY+0.5)

iX := (INT)Math.Floor(nX+0.5)

oPoint := Point{iX, iY}

RETURN oPoint

METHOD ConvertPaperCoords2Point(oPPC AS PaperCoords) AS Point

LOCAL nDown, nAcross AS Double

LOCAL oPoint AS Point

LOCAL nY, nX AS Double

LOCAL iY, iX AS INT

LOCAL r8LM, r8PCPW AS Double

LOCAL r8TM, r8PCPL AS Double

nDown := oPPC:nDown

nAcross := oPPC:nAcross

IF nDown < 0 .or. nDown > 100

//--------- warn box ---------

RETURN Point{0,0}

ENDIF

IF nAcross < 0 .or. nAcross > 100

//---------- warn box ---------

RETURN Point{0,0}

ENDIF

r8TM := SELF:TM

r8PCPL := SELF:PCPL

nY := r8TM+nDown*r8PCPL

r8LM := SELF:LM

r8PCPW := SELF:PCPW

nX := r8LM+nAcross*r8PCPW

iY := (INT)Math.Floor(nY+0.5)

iX := (INT)Math.Floor(nX+0.5)

oPoint := Point{iX, iY}

RETURN oPoint

METHOD ConvertPoint2PaperCoords(oPoint AS Point) AS PaperCoords

LOCAL oPPC AS PaperCoords

LOCAL nDown, nAcross AS Double

LOCAL r8TM, r8PCPL AS Double

LOCAL r8LM, r8PCPW AS Double

LOCAL nY, nX AS Double

nX := (Double)oPoint:X

nY := (Double)oPoint:Y

r8TM := SELF:TM

r8PCPL := SELF:PCPL

nDown := (nY-r8TM)/r8PCPL

r8LM := SELF:LM

r8PCPW := SELF:PCPW

nAcross := (nX-r8LM)/r8PCPW

oPPC := PaperCoords{nDown, nAcross}

RETURN oPPC

METHOD ConvertPoint2PageCoords(oPoint AS Point) AS PageCoords

LOCAL oPGC AS PageCoords

LOCAL nY, nX AS Double

LOCAL r8LM, r8PCPW, nAcross, r8TM, nLine AS Double

nX := (Double)oPoint:X

nY := (Double)oPoint:Y

r8LM := SELF:LM

r8PCPW := SELF:PCPW

nAcross := (nX-r8LM)/r8PCPW

r8TM := SELF:TM

nLine := (nY-r8TM)/SELF:LineHeight

oPGC := PageCoords{nLine, nAcross}

RETURN oPGC

METHOD ConvertPoint2GraphCoords(oPoint AS Point) AS GraphCoords

LOCAL oGC AS GraphCoords

LOCAL oPPC AS PaperCoords

oPPC := ConvertPoint2PaperCoords(oPoint)

oGC := ConvertPaperCoords2GraphCoords(oPPC)

RETURN oGC

METHOD ConvertGraphCoords2PaperCoords(oGC AS GraphCoords) AS PaperCoords

LOCAL oPPC AS PaperCoords

oPPC := PaperCoords{100-oGC:nUp, oGC:nAcross}

RETURN oPPC

METHOD ConvertPaperCoords2GraphCoords(oPPC AS PaperCoords) AS GraphCoords

LOCAL oGC AS GraphCoords

oGC := GraphCoords{100-oPPC:nDown, oPPC:nAcross}

RETURN oGC

METHOD ConvertPageCoords2PaperCoords(oPGC AS PageCoords) AS PaperCoords

LOCAL oPPC AS PaperCoords

LOCAL oPoint AS Point

oPoint := ConvertPageCoords2Point(oPGC)

oPPC := ConvertPoint2PaperCoords(oPoint)

RETURN oPPC

METHOD ConvertPaperCoords2PageCoords(oPPC AS PaperCoords) AS PageCoords

LOCAL oPGC AS PageCoords

LOCAL oPoint AS Point

oPoint := ConvertPaperCoords2Point(oPPC)

oPGC := ConvertPoint2PageCoords(oPoint)

RETURN oPGC


Working Code

Navigation: »No topics above this level«

Working Code

 Return to chapter overview

Enter topic text here.